From bd3fffbfe75ad426efb46d688ac66a74128dd273 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Wed, 26 Nov 2025 17:07:40 +0100 Subject: [PATCH 001/151] remove pizzarr for anndataR support --- DESCRIPTION | 11 ++++------- R/read.R | 9 ++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9e2249b1..615fdc6f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -63,13 +63,10 @@ Suggests: SpatialData.data, SpatialData.plot, testthat, - DT -Enhances: - anndataR, - pizzarr + DT, + anndataR Remotes: - keller-mark/pizzarr, - keller-mark/anndataR@spatialdata, + keller-mark/anndataR@keller-mark/zarr, HelenaLC/SpatialData.data, HelenaLC/SpatialData.plot biocViews: @@ -82,7 +79,7 @@ biocViews: SingleCell, Spatial License: Artistic-2.0 -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Encoding: UTF-8 VignetteBuilder: knitr URL: https://github.com/HelenaLC/SpatialData diff --git a/R/read.R b/R/read.R index 929fd140..3c951a8c 100644 --- a/R/read.R +++ b/R/read.R @@ -150,15 +150,10 @@ readShape <- function(x, ...) { .readTable_anndataR <- function(x) { if (!requireNamespace('anndataR', quietly=TRUE)) { stop("To use this function, install the 'anndataR' package via\n", - "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") - } - if (!requireNamespace('pizzarr', quietly=TRUE)) { - stop("To use this function, install the 'pizzarr' package via\n", - "`BiocManager::install(\"keller-mark/pizzarr\")`") + "`BiocManager::install(\"keller-mark/anndataR\", ref=\"keller-mark/zarr\")`") } suppressWarnings({ # suppress warnings related to hidden files - adata <- anndataR::read_zarr(x) - anndataR::to_SingleCellExperiment(adata) + adata <- anndataR::read_zarr(x, as = "SingleCellExperiment") }) } From 6b5bd3996a3575f5ab796479bbda39f57b9332fa Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 26 Nov 2025 22:12:56 +0100 Subject: [PATCH 002/151] rmv pizzarr --- R/read.R | 8 ++++---- man/SpatialData.Rd | 8 ++++---- man/readSpatialData.Rd | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/R/read.R b/R/read.R index 3c951a8c..83cb82fa 100644 --- a/R/read.R +++ b/R/read.R @@ -49,8 +49,8 @@ allp = c("session_info==1.0.0", "spatialdata==0.3.0", "spatialdata_io==0.1.7", #' The default, NULL, reads all elements; alternatively, may be FALSE #' to skip a layer, or a integer vector specifying which elements to read. #' @param anndataR logical specifying whether -#' to use \code{anndataR} to read tables; defaults to FALSE in `readSpatialData`, -#' and `readTable`, +#' to use \code{anndataR} to read tables; +#' defaults to FALSE in `readSpatialData`, and `readTable`, #' so that pythonic \code{spatialdata} and \code{zellkonverter} are used. #' @param ... option arguments passed to and from other methods. #' @@ -64,7 +64,7 @@ allp = c("session_info==1.0.0", "spatialdata==0.3.0", "spatialdata_io==0.1.7", #' library(SpatialData.data) #' dir.create(tf <- tempfile()) #' base <- SpatialData.data:::.unzip_merfish_demo(tf) -#' (x <- readSpatialData(base)) +#' (x <- readSpatialData(base, anndataR=TRUE)) NULL readsdlayer <- function(x, ...) { @@ -153,7 +153,7 @@ readShape <- function(x, ...) { "`BiocManager::install(\"keller-mark/anndataR\", ref=\"keller-mark/zarr\")`") } suppressWarnings({ # suppress warnings related to hidden files - adata <- anndataR::read_zarr(x, as = "SingleCellExperiment") + anndataR::read_zarr(x, as="SingleCellExperiment") }) } diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index e760cfd6..9521d85c 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index 000ed434..5a3d0556 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -42,8 +42,8 @@ The default, NULL, reads all elements; alternatively, may be FALSE to skip a layer, or a integer vector specifying which elements to read.} \item{anndataR}{logical specifying whether -to use \code{anndataR} to read tables; defaults to FALSE in `readSpatialData`, -and `readTable`, +to use \code{anndataR} to read tables; +defaults to FALSE in `readSpatialData`, and `readTable`, so that pythonic \code{spatialdata} and \code{zellkonverter} are used.} } \value{ @@ -59,5 +59,5 @@ Reading `SpatialData` library(SpatialData.data) dir.create(tf <- tempfile()) base <- SpatialData.data:::.unzip_merfish_demo(tf) -(x <- readSpatialData(base)) +(x <- readSpatialData(base, anndataR=TRUE)) } From 3e2995041c9f184b732d7cc05300f5153d77f284 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Mon, 12 Jan 2026 16:52:33 +0100 Subject: [PATCH 003/151] update read basilisk --- R/read.R | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/R/read.R b/R/read.R index 929fd140..45ab2a6a 100644 --- a/R/read.R +++ b/R/read.R @@ -128,25 +128,30 @@ readShape <- function(x, ...) { #' @importFrom SingleCellExperiment int_metadata #' @importFrom basilisk basiliskStart basiliskStop basiliskRun .readTables_basilisk <- function(x) { - proc <- basiliskStart(.env) - on.exit(basiliskStop(proc)) - basiliskRun(proc, x=x, \(x) { - # read in 'SpatialData' from .zarr store - sd <- import("spatialdata") - zs <- sd$read_zarr(x) - # return (named) list of SCEs - names(ts) <- ts <- names(zs$tables$data) - lapply(ts, \(z) { - se <- AnnData2SCE(zs$tables[z]) - nm <- "spatialdata_attrs" - md <- metadata(se)[[nm]] - int_metadata(se)[[nm]] <- md - metadata(se)[[nm]] <- NULL - se - }) - }) + proc <- basiliskStart(.env) + on.exit(basiliskStop(proc)) + basiliskRun(proc, x=x, \(x) { + # read in 'SpatialData' from .zarr store + # sd <- import("spatialdata") + sd <- import("anndata") + z <- import("zarr") + # zs <- sd$read_zarr(x) + # return (named) list of SCEs + # names(ts) <- ts <- names(zs$tables$data) + names(ts) <- ts <- list.dirs(file.path(x,"tables/"), + recursive = FALSE, + full.names = FALSE) + lapply(ts, \(z) { + zs <- sd$read_zarr(file.path(x, "tables", ts)) + se <- AnnData2SCE(zs) + nm <- "spatialdata_attrs" + md <- metadata(se)[[nm]] + int_metadata(se)[[nm]] <- md + metadata(se)[[nm]] <- NULL + se + }) + }) } - .readTable_anndataR <- function(x) { if (!requireNamespace('anndataR', quietly=TRUE)) { stop("To use this function, install the 'anndataR' package via\n", From 9937bd516ad7facca628a457ca9f0de96b76d809 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 15:43:39 +0100 Subject: [PATCH 004/151] add new GHA runners --- .github/workflows/check-bioc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 996b6f31..1acb0554 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -53,8 +53,8 @@ jobs: matrix: config: - { os: ubuntu-latest, r: '4.5', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } -# - { os: macOS-latest, r: '4.5', bioc: 'devel'} -# - { os: windows-latest, r: '4.5', bioc: 'devel'} + - { os: macOS-latest, r: '4.5', bioc: 'devel'} + - { os: windows-latest, r: '4.5', bioc: 'devel'} ## Check https://github.com/r-lib/actions/tree/master/examples ## for examples using the http-user-agent env: From 3b1de05239e78b2b86004b8fdd3e0f2e0968a36a Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 15:47:06 +0100 Subject: [PATCH 005/151] update r version --- .github/workflows/check-bioc.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 1acb0554..1923617c 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -52,9 +52,9 @@ jobs: fail-fast: false matrix: config: - - { os: ubuntu-latest, r: '4.5', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - - { os: macOS-latest, r: '4.5', bioc: 'devel'} - - { os: windows-latest, r: '4.5', bioc: 'devel'} + - { os: ubuntu-latest, r: '4.6', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } + - { os: macOS-latest, r: '4.6', bioc: 'devel'} + - { os: windows-latest, r: '4.6', bioc: 'devel'} ## Check https://github.com/r-lib/actions/tree/master/examples ## for examples using the http-user-agent env: From dd159cb2ebe80b56cb87c3fe296e00319adac695 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 15:51:20 +0100 Subject: [PATCH 006/151] r version --- .github/workflows/check-bioc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 1923617c..d1378fd9 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -53,8 +53,8 @@ jobs: matrix: config: - { os: ubuntu-latest, r: '4.6', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - - { os: macOS-latest, r: '4.6', bioc: 'devel'} - - { os: windows-latest, r: '4.6', bioc: 'devel'} + - { os: macOS-latest, r: 'devel', bioc: 'devel'} + - { os: windows-latest, r: 'devel', bioc: 'devel'} ## Check https://github.com/r-lib/actions/tree/master/examples ## for examples using the http-user-agent env: From af96e7f90c1940ea97659c79f26c300f721f89c7 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 16:31:16 +0100 Subject: [PATCH 007/151] fix indent --- R/read.R | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/R/read.R b/R/read.R index 45ab2a6a..dce2ec08 100644 --- a/R/read.R +++ b/R/read.R @@ -128,29 +128,29 @@ readShape <- function(x, ...) { #' @importFrom SingleCellExperiment int_metadata #' @importFrom basilisk basiliskStart basiliskStop basiliskRun .readTables_basilisk <- function(x) { - proc <- basiliskStart(.env) - on.exit(basiliskStop(proc)) - basiliskRun(proc, x=x, \(x) { - # read in 'SpatialData' from .zarr store - # sd <- import("spatialdata") - sd <- import("anndata") - z <- import("zarr") - # zs <- sd$read_zarr(x) - # return (named) list of SCEs - # names(ts) <- ts <- names(zs$tables$data) - names(ts) <- ts <- list.dirs(file.path(x,"tables/"), - recursive = FALSE, - full.names = FALSE) - lapply(ts, \(z) { - zs <- sd$read_zarr(file.path(x, "tables", ts)) - se <- AnnData2SCE(zs) - nm <- "spatialdata_attrs" - md <- metadata(se)[[nm]] - int_metadata(se)[[nm]] <- md - metadata(se)[[nm]] <- NULL - se - }) - }) + proc <- basiliskStart(.env) + on.exit(basiliskStop(proc)) + basiliskRun(proc, x=x, \(x) { + # read in 'SpatialData' from .zarr store + # sd <- import("spatialdata") + sd <- import("anndata") + z <- import("zarr") + # zs <- sd$read_zarr(x) + # return (named) list of SCEs + # names(ts) <- ts <- names(zs$tables$data) + names(ts) <- ts <- list.dirs(file.path(x,"tables/"), + recursive = FALSE, + full.names = FALSE) + lapply(ts, \(z) { + zs <- sd$read_zarr(file.path(x, "tables", ts)) + se <- AnnData2SCE(zs) + nm <- "spatialdata_attrs" + md <- metadata(se)[[nm]] + int_metadata(se)[[nm]] <- md + metadata(se)[[nm]] <- NULL + se + }) + }) } .readTable_anndataR <- function(x) { if (!requireNamespace('anndataR', quietly=TRUE)) { From 2a2d892217257b81a990fb18209130281073f8d8 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 16:48:44 +0100 Subject: [PATCH 008/151] fix lapply bug in readBasilisk --- .Rbuildignore | 2 ++ .gitignore | 2 ++ R/read.R | 8 ++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 824fa780..c5b2efa1 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,3 +4,5 @@ vignettes/*html vignettes/*cache vignettes/*files +^doc$ +^Meta$ diff --git a/.gitignore b/.gitignore index 7ac4af56..ad437a89 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ inst/extdata/xenium1.zarr inst/extdata/visiumhd.zarr *.Rproj *.html +/doc/ +/Meta/ diff --git a/R/read.R b/R/read.R index dce2ec08..bead6e4f 100644 --- a/R/read.R +++ b/R/read.R @@ -132,17 +132,13 @@ readShape <- function(x, ...) { on.exit(basiliskStop(proc)) basiliskRun(proc, x=x, \(x) { # read in 'SpatialData' from .zarr store - # sd <- import("spatialdata") sd <- import("anndata") - z <- import("zarr") - # zs <- sd$read_zarr(x) - # return (named) list of SCEs - # names(ts) <- ts <- names(zs$tables$data) + za <- import("zarr") names(ts) <- ts <- list.dirs(file.path(x,"tables/"), recursive = FALSE, full.names = FALSE) lapply(ts, \(z) { - zs <- sd$read_zarr(file.path(x, "tables", ts)) + zs <- sd$read_zarr(file.path(x, "tables", z)) se <- AnnData2SCE(zs) nm <- "spatialdata_attrs" md <- metadata(se)[[nm]] From fcea8955cf5244db1e03a8d8d0e726596da13573 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 16:51:01 +0100 Subject: [PATCH 009/151] fix comments --- R/read.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/read.R b/R/read.R index bead6e4f..0ae40ec2 100644 --- a/R/read.R +++ b/R/read.R @@ -131,9 +131,10 @@ readShape <- function(x, ...) { proc <- basiliskStart(.env) on.exit(basiliskStop(proc)) basiliskRun(proc, x=x, \(x) { - # read in 'SpatialData' from .zarr store + # read in 'AnnData' tables from .zarr store sd <- import("anndata") za <- import("zarr") + # return (named) list of SCEs names(ts) <- ts <- list.dirs(file.path(x,"tables/"), recursive = FALSE, full.names = FALSE) From 5faf495daccfa79dc09cb9031b98a65e56f79201 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 16:52:09 +0100 Subject: [PATCH 010/151] more redundant fixes --- .Rbuildignore | 4 +- .gitignore | 4 +- Meta/vignette.rds | Bin 0 -> 197 bytes doc/SpatialData.R | 70 ++++++++++++++++++++ doc/SpatialData.Rmd | 157 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 Meta/vignette.rds create mode 100644 doc/SpatialData.R create mode 100644 doc/SpatialData.Rmd diff --git a/.Rbuildignore b/.Rbuildignore index c5b2efa1..448e34d1 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,6 +3,4 @@ ^\.github$ vignettes/*html vignettes/*cache -vignettes/*files -^doc$ -^Meta$ +vignettes/*files \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad437a89..af4f7808 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,4 @@ inst/extdata/mibitof.zarr inst/extdata/xenium1.zarr inst/extdata/visiumhd.zarr *.Rproj -*.html -/doc/ -/Meta/ +*.html \ No newline at end of file diff --git a/Meta/vignette.rds b/Meta/vignette.rds new file mode 100644 index 0000000000000000000000000000000000000000..1e01f0673518e3f5073ec7d2250b80f92491e5de GIT binary patch literal 197 zcmV;$06PC4iwFP!000001B>8dU|?WkU||K4%s?iyFpyvaVgVp#WME<71k(J$1&Jk@ zi8(HbC5d`LxhZJU+(^=BQUb_2GfHxE(ByehOa{6^7%T}<1OzNhNQznW5_40F(M(`> zNi9gtO92W0!>*t4KbjT}@6^ij{34hh6g6y6H7stKIjJyvLo!R?9OeKQHz<=a2;B`B z=A*cuvnan@59SA6P`Eb$iT^-=?$P9&#A0-ha;1R6D6I&nALMQT2} Date: Thu, 29 Jan 2026 16:53:10 +0100 Subject: [PATCH 011/151] add back some lines --- .Rbuildignore | 2 +- .gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 448e34d1..824fa780 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,4 +3,4 @@ ^\.github$ vignettes/*html vignettes/*cache -vignettes/*files \ No newline at end of file +vignettes/*files diff --git a/.gitignore b/.gitignore index af4f7808..7ac4af56 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ inst/extdata/mibitof.zarr inst/extdata/xenium1.zarr inst/extdata/visiumhd.zarr *.Rproj -*.html \ No newline at end of file +*.html From cdf4eb83507f822030956584880d6b7da8e9e41c Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 29 Jan 2026 16:54:29 +0100 Subject: [PATCH 012/151] remove doc and meta --- Meta/vignette.rds | Bin 197 -> 0 bytes doc/SpatialData.R | 70 -------------------- doc/SpatialData.Rmd | 157 -------------------------------------------- 3 files changed, 227 deletions(-) delete mode 100644 Meta/vignette.rds delete mode 100644 doc/SpatialData.R delete mode 100644 doc/SpatialData.Rmd diff --git a/Meta/vignette.rds b/Meta/vignette.rds deleted file mode 100644 index 1e01f0673518e3f5073ec7d2250b80f92491e5de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197 zcmV;$06PC4iwFP!000001B>8dU|?WkU||K4%s?iyFpyvaVgVp#WME<71k(J$1&Jk@ zi8(HbC5d`LxhZJU+(^=BQUb_2GfHxE(ByehOa{6^7%T}<1OzNhNQznW5_40F(M(`> zNi9gtO92W0!>*t4KbjT}@6^ij{34hh6g6y6H7stKIjJyvLo!R?9OeKQHz<=a2;B`B z=A*cuvnan@59SA6P`Eb$iT^-=?$P9&#A0-ha;1R6D6I&nALMQT2} Date: Wed, 11 Feb 2026 17:26:38 +0100 Subject: [PATCH 013/151] rmv magick (#129) --- .github/workflows/check-bioc.yml | 6 +++--- DESCRIPTION | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 996b6f31..e3f41cb5 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -130,9 +130,9 @@ jobs: brew install libxml2 echo "XML_CONFIG=/usr/local/opt/libxml2/bin/xml2-config" >> $GITHUB_ENV - ## Required to install magick as noted at - ## https://github.com/r-lib/usethis/commit/f1f1e0d10c1ebc75fd4c18fa7e2de4551fd9978f#diff-9bfee71065492f63457918efcd912cf2 - brew install imagemagick@6 + ## ## Required to install magick as noted at + ## ## https://github.com/r-lib/usethis/commit/f1f1e0d10c1ebc75fd4c18fa7e2de4551fd9978f#diff-9bfee71065492f63457918efcd912cf2 + ## brew install imagemagick@6 ## For textshaping, required by ragg, and required by pkgdown brew install harfbuzz fribidi diff --git a/DESCRIPTION b/DESCRIPTION index 9e2249b1..746666db 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -56,7 +56,6 @@ Suggests: BiocStyle, ggnewscale, knitr, - magick, patchwork, paws, Rgraphviz, From 5835aac4e993e693a52fd1a61dd3e5386acda3a4 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 26 Feb 2026 14:20:29 +0100 Subject: [PATCH 014/151] update basilisk env to latest sd version --- DESCRIPTION | 4 +-- R/read.R | 56 ++++++++++++++++++++------------------- vignettes/SpatialData.Rmd | 2 +- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9e2249b1..bf7ee952 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R -Depends: R (>= 4.4) -Version: 0.99.23 +Depends: R (>= 4.5) +Version: 0.99.24 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufracturer data and writing to .zarr, on-disk representation of images/labels as diff --git a/R/read.R b/R/read.R index 929fd140..11f03dc3 100644 --- a/R/read.R +++ b/R/read.R @@ -1,30 +1,32 @@ # -allp = c("session_info==1.0.0", "spatialdata==0.3.0", "spatialdata_io==0.1.7", -"pillow==11.1.0", "anndata==0.11.3", "annotated_types==0.7.0", "asciitree==0.3.3", -"attr==0.3.2", "certifi==2025.01.31", "charset_normalizer==3.4.1", -"click==8.1.8", "cloudpickle==3.1.1", "cycler==0.12.1", "dask==2024.4.1", -"dask_image==2024.5.3", "datashader==0.17.0", -"deprecated==1.2.18", "distributed==2024.4.1", -"flowio==1.3.0", "fsspec==2025.2.0", "geopandas==1.0.1", "h5py==3.12.1", -"idna==3.10", "imagecodecs==2024.12.30", "imageio==2.37.0", "jinja2==3.1.5", -"joblib==1.4.2", "kiwisolver==1.4.8", "lazy_loader==0.4", "legacy_api_wrap==1.4.1", -"llvmlite==0.44.0", "locket==1.0.0", "markupsafe==3.0.2", "matplotlib==3.10.0", -"more_itertools==10.3.0", "msgpack==1.1.0", "multipledispatch==0.6.0", -"multiscale_spatial_image==2.0.2", "natsort==8.4.0", "networkx==3.4.2", -"numba==0.61.0", "numcodecs==0.15.1", "numpy==2.1.3", "ome_types==0.5.3", -"ome_zarr==0.10.3", "packaging==24.2", "pandas==2.2.3", "param==2.2.0", -"pims==0.7", "platformdirs==4.3.6", "psutil==7.0.0", "pyarrow==19.0.0", -"pyct==0.5.0", "pydantic==2.10.6", "pydantic_compat==0.1.2", -"pydantic_core==2.27.2", "pygments==2.19.1", "pyparsing==3.2.1", -"pyproj==3.7.0", "pytz==2025.1", "readfcs==2.0.1", "requests==2.32.3", -"rich==13.9.4", "scanpy==1.11.0", "scipy==1.15.1", "setuptools==75.8.0", -"shapely==2.0.7", "six==1.17.0", "scikit-image==0.25.1", "scikit-learn==1.5.2", -"slicerator==1.1.0", "sortedcontainers==2.4.0", "spatial_image==1.1.0", -"tblib==3.0.0", "threadpoolctl==3.5.0", "tifffile==2025.1.10", -"toolz==1.0.0", "tornado==6.4.2", "tqdm==4.67.1", -"typing_extensions==4.12.2", "urllib3==2.3.0", "wrapt==1.17.2", -"xarray==2024.11.0", "xarray_dataclasses==1.9.1", "xarray_schema==0.0.3", -"zarr==2.18.4", "zict==3.0.0") +# allp = c("session_info==1.0.0", "spatialdata==0.3.0", "spatialdata_io==0.1.7", +# "pillow==11.1.0", "anndata==0.11.3", "annotated_types==0.7.0", "asciitree==0.3.3", +# "attr==0.3.2", "certifi==2025.01.31", "charset_normalizer==3.4.1", +# "click==8.1.8", "cloudpickle==3.1.1", "cycler==0.12.1", "dask==2024.4.1", +# "dask_image==2024.5.3", "datashader==0.17.0", +# "deprecated==1.2.18", "distributed==2024.4.1", +# "flowio==1.3.0", "fsspec==2025.2.0", "geopandas==1.0.1", "h5py==3.12.1", +# "idna==3.10", "imagecodecs==2024.12.30", "imageio==2.37.0", "jinja2==3.1.5", +# "joblib==1.4.2", "kiwisolver==1.4.8", "lazy_loader==0.4", "legacy_api_wrap==1.4.1", +# "llvmlite==0.44.0", "locket==1.0.0", "markupsafe==3.0.2", "matplotlib==3.10.0", +# "more_itertools==10.3.0", "msgpack==1.1.0", "multipledispatch==0.6.0", +# "multiscale_spatial_image==2.0.2", "natsort==8.4.0", "networkx==3.4.2", +# "numba==0.61.0", "numcodecs==0.15.1", "numpy==2.1.3", "ome_types==0.5.3", +# "ome_zarr==0.10.3", "packaging==24.2", "pandas==2.2.3", "param==2.2.0", +# "pims==0.7", "platformdirs==4.3.6", "psutil==7.0.0", "pyarrow==19.0.0", +# "pyct==0.5.0", "pydantic==2.10.6", "pydantic_compat==0.1.2", +# "pydantic_core==2.27.2", "pygments==2.19.1", "pyparsing==3.2.1", +# "pyproj==3.7.0", "pytz==2025.1", "readfcs==2.0.1", "requests==2.32.3", +# "rich==13.9.4", "scanpy==1.11.0", "scipy==1.15.1", "setuptools==75.8.0", +# "shapely==2.0.7", "six==1.17.0", "scikit-image==0.25.1", "scikit-learn==1.5.2", +# "slicerator==1.1.0", "sortedcontainers==2.4.0", "spatial_image==1.1.0", +# "tblib==3.0.0", "threadpoolctl==3.5.0", "tifffile==2025.1.10", +# "toolz==1.0.0", "tornado==6.4.2", "tqdm==4.67.1", +# "typing_extensions==4.12.2", "urllib3==2.3.0", "wrapt==1.17.2", +# "xarray==2024.11.0", "xarray_dataclasses==1.9.1", "xarray_schema==0.0.3", +# "zarr==2.18.4", "zict==3.0.0") +allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", + "spatialdata_plot==0.2.14", "setuptools==75.8.0") # notes from VJC -- readSpatialData was modified below so # that if anndataR = FALSE, spatialdata.read_zarr is used # to get the whole zarr store, and then the tables are @@ -119,7 +121,7 @@ readShape <- function(x, ...) { #' @importFrom basilisk BasiliskEnvironment .env <- BasiliskEnvironment( pkgname="SpatialData", envname="anndata_env", - packages=c( "python==3.12.0", "zarr==2.18.4" ), + packages=c( "python==3.13.0"), pip=allp) #' @importFrom reticulate import diff --git a/vignettes/SpatialData.Rmd b/vignettes/SpatialData.Rmd index 956096ff..25c40d6c 100644 --- a/vignettes/SpatialData.Rmd +++ b/vignettes/SpatialData.Rmd @@ -22,7 +22,7 @@ knitr::opts_chunk$set(cache=FALSE, message=FALSE, warning=FALSE) ``` ```{r load-libs} -library(SpatialData) +# library(SpatialData) library(SingleCellExperiment) ``` From b32a65e87c239c53e250f3de9cd132f5156f8d91 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 26 Feb 2026 15:18:00 +0100 Subject: [PATCH 015/151] library in rmd --- vignettes/SpatialData.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/SpatialData.Rmd b/vignettes/SpatialData.Rmd index 25c40d6c..956096ff 100644 --- a/vignettes/SpatialData.Rmd +++ b/vignettes/SpatialData.Rmd @@ -22,7 +22,7 @@ knitr::opts_chunk$set(cache=FALSE, message=FALSE, warning=FALSE) ``` ```{r load-libs} -# library(SpatialData) +library(SpatialData) library(SingleCellExperiment) ``` From 2b040ef1484a431193f5bf6aa1aa3f578ccdbd04 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Thu, 26 Feb 2026 16:40:36 +0100 Subject: [PATCH 016/151] comment out a zarr v3 example --- DESCRIPTION | 2 +- R/tx_to_ext.R | 15 +++++++++------ man/SpatialData.Rd | 8 ++++---- man/do_tx_to_ext.Rd | 15 +++++++++------ 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index bf7ee952..ef71bfa5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -82,7 +82,7 @@ biocViews: SingleCell, Spatial License: Artistic-2.0 -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Encoding: UTF-8 VignetteBuilder: knitr URL: https://github.com/HelenaLC/SpatialData diff --git a/R/tx_to_ext.R b/R/tx_to_ext.R index 96128c7c..f0149435 100644 --- a/R/tx_to_ext.R +++ b/R/tx_to_ext.R @@ -24,12 +24,15 @@ #' @examples #' src <- system.file("extdata", "blobs.zarr", package="SpatialData") #' td <- tempfile() -#' do_tx_to_ext( -#' srcdir=src, dest=td, -#' coordinate_system="global", -#' maintain_positioning=FALSE, -#' target_width=400.) -#' (sd <- readSpatialData(td)) +#' # TODO: for now this example converts to a zarr v3 so we comment out, +#' # check again later +#' +#' # do_tx_to_ext( +#' # srcdir=src, dest=td, +#' # coordinate_system="global", +#' # maintain_positioning=FALSE, +#' # target_width=400.) +#' # (sd <- readSpatialData(td)) #' #' @export do_tx_to_ext <- function(srcdir, dest, coordinate_system, ...) { diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index e760cfd6..9521d85c 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/do_tx_to_ext.Rd b/man/do_tx_to_ext.Rd index 198d6149..da7c529f 100644 --- a/man/do_tx_to_ext.Rd +++ b/man/do_tx_to_ext.Rd @@ -23,11 +23,14 @@ Use Python's 'spatialdata' 'transform_to_data_extent' on a spatialdata zarr stor \examples{ src <- system.file("extdata", "blobs.zarr", package="SpatialData") td <- tempfile() -do_tx_to_ext( - srcdir=src, dest=td, - coordinate_system="global", - maintain_positioning=FALSE, - target_width=400.) -(sd <- readSpatialData(td)) +# TODO: for now this example converts to a zarr v3 so we comment out, +# check again later + +# do_tx_to_ext( +# srcdir=src, dest=td, +# coordinate_system="global", +# maintain_positioning=FALSE, +# target_width=400.) +# (sd <- readSpatialData(td)) } From 1e757c656c13033945006eefa8e3542ea5e94d61 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Tue, 17 Mar 2026 22:46:13 +0100 Subject: [PATCH 017/151] switch from Rarr to ZarrArray --- DESCRIPTION | 5 +++-- NAMESPACE | 2 +- R/ImageArray.R | 2 +- R/LabelArray.R | 2 +- R/read.R | 5 ++--- man/ImageArray.Rd | 2 +- man/LabelArray.Rd | 2 +- man/SpatialData.Rd | 8 ++++---- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b09a82fa..add5c001 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,7 +43,7 @@ Imports: jsonlite, Matrix, methods, - Rarr, + ZarrArray, RBGL, reticulate, sf, @@ -70,7 +70,8 @@ Remotes: keller-mark/pizzarr, keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, - HelenaLC/SpatialData.plot + HelenaLC/SpatialData.plot, + Bioconductor/ZarrArray biocViews: DataImport, DataRepresentation, diff --git a/NAMESPACE b/NAMESPACE index 2f78dffc..a090f831 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -91,7 +91,6 @@ importFrom(Matrix,rowSums) importFrom(Matrix,sparseVector) importFrom(Matrix,t) importFrom(RBGL,sp.between) -importFrom(Rarr,ZarrArray) importFrom(S4Arrays,as.array.Array) importFrom(S4Vectors,"metadata<-") importFrom(S4Vectors,coolcat) @@ -107,6 +106,7 @@ importFrom(SingleCellExperiment,int_metadata) importFrom(SummarizedExperiment,"colData<-") importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,colData) +importFrom(ZarrArray,ZarrArray) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) importFrom(basilisk,basiliskRun) diff --git a/R/ImageArray.R b/R/ImageArray.R index c54901e7..583fb210 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -2,7 +2,7 @@ #' @title The `ImageArray` class #' #' @param x \code{ImageArray} -#' @param data list of \code{\link[Rarr]{ZarrArray}}s +#' @param data list of \code{\link[ZarrArray]{ZarrArray}}s #' @param meta \code{\link{Zattrs}} #' @param metadata optional list of arbitrary #' content describing the overall object. diff --git a/R/LabelArray.R b/R/LabelArray.R index 64ed3f67..519fc7c2 100644 --- a/R/LabelArray.R +++ b/R/LabelArray.R @@ -14,7 +14,7 @@ #' } #' #' @param x \code{LabelArray} -#' @param data list of \code{\link[Rarr]{ZarrArray}}s +#' @param data list of \code{\link[ZarrArray]{ZarrArray}}s #' @param meta \code{\link{Zattrs}} #' @param metadata optional list of arbitrary #' content describing the overall object. diff --git a/R/read.R b/R/read.R index 11f03dc3..de5e9606 100644 --- a/R/read.R +++ b/R/read.R @@ -69,15 +69,15 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", #' (x <- readSpatialData(base)) NULL +#' @importFrom ZarrArray ZarrArray readsdlayer <- function(x, ...) { md <- fromJSON(file.path(x, ".zattrs")) ps <- .get_multiscales_dataset_paths(md) - list(array = lapply(ps, \(.) ZarrArray(file.path(x, as.character(.)))), + list(array = lapply(ps, \(.) ZarrArray::ZarrArray(file.path(x, as.character(.)))), md = md) } #' @rdname readSpatialData -#' @importFrom Rarr ZarrArray #' @importFrom jsonlite fromJSON #' @export readImage <- function(x, ...) { @@ -86,7 +86,6 @@ readImage <- function(x, ...) { } #' @rdname readSpatialData -#' @importFrom Rarr ZarrArray #' @importFrom jsonlite fromJSON #' @export readLabel <- function(x, ...) { diff --git a/man/ImageArray.Rd b/man/ImageArray.Rd index 85160a3e..96c1b35a 100644 --- a/man/ImageArray.Rd +++ b/man/ImageArray.Rd @@ -17,7 +17,7 @@ ImageArray(data = list(), meta = Zattrs(), metadata = list(), ...) \S4method{[}{ImageArray,ANY,ANY,ANY}(x, i, j, k, ..., drop = FALSE) } \arguments{ -\item{data}{list of \code{\link[Rarr]{ZarrArray}}s} +\item{data}{list of \code{\link[ZarrArray]{ZarrArray}}s} \item{meta}{\code{\link{Zattrs}}} diff --git a/man/LabelArray.Rd b/man/LabelArray.Rd index 44f557f5..2d9c7f17 100644 --- a/man/LabelArray.Rd +++ b/man/LabelArray.Rd @@ -10,7 +10,7 @@ LabelArray(data = array(), meta = Zattrs(), metadata = list(), ...) \S4method{[}{LabelArray,ANY,ANY,ANY}(x, i, j, ..., drop = FALSE) } \arguments{ -\item{data}{list of \code{\link[Rarr]{ZarrArray}}s} +\item{data}{list of \code{\link[ZarrArray]{ZarrArray}}s} \item{meta}{\code{\link{Zattrs}}} diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 9521d85c..e760cfd6 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From 3758cb7419049bd10b3ff677a53c1cef028f8eda Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 00:22:13 +0100 Subject: [PATCH 018/151] add release GHA runner --- .github/workflows/check-bioc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index e36c8c84..b3830dc5 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -52,6 +52,7 @@ jobs: fail-fast: false matrix: config: + - { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - { os: ubuntu-latest, r: '4.6', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - { os: macOS-latest, r: 'devel', bioc: 'devel'} - { os: windows-latest, r: 'devel', bioc: 'devel'} From e4762bd12200b2a60d353285b1630b9f71b526c6 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sun, 29 Mar 2026 10:27:57 +0200 Subject: [PATCH 019/151] remove pizzarr everywhere --- DESCRIPTION | 4 +--- R/read.R | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index add5c001..7998ae0d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -64,10 +64,8 @@ Suggests: testthat, DT Enhances: - anndataR, - pizzarr + anndataR Remotes: - keller-mark/pizzarr, keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, HelenaLC/SpatialData.plot, diff --git a/R/read.R b/R/read.R index de5e9606..868aab18 100644 --- a/R/read.R +++ b/R/read.R @@ -153,10 +153,6 @@ readShape <- function(x, ...) { stop("To use this function, install the 'anndataR' package via\n", "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") } - if (!requireNamespace('pizzarr', quietly=TRUE)) { - stop("To use this function, install the 'pizzarr' package via\n", - "`BiocManager::install(\"keller-mark/pizzarr\")`") - } suppressWarnings({ # suppress warnings related to hidden files adata <- anndataR::read_zarr(x) anndataR::to_SingleCellExperiment(adata) From 39fd19fb24198d27b728df3f530108ab6cab3824 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sun, 29 Mar 2026 12:42:09 +0200 Subject: [PATCH 020/151] read_zarr_attributes rewrite --- DESCRIPTION | 6 +- NAMESPACE | 2 +- R/ImageArray.R | 65 +++++++++------------- R/coord_utils.R | 121 +++++++++++++++-------------------------- R/read.R | 41 +++++++++----- R/sdArray.R | 10 +++- man/Array-methods.Rd | 10 +++- man/SpatialData.Rd | 8 +-- man/coord-utils.Rd | 18 +++--- man/readSpatialData.Rd | 14 ++++- 10 files changed, 144 insertions(+), 151 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7998ae0d..fec55d8d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,10 +40,10 @@ Imports: dplyr, geoarrow, graph, - jsonlite, Matrix, methods, ZarrArray, + Rarr, RBGL, reticulate, sf, @@ -64,8 +64,10 @@ Suggests: testthat, DT Enhances: - anndataR + anndataR, + pizzarr Remotes: + keller-mark/pizzarr, keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, HelenaLC/SpatialData.plot, diff --git a/NAMESPACE b/NAMESPACE index a090f831..1567a7e0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -91,6 +91,7 @@ importFrom(Matrix,rowSums) importFrom(Matrix,sparseVector) importFrom(Matrix,t) importFrom(RBGL,sp.between) +importFrom(Rarr,read_zarr_attributes) importFrom(S4Arrays,as.array.Array) importFrom(S4Vectors,"metadata<-") importFrom(S4Vectors,coolcat) @@ -130,7 +131,6 @@ importFrom(graph,graph.par) importFrom(graph,graphAM) importFrom(graph,nodeData) importFrom(graph,nodes) -importFrom(jsonlite,fromJSON) importFrom(methods,as) importFrom(methods,callNextMethod) importFrom(methods,is) diff --git a/R/ImageArray.R b/R/ImageArray.R index 583fb210..4355d22b 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -40,52 +40,39 @@ setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) #' @importFrom S4Vectors isSequence .get_multiscales_dataset_paths <- function(md) { - - # validate multiscales attributes + # validate 'multiscales' .validate_multiscales_dataset_path(md) - - # get paths - paths <- md$multiscales$datasets[[1]]$path - paths <- suppressWarnings({as.numeric(sort(paths, decreasing=FALSE))}) - - # TODO: how to check if a vector of values here are integers - # check paths and return - # if(all(paths %% 0 == 0)){ - # if(S4Vectors::isSequence(paths)) - # return(paths) - # } - return(paths) - - # stop if not a sequence of integers - stop("ImageArray paths are ill-defined, should be e.g. 0,1,2, ..., n") + # get & validate 'path's + ps <- md$multiscales[[1]]$datasets[[1]]$path + ps <- suppressWarnings(as.numeric(sort(ps, decreasing=FALSE))) + if (length(ps)) { + qs <- seq(min(ps), max(ps)) + if (!all.equal(ps, qs)) + stop("ImageArray paths are ill-defined, should", + " be an integer sequence, e.g., 0,1,...,n") + } + return(ps) } #' @noRd .validate_multiscales_dataset_path <- function(md) { # validate 'multiscales' - if ("multiscales" %in% names(md)) { - ms <- md[["multiscales"]] - + ms <- md$multiscales + if (!is.null(ms)) { # validate 'datasets' - if("datasets" %in% names(ms)) { - ds <- ms[["datasets"]] - - # validate 'paths' - valid <- vapply(ds, \(ds) "path" %in% colnames(ds), logical(1)) - - if (!all(valid)) { - stop("'ImageArray' paths are ill-defined,", - " no 'path' attribute under 'multiscale-datasets'") - } - - } else { - stop("'ImageArray' paths are ill-defined,", - " no 'datasets' attribute under 'multiscale'") - } - } else { - stop("'ImageArray' paths are ill-defined,", - " no 'multiscales' attribute under '.zattrs'") - } + ds <- ms[[1]]$datasets + if (!is.null(ds)) { + # validate 'paths' + ok <- vapply(ds, \(.) !is.null(.$path), logical(1)) + if (!all(ok)) + stop("'ImageArray' paths are ill-defined,", + " no 'path' attribute under 'multiscale-datasets'") + } else stop( + "'ImageArray' paths are ill-defined,", + " no 'datasets' attribute under 'multiscale'") + } else stop( + "'ImageArray' paths are ill-defined,", + " no 'multiscales' attribute under '.zattrs'") } .check_jk <- \(x, .) { diff --git a/R/coord_utils.R b/R/coord_utils.R index 4dbe2c42..b29d9b73 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -29,8 +29,8 @@ #' # retrieve transformation from element to target space #' CTpath(x, "blobs_labels", "sequence") #' -#' # view available coordinate transformations -#' CTdata(z <- meta(label(x))) +#' # view available target coordinate systems +#' CTname(z <- meta(label(x))) #' #' # add #' addCT(z, "scale", "scale", c(12, 34)) # can't overwrite @@ -53,9 +53,9 @@ NULL #' @rdname coord-utils #' @export setMethod("axes", "Zattrs", \(x, ...) { - if (!is.null(ms <- x$multiscales)) x <- ms + if (!is.null(ms <- x$multiscales)) x <- ms[[1]] if (is.null(x <- x$axes)) stop("couldn't find 'axes'") - if (is.character(x)) x else x[[1]] + data.frame(do.call(rbind, x)) }) #' @rdname coord-utils @@ -66,28 +66,23 @@ setMethod("axes", "SpatialDataElement", \(x, ...) axes(meta(x))) #' @rdname coord-utils #' @export -setMethod("CTdata", "Zattrs", \(x, ...) { - ms <- x$multiscales - if (!is.null(ms)) x <- ms - x <- x$coordinateTransformations - if (is.null(dim(x))) x[[1]] else x -}) +setMethod("CTdata", "Zattrs", \(x, ...) x$multiscales[[1]]$coordinateTransformations) #' @rdname coord-utils #' @export -setMethod("CTdata", "SpatialDataElement", \(x, ...) CTdata(meta(x))) +setMethod("CTtype", "Zattrs", \(x, ...) vapply(CTdata(x), \(.) .$type, character(1))) #' @rdname coord-utils #' @export -setMethod("CTtype", "Zattrs", \(x, ...) CTdata(x)$type) +setMethod("CTname", "Zattrs", \(x, ...) vapply(CTdata(x), \(.) .$output$name, character(1))) #' @rdname coord-utils #' @export -setMethod("CTtype", "SpatialDataElement", \(x, ...) CTtype(meta(x))) +setMethod("CTdata", "SpatialDataElement", \(x, ...) CTdata(meta(x))) #' @rdname coord-utils #' @export -setMethod("CTname", "Zattrs", \(x, ...) CTdata(x)$output$name) +setMethod("CTtype", "SpatialDataElement", \(x, ...) CTtype(meta(x))) #' @rdname coord-utils #' @export @@ -139,41 +134,40 @@ setMethod("CTgraph", "ANY", \(x) stop("'x' should be a", for (l in names(md)) for (e in names(md[[l]])) { .md <- md[[l]][[e]] ms <- .md$multiscales - if (!is.null(ms)) .md <- ms + if (!is.null(ms)) .md <- ms[[1]] ct <- .md$coordinateTransformations - ct <- if (length(ct) == 1) ct[[1]] else ct g <- addNode(e, g) nodeData(g, e, "type") <- "element" - for (i in seq(nrow(ct))) { - n <- ct$output$name[i] + for (i in seq_along(ct)) { + n <- ct[[i]]$output$name if (!n %in% nodes(g)) { g <- addNode(n, g) nodeData(g, n, "type") <- "space" } - t <- ct$type[i] + t <- ct[[i]]$type if (t == "sequence") { - sq <- ct$transformations[i][[1]] + sq <- ct[[i]]$transformations . <- e - for (j in seq(nrow(sq))) { - if (j == nrow(sq)) { + for (j in seq_along(sq)) { + if (j == length(sq)) { m <- n } else { m <- paste(e, n, j, sep="_") g <- addNode(m, g) nodeData(g, m, "type") <- "none" } - t <- sq$type[j] - d <- sq[[t]][j] + t <- sq[[j]]$type + d <- sq[[j]][[t]] g <- addEdge(., m, g) edgeData(g, ., m, "type") <- t - edgeData(g, ., m, "data") <- d + edgeData(g, ., m, "data") <- list(d) . <- m } } else { g <- addEdge(e, n, g) - d <- ct[[ct$type[i]]][i] + d <- ct[[i]][[ct[[i]]$type]] edgeData(g, e, n, "type") <- t - edgeData(g, e, n, "data") <- d + edgeData(g, e, n, "data") <- list(d) } } } @@ -267,67 +261,38 @@ setMethod("addCT", "SpatialDataElement", \(x, name, type, data) { #' @rdname coord-utils #' @export -setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { +setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL, force=FALSE) { stopifnot( is.character(name), length(name) == 1, is.character(type), length(type) == 1) .check_ct(x, type, data) + # don't overwrite + ow <- match(name, CTname(x)) + if (!is.na(ow)) { + if (!isTRUE(force)) + stop("CT of 'name' ", name, " already exists;", + " use 'force=TRUE' to force overwrite.") + } + # use existing as skeleton + old <- CTdata(x) + new <- old[[1]][c("input", "output", "type")] + new$type <- type + new$output$name <- name + new[[new$type]] <- list(data) + # append/overwrite & stash ms <- "multiscales" - ts <- "transformations" ct <- "coordinateTransformations" - # use existing as skeleton - fd <- (df <- CTdata(x))[1, ] - fd <- fd[, c("input", "output", "type")] - fd$type <- type - fd$output$name <- name - fd[[fd$type]] <- list(data) - # append to existing if 'name' already present - idx <- match(name, CTname(x)) - typ <- CTtype(x)[idx] - if (!is.na(typ) && typ == "identity") { - df <- df[0, ] - app <- FALSE - } else if (app <- !is.na(idx)) { - if (seq <- (typ == "sequence")) { - df <- df[idx, ][[ts]][[1]] - fd$output$name <- df$output$name[1] - } else { - df <- df[idx, ] - if (is.null(df[[ts]])) { - - } else { - df[[ts]][[1]] <- df - } - # fd$type <- type - # fd[[fd$type]] <- list(data) - } + if (is.na(ow)) { + new <- c(old, list(new)) } else { - # fd$type <- type - # fd[[fd$type]] <- list(data) + old[[ow]] <- new + new <- old } - na <- setdiff(names(df), names(fd)) - for (. in na) fd[[.]] <- list(NULL) - na <- setdiff(names(fd), names(df)) - for (. in na) df[[.]] <- if (nrow(df) > 0) list(NULL) else list() - fd <- fd[, names(col) <- col <- names(df)] - # combine - if (app && !seq) { - # append to other - rownames(df) <- rownames(df$input) <- rownames(df$output) <- 1 - rownames(fd) <- rownames(fd$input) <- rownames(fd$output) <- 2 + if (is.null(x[[ms]])) { + x[[ct]] <- new } else { - # append to table or sequence - rownames(fd$input) <- rownames(fd$output) <- nrow(df)+1 + x[[ms]][[1]][[ct]] <- new } - new <- rbind(df, fd) - if (is_ms <- !is.null(x[[ms]])) { - .x <- x[[ms]][[ct]][[1]] - } else .x <- x[[ct]] - if (app) { - .x$type[idx] <- "sequence" - .x[idx, ]$transformations[[1]] <- new - } else .x <- new - if (is_ms) x[[ms]][[ct]][[1]] <- .x else x[[ct]] <- .x return(x) }) diff --git a/R/read.R b/R/read.R index 868aab18..3d005780 100644 --- a/R/read.R +++ b/R/read.R @@ -65,20 +65,31 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", #' @examples #' library(SpatialData.data) #' dir.create(tf <- tempfile()) -#' base <- SpatialData.data:::.unzip_merfish_demo(tf) -#' (x <- readSpatialData(base)) +#' zs <- SpatialData.data:::.unzip_merfish_demo(tf) +#' +#' # read complete Zarr store +#' (sd <- readSpatialData(zs, anndataR=TRUE)) +#' +#' # helper that gets path to first element in layer 'l' +#' fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] +#' +#' # read individual element +#' readImage(fn("images")) +#' readShape(fn("shapes")) +#' readPoint(fn("points")) NULL +#' @importFrom Rarr read_zarr_attributes #' @importFrom ZarrArray ZarrArray readsdlayer <- function(x, ...) { - md <- fromJSON(file.path(x, ".zattrs")) - ps <- .get_multiscales_dataset_paths(md) - list(array = lapply(ps, \(.) ZarrArray::ZarrArray(file.path(x, as.character(.)))), - md = md) + md <- read_zarr_attributes(x) + ps <- .get_multiscales_dataset_paths(md) + ps <- file.path(x, as.character(ps)) + as <- lapply(ps, ZarrArray) + list(array=as, md=md) } #' @rdname readSpatialData -#' @importFrom jsonlite fromJSON #' @export readImage <- function(x, ...) { lyrs <- readsdlayer(x, ...) @@ -86,7 +97,6 @@ readImage <- function(x, ...) { } #' @rdname readSpatialData -#' @importFrom jsonlite fromJSON #' @export readLabel <- function(x, ...) { lyrs <- readsdlayer(x, ...) @@ -94,23 +104,23 @@ readLabel <- function(x, ...) { } #' @rdname readSpatialData -#' @importFrom jsonlite fromJSON #' @importFrom arrow open_dataset +#' @importFrom Rarr read_zarr_attributes #' @export readPoint <- function(x, ...) { - md <- fromJSON(file.path(x, ".zattrs")) + md <- read_zarr_attributes(x) pq <- list.files(x, "\\.parquet$", full.names=TRUE) PointFrame(data=open_dataset(pq), meta=Zattrs(md)) } #' @rdname readSpatialData -#' @importFrom jsonlite fromJSON #' @importFrom arrow open_dataset +#' @importFrom Rarr read_zarr_attributes #' @import geoarrow #' @export readShape <- function(x, ...) { requireNamespace("geoarrow", quietly=TRUE) - md <- fromJSON(file.path(x, ".zattrs")) + md <- read_zarr_attributes(x) # TODO: previously had read_parquet(), # but that doesn't work with geoparquet? pq <- list.files(x, "\\.parquet$", full.names=TRUE) @@ -153,6 +163,10 @@ readShape <- function(x, ...) { stop("To use this function, install the 'anndataR' package via\n", "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") } + if (!requireNamespace('pizzarr', quietly=TRUE)) { + stop("To use this function, install the 'pizzarr' package via\n", + "`BiocManager::install(\"keller-mark/pizzarr\")`") + } suppressWarnings({ # suppress warnings related to hidden files adata <- anndataR::read_zarr(x) anndataR::to_SingleCellExperiment(adata) @@ -160,7 +174,6 @@ readShape <- function(x, ...) { } #' @rdname readSpatialData -#' @importFrom jsonlite fromJSON #' @importFrom S4Vectors metadata metadata<- #' @importFrom SummarizedExperiment colData colData<- #' @importFrom SingleCellExperiment @@ -206,6 +219,6 @@ readSpatialData <- function(x, f <- get(paste0("read", toupper(substr(i, 1, 1)), substr(i, 2, nchar(i)-1))) lapply(j, \(.) do.call(f, list(.))) }) - if (!anndataR) sd$tables <- .readTables_basilisk(x) + if (!anndataR && !isFALSE(tables)) sd$tables <- .readTables_basilisk(x) do.call(SpatialData, sd) } diff --git a/R/sdArray.R b/R/sdArray.R index 4d837938..bc20d61d 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -15,7 +15,15 @@ #' @return \code{ImageArray} #' #' @examples -#' # TODO +#' library(SpatialData.data) +#' dir.create(tf <- tempfile()) +#' zs <- SpatialData.data:::.unzip_merfish_demo(tf) +#' +#' # helper that gets path to first element in layer 'l' +#' fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] +#' +#' # read individual element +#' (ia <- readImage(fn("images"))) #' #' @importFrom S4Vectors metadata<- #' @importFrom methods new diff --git a/man/Array-methods.Rd b/man/Array-methods.Rd index 083e08df..489c139a 100644 --- a/man/Array-methods.Rd +++ b/man/Array-methods.Rd @@ -31,6 +31,14 @@ Methods for `ImageArray` and `LabelArray` class } \examples{ -# TODO +library(SpatialData.data) +dir.create(tf <- tempfile()) +zs <- SpatialData.data:::.unzip_merfish_demo(tf) + +# helper that gets path to first element in layer 'l' +fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] + +# read individual element +(ia <- readImage(fn("images"))) } diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index e760cfd6..9521d85c 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index a321588a..2a8395c3 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -13,10 +13,10 @@ \alias{axes,Zattrs-method} \alias{axes,SpatialDataElement-method} \alias{CTdata,Zattrs-method} -\alias{CTdata,SpatialDataElement-method} \alias{CTtype,Zattrs-method} -\alias{CTtype,SpatialDataElement-method} \alias{CTname,Zattrs-method} +\alias{CTdata,SpatialDataElement-method} +\alias{CTtype,SpatialDataElement-method} \alias{CTname,SpatialDataElement-method} \alias{CTname,SpatialData-method} \alias{CTgraph,SpatialData-method} @@ -36,14 +36,14 @@ \S4method{CTdata}{Zattrs}(x, ...) -\S4method{CTdata}{SpatialDataElement}(x, ...) - \S4method{CTtype}{Zattrs}(x, ...) -\S4method{CTtype}{SpatialDataElement}(x, ...) - \S4method{CTname}{Zattrs}(x, ...) +\S4method{CTdata}{SpatialDataElement}(x, ...) + +\S4method{CTtype}{SpatialDataElement}(x, ...) + \S4method{CTname}{SpatialDataElement}(x, ...) \S4method{CTname}{SpatialData}(x, ...) @@ -64,7 +64,7 @@ \S4method{addCT}{SpatialDataElement}(x, name, type, data) -\S4method{addCT}{Zattrs}(x, name, type = "identity", data = NULL) +\S4method{addCT}{Zattrs}(x, name, type = "identity", data = NULL, force = FALSE) } \arguments{ \item{x}{\code{SpatialData}, an element, or \code{Zattrs}.} @@ -103,8 +103,8 @@ plotCoordGraph(g) # retrieve transformation from element to target space CTpath(x, "blobs_labels", "sequence") -# view available coordinate transformations -CTdata(z <- meta(label(x))) +# view available target coordinate systems +CTname(z <- meta(label(x))) # add addCT(z, "scale", "scale", c(12, 34)) # can't overwrite diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index 000ed434..f16be969 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -58,6 +58,16 @@ Reading `SpatialData` \examples{ library(SpatialData.data) dir.create(tf <- tempfile()) -base <- SpatialData.data:::.unzip_merfish_demo(tf) -(x <- readSpatialData(base)) +zs <- SpatialData.data:::.unzip_merfish_demo(tf) + +# read complete Zarr store +(sd <- readSpatialData(zs, anndataR=TRUE)) + +# helper that gets path to first element in layer 'l' +fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] + +# read individual element +readImage(fn("images")) +readShape(fn("shapes")) +readPoint(fn("points")) } From db77ce08d04c442629fd37d239f00f21fc5b8a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BCr=20Manukyan?= Date: Sun, 29 Mar 2026 14:41:40 +0200 Subject: [PATCH 021/151] Modify dependency installation to skip building vignettes Change build_vignettes parameter to FALSE during dependency installation. --- .github/workflows/check-bioc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index b3830dc5..91cae76c 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -184,7 +184,7 @@ jobs: run: | ## Pass #2 at installing dependencies message(paste('****', Sys.time(), 'pass number 2 at installing dependencies: any remaining dependencies ****')) - remotes::install_local(dependencies = TRUE, repos = BiocManager::repositories(), build_vignettes = TRUE, upgrade = TRUE, force = TRUE) + remotes::install_local(dependencies = TRUE, repos = BiocManager::repositories(), build_vignettes = FALSE, upgrade = TRUE, force = TRUE) shell: Rscript {0} - name: Install BiocGenerics From 9cd6eda6ff77ed69b1a5cb1271c66aa06d13e839 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 17:05:32 +0200 Subject: [PATCH 022/151] update anndataR branch --- DESCRIPTION | 2 +- R/read.R | 5 ++++- man/readSpatialData.Rd | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a64b7fc8..537779ed 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -65,7 +65,7 @@ Suggests: DT, anndataR Remotes: - keller-mark/anndataR@keller-mark/zarr, + keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, HelenaLC/SpatialData.plot, Bioconductor/ZarrArray diff --git a/R/read.R b/R/read.R index 6a0ce66c..47c99038 100644 --- a/R/read.R +++ b/R/read.R @@ -66,6 +66,9 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", #' library(SpatialData.data) #' dir.create(tf <- tempfile()) #' base <- SpatialData.data:::.unzip_merfish_demo(tf) +#' (x <- readSpatialData(base)) +#' +#' # import tables using anndataR #' (x <- readSpatialData(base, anndataR=TRUE)) NULL @@ -153,7 +156,7 @@ readShape <- function(x, ...) { .readTable_anndataR <- function(x) { if (!requireNamespace('anndataR', quietly=TRUE)) { stop("To use this function, install the 'anndataR' package via\n", - "`BiocManager::install(\"keller-mark/anndataR\", ref=\"keller-mark/zarr\")`") + "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") } suppressWarnings({ # suppress warnings related to hidden files anndataR::read_zarr(x, as="SingleCellExperiment") diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index 5a3d0556..e6986bcb 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -59,5 +59,8 @@ Reading `SpatialData` library(SpatialData.data) dir.create(tf <- tempfile()) base <- SpatialData.data:::.unzip_merfish_demo(tf) +(x <- readSpatialData(base)) + +# import tables using anndataR (x <- readSpatialData(base, anndataR=TRUE)) } From 974be3c769ac53951802a8ec8db87aa09a16649e Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 18:07:36 +0200 Subject: [PATCH 023/151] remove zellkonverter for as_sce method in reticulateanndata --- DESCRIPTION | 7 +++---- R/read.R | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 537779ed..2dd7a6b8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -46,12 +46,12 @@ Imports: ZarrArray, RBGL, reticulate, + anndataR, sf, S4Arrays, S4Vectors, SingleCellExperiment, - SummarizedExperiment, - zellkonverter + SummarizedExperiment Suggests: BiocStyle, ggnewscale, @@ -62,8 +62,7 @@ Suggests: SpatialData.data, SpatialData.plot, testthat, - DT, - anndataR + DT Remotes: keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, diff --git a/R/read.R b/R/read.R index 47c99038..c92f9289 100644 --- a/R/read.R +++ b/R/read.R @@ -144,7 +144,7 @@ readShape <- function(x, ...) { full.names = FALSE) lapply(ts, \(z) { zs <- sd$read_zarr(file.path(x, "tables", z)) - se <- AnnData2SCE(zs) + se <- zs$as_SingleCellExperiment() nm <- "spatialdata_attrs" md <- metadata(se)[[nm]] int_metadata(se)[[nm]] <- md From 2a6c24fe78dedd66234bf8468335240c8edf87f7 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 19:56:43 +0200 Subject: [PATCH 024/151] import anndataR --- NAMESPACE | 2 +- R/read.R | 2 +- man/SpatialData.Rd | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index a090f831..7dacaede 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -80,6 +80,7 @@ exportMethods(tableNames) exportMethods(tables) exportMethods(translation) exportMethods(valTable) +import(anndataR) import(geoarrow) importClassesFrom(S4Arrays,Array) importClassesFrom(S4Vectors,DFrame) @@ -149,4 +150,3 @@ importFrom(sf,st_sfc) importFrom(utils,.DollarNames) importFrom(utils,head) importFrom(utils,tail) -importFrom(zellkonverter,AnnData2SCE) diff --git a/R/read.R b/R/read.R index c92f9289..7f71065e 100644 --- a/R/read.R +++ b/R/read.R @@ -126,9 +126,9 @@ readShape <- function(x, ...) { packages=c( "python==3.13.0"), pip=allp) +#' @import anndataR #' @importFrom reticulate import #' @importFrom S4Vectors metadata -#' @importFrom zellkonverter AnnData2SCE #' @importFrom SingleCellExperiment int_metadata #' @importFrom basilisk basiliskStart basiliskStop basiliskRun .readTables_basilisk <- function(x) { diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index e760cfd6..9521d85c 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From 4bd717391f02794a3255c65312a9eb504109658a Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 20:50:51 +0200 Subject: [PATCH 025/151] update anndataR message --- R/read.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/R/read.R b/R/read.R index 7f71065e..fbdba37e 100644 --- a/R/read.R +++ b/R/read.R @@ -155,8 +155,9 @@ readShape <- function(x, ...) { } .readTable_anndataR <- function(x) { if (!requireNamespace('anndataR', quietly=TRUE)) { - stop("To use this function, install the 'anndataR' package via\n", - "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") + message("To make sure 'anndataR' package works as intended, ", + "install the development version via\n", + "`BiocManager::install(\"keller-mark/anndataR\", ref=\"spatialdata\")`") } suppressWarnings({ # suppress warnings related to hidden files anndataR::read_zarr(x, as="SingleCellExperiment") From f49d15064a4311a1ebde740c162b9ff5b96cfa14 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sun, 29 Mar 2026 22:02:10 +0200 Subject: [PATCH 026/151] update NEWS and remove zellkonverter comments --- R/read.R | 8 ++++---- inst/NEWS | 8 ++++++++ man/readSpatialData.Rd | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/R/read.R b/R/read.R index fbdba37e..7f64e2ba 100644 --- a/R/read.R +++ b/R/read.R @@ -27,10 +27,10 @@ # "zarr==2.18.4", "zict==3.0.0") allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", "spatialdata_plot==0.2.14", "setuptools==75.8.0") -# notes from VJC -- readSpatialData was modified below so -# that if anndataR = FALSE, spatialdata.read_zarr is used +# notes from VJC/AM -- readSpatialData was modified below so +# that if anndataR = FALSE, anndata.read_zarr is used # to get the whole zarr store, and then the tables are -# transformed via zellkonverter. this gives a 10x speedup +# transformed via anndataR. This gives a 10x speedup # for ingesting the visium_hd_3.0.0 example but fails on # the blobs dataset in example("table-utils") because # of matters related to metadata/hasTable behavior @@ -53,7 +53,7 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", #' @param anndataR logical specifying whether #' to use \code{anndataR} to read tables; #' defaults to FALSE in `readSpatialData`, and `readTable`, -#' so that pythonic \code{spatialdata} and \code{zellkonverter} are used. +#' so that pythonic \code{anndata} are used. #' @param ... option arguments passed to and from other methods. #' #' @return diff --git a/inst/NEWS b/inst/NEWS index d61edd0f..d7033001 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,11 @@ +changes in version 0.99.24 + +- ZarrArray imported by Bioconductor/ZarrArray +- Rarr replaces pizzarr for importing tables via anndataR +- anndataR replaces zellkonverter +- update basilisk env to spatialdata==0.7.0 +- replace spatialdata.read with anndata.read_zarr to read tables + changes in version 0.99.22 - split off 'SpatialData.data' diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index e6986bcb..fdb1dcc3 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -44,7 +44,7 @@ to skip a layer, or a integer vector specifying which elements to read.} \item{anndataR}{logical specifying whether to use \code{anndataR} to read tables; defaults to FALSE in `readSpatialData`, and `readTable`, -so that pythonic \code{spatialdata} and \code{zellkonverter} are used.} +so that pythonic \code{anndata} are used.} } \value{ \itemize{ From 1e6092ad4a17f664b10b151ae9191c32d86f99f3 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 08:47:07 +0200 Subject: [PATCH 027/151] v0.99.25 --- DESCRIPTION | 2 +- R/coord_utils.R | 40 +++++++++++++++++------------------ R/methods.R | 9 +++++--- R/trans.R | 7 +++--- inst/NEWS | 5 +++++ man/coord-utils.Rd | 4 ++-- man/trans.Rd | 7 +++--- tests/testthat/test-mask.R | 2 +- tests/testthat/test-methods.R | 4 ++-- tests/testthat/test-query.R | 4 ++-- tests/testthat/test-reading.R | 6 +----- tests/testthat/test-tables.R | 16 +++++++------- tests/testthat/test-zattrs.R | 6 +++--- 13 files changed, 56 insertions(+), 56 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 000f2c9e..14317366 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R Depends: R (>= 4.5) -Version: 0.99.24 +Version: 0.99.25 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufracturer data and writing to .zarr, on-disk representation of images/labels as diff --git a/R/coord_utils.R b/R/coord_utils.R index b29d9b73..87dc137f 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -33,7 +33,7 @@ #' CTname(z <- meta(label(x))) #' #' # add -#' addCT(z, "scale", "scale", c(12, 34)) # can't overwrite +#' addCT(z, "scale", "scale", c(12, 34)) # overwrite #' CTdata(addCT(z, "new", "translation", c(12, 34))) #' #' # rmv @@ -55,6 +55,7 @@ NULL setMethod("axes", "Zattrs", \(x, ...) { if (!is.null(ms <- x$multiscales)) x <- ms[[1]] if (is.null(x <- x$axes)) stop("couldn't find 'axes'") + if (is.null(names(x[[1]]))) return(unlist(x)) data.frame(do.call(rbind, x)) }) @@ -66,7 +67,12 @@ setMethod("axes", "SpatialDataElement", \(x, ...) axes(meta(x))) #' @rdname coord-utils #' @export -setMethod("CTdata", "Zattrs", \(x, ...) x$multiscales[[1]]$coordinateTransformations) +setMethod("CTdata", "Zattrs", \(x, ...) { + ms <- "multiscales" + ct <- "coordinateTransformations" + if (is.null(x[[ms]])) return(x[[ct]]) + x[[ms]][[1]][[ct]] +}) #' @rdname coord-utils #' @export @@ -221,19 +227,17 @@ setMethod("rmvCT", "Zattrs", \(x, i) { "couln't find 'coordTrans' of name(s) ", paste(dQuote(nan), collapse=",")) i <- match(i, nms) - # # prevent against dropping identity - # i <- i[CTtype(x)[i] != "identity"] + # protect against dropping identity + i <- i[CTtype(x)[i] != "identity"] + if (!length(i)) stop("can't drop identity") ms <- "multiscales" ct <- "coordinateTransformations" if (length(i)) { - # utility to drop empty columns - j <- \(.) vapply(., \(.) !is.null(unlist(.)), logical(1)) - if (!is.null(x[[ms]])) { - y <- x[[ms]][[ct]][[1]][-i, ] - x[[ms]][[ct]][[1]] <- y[, j(y)] + if (is.null(x[[ms]])) { + x[[ct]] <- x[[ct]][i] } else { - y <- x[[ct]][-i, ] - x[[ct]] <- y[, j(y)] + y <- x[[ms]][[1]][[ct]][-i] + x[[ms]][[1]][[ct]] <- y } } return(x) @@ -261,18 +265,11 @@ setMethod("addCT", "SpatialDataElement", \(x, name, type, data) { #' @rdname coord-utils #' @export -setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL, force=FALSE) { +setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { stopifnot( is.character(name), length(name) == 1, is.character(type), length(type) == 1) .check_ct(x, type, data) - # don't overwrite - ow <- match(name, CTname(x)) - if (!is.na(ow)) { - if (!isTRUE(force)) - stop("CT of 'name' ", name, " already exists;", - " use 'force=TRUE' to force overwrite.") - } # use existing as skeleton old <- CTdata(x) new <- old[[1]][c("input", "output", "type")] @@ -282,10 +279,11 @@ setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL, force=FALSE) # append/overwrite & stash ms <- "multiscales" ct <- "coordinateTransformations" - if (is.na(ow)) { + i <- match(name, CTname(x)) + if (is.na(i)) { new <- c(old, list(new)) } else { - old[[ow]] <- new + old[[i]] <- new new <- old } if (is.null(x[[ms]])) { diff --git a/R/methods.R b/R/methods.R index eaf7dc44..ed30acf8 100644 --- a/R/methods.R +++ b/R/methods.R @@ -43,7 +43,10 @@ setMethod("[[", c("SpatialData", "character"), \(x, i, ...) { for (. in names(j)) { .j <- j[[.]] n <- length(attr(x, .)) - if (length(.j) == 1 && is.infinite(.j)) { + if (is.character(.j)) { + if (!all(.j %in% names(attr(x, .)))) + stop("invalid 'j'") + } else if (length(.j) == 1 && is.infinite(.j)) { .j <- n } else if (any(.j > n)) { stop("invalid 'j'") @@ -217,7 +220,7 @@ NULL f <- \(.) setReplaceMethod(., c("SpatialData", "character", typ[[.]]), - \(x, i, value) { + \(x, i, value) { y <- attr(x, paste0(., "s")) y[[i]] <- value attr(x, paste0(., "s")) <- y @@ -253,7 +256,7 @@ NULL f <- \(.) setReplaceMethod(., c("SpatialData", "missing", typ[[.]]), - \(x, i, value) { + \(x, i, value) { f <- get(paste0(., "<-")) f(x=x, i=1, value=value) }) diff --git a/R/trans.R b/R/trans.R index bef7a277..34ccf39e 100644 --- a/R/trans.R +++ b/R/trans.R @@ -23,7 +23,7 @@ #' # point #' y <- x #' point(y, "rot") <- rotate(point(y), 20) -#' point(y, "wide") <- scale(point(y), c(1, 1.2)) +#' point(y, "wide") <- scale(point(y), c(1.2, 1)) #' #' xy0 <- as.data.frame(point(y)) #' xy1 <- as.data.frame(point(y, "rot")) @@ -36,10 +36,9 @@ #' # shape #' y <- x #' shape(y, "rot") <- rotate(shape(y), 5) -#' shape(y, "high") <- scale(shape(y), c(1.2, 1)) +#' shape(y, "wide") <- scale(shape(y), c(1.2, 1)) #' shape(y, "left") <- translation(shape(y), c(-5, 0)) -#' -#' graph::plot(CTgraph(y)) +#' y["shapes", c("rot", "wide", "left")] NULL # image ---- diff --git a/inst/NEWS b/inst/NEWS index d7033001..daa29301 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,8 @@ +changes in version 0.99.25 + +- replace jsonlite::fromJSON() with Rarr::read_zarr_attributes() + for reading .zattrs & rewrite code-/test-base accordingly + changes in version 0.99.24 - ZarrArray imported by Bioconductor/ZarrArray diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index 2a8395c3..ee2a91b5 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -64,7 +64,7 @@ \S4method{addCT}{SpatialDataElement}(x, name, type, data) -\S4method{addCT}{Zattrs}(x, name, type = "identity", data = NULL, force = FALSE) +\S4method{addCT}{Zattrs}(x, name, type = "identity", data = NULL) } \arguments{ \item{x}{\code{SpatialData}, an element, or \code{Zattrs}.} @@ -107,7 +107,7 @@ CTpath(x, "blobs_labels", "sequence") CTname(z <- meta(label(x))) # add -addCT(z, "scale", "scale", c(12, 34)) # can't overwrite +addCT(z, "scale", "scale", c(12, 34)) # overwrite CTdata(addCT(z, "new", "translation", c(12, 34))) # rmv diff --git a/man/trans.Rd b/man/trans.Rd index 8602b2e5..7bc36c57 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -55,7 +55,7 @@ CTpath(image(y), "global") # point y <- x point(y, "rot") <- rotate(point(y), 20) -point(y, "wide") <- scale(point(y), c(1, 1.2)) +point(y, "wide") <- scale(point(y), c(1.2, 1)) xy0 <- as.data.frame(point(y)) xy1 <- as.data.frame(point(y, "rot")) @@ -68,8 +68,7 @@ points(xy2[, c(1, 2)], col=4) # shape y <- x shape(y, "rot") <- rotate(shape(y), 5) -shape(y, "high") <- scale(shape(y), c(1.2, 1)) +shape(y, "wide") <- scale(shape(y), c(1.2, 1)) shape(y, "left") <- translation(shape(y), c(-5, 0)) - -graph::plot(CTgraph(y)) +y["shapes", c("rot", "wide", "left")] } diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index ad3545b9..9039bfe6 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -1,7 +1,7 @@ library(SingleCellExperiment) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, anndataR=FALSE) +x <- readSpatialData(x, anndataR=TRUE) test_that("mask(),ImageArray,LabelArray", { i <- "blobs_image" diff --git a/tests/testthat/test-methods.R b/tests/testthat/test-methods.R index 7ee02f2c..8f312669 100644 --- a/tests/testthat/test-methods.R +++ b/tests/testthat/test-methods.R @@ -4,9 +4,9 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x) -sdtable = SpatialData::table # skirt ambiguity and limitations of get() +sdtable <- SpatialData::table # skirt ambiguity and limitations of get() "sdtable<-" = "SpatialData::table<-" # skirt ambiguity and limitations of get() -sdtables = SpatialData::tables +sdtables <- SpatialData::tables fun <- c("image", "label", "shape", "point", "sdtable") nms <- c("blobs_image", "blobs_labels", "blobs_circles", "blobs_points", "table") diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 1c742f6c..c2b51413 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -1,4 +1,4 @@ -library(sf) +suppressPackageStartupMessages(library(sf)) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) @@ -29,7 +29,7 @@ test_that("query,ImageArray", { j <- query(i, xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) expect_equal(dim(j), c(3, 1+h-2, 1+w-1)) expect_equal(CTtype(j), t <- "translation") - expect_equivalent(CTdata(j)[[t]][[1]], c(0, 2, 1)) + expect_equivalent(CTdata(j)[[1]][[t]][[1]], c(0, 2, 1)) }) test_that("query,PointFrame", { diff --git a/tests/testthat/test-reading.R b/tests/testthat/test-reading.R index 807d261e..951ed8b2 100644 --- a/tests/testthat/test-reading.R +++ b/tests/testthat/test-reading.R @@ -11,11 +11,7 @@ test_that("readElement()", { for (l in names(typ)) { f <- paste0(toupper(substr(l, 1, 1)), substr(l, 2, nchar(l)-1)) y <- list.files(file.path(x, l), full.names=TRUE)[1] - if (l != "tables") { - expect_is(get(paste0("read", f))(y), typ[l]) - } else { - expect_is(.readTables_basilisk(x)[[1]], typ[l]) - } + expect_is(get(paste0("read", f))(y), typ[l]) } }) diff --git a/tests/testthat/test-tables.R b/tests/testthat/test-tables.R index 74e5f6b6..ae0ab567 100644 --- a/tests/testthat/test-tables.R +++ b/tests/testthat/test-tables.R @@ -4,7 +4,7 @@ options(arrow.pull_as_vector=TRUE) require(SingleCellExperiment, quietly=TRUE) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, table=1, anndataR=FALSE) +x <- readSpatialData(x, table=1, anndataR=TRUE) md <- int_metadata(SpatialData::table(x)) md <- md$spatialdata_attrs @@ -124,13 +124,13 @@ test_that("valTable()", { expect_error(valTable(x, i, 123)) expect_error(valTable(x, i, sample(rownames(t), 2))) expect_error(valTable(x, i, sample(names(colData(t)), 2))) - # 'colData' - df <- DataFrame(a=sample(letters, n), b=runif(n), - region = valTable(x, i, j <- "region")) - s <- t; colData(s) <- df; y <- x; SpatialData::table(y) <- s - expect_identical(valTable(y, i, j <- "a"), s[[j]]) - expect_identical(valTable(y, i, j <- "b"), s[[j]]) - expect_error(valTable(y, i, "c")) + # # 'colData' + # df <- DataFrame(a=sample(letters, n), b=runif(n), + # region = valTable(x, i, j <- "region")) + # s <- t; colData(s) <- df; y <- x; SpatialData::table(y) <- s + # expect_identical(valTable(y, i, j <- "a"), s[[j]]) + # expect_identical(valTable(y, i, j <- "b"), s[[j]]) + # expect_error(valTable(y, i, "c")) # 'assay' data j <- sample(rownames(t), 1) v <- valTable(x, i, j) diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index c618854e..de5b1c1f 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -28,10 +28,10 @@ test_that("rmvCT", { expect_error(rmvCT(y, ".")) expect_error(rmvCT(y, c(".", CTname(y)[1]))) # by name - i <- sample(CTname(y), 2) + i <- sample(setdiff(CTname(y), "global"), 2) expect_identical(CTname(rmvCT(y, i)), setdiff(CTname(y), i)) # by index - i <- sample(seq_along(CTname(y)), 2) + i <- sample(which(CTtype(y) != "identity"), 2) expect_identical(CTname(rmvCT(y, i)), CTname(y)[-i]) }) @@ -41,7 +41,7 @@ test_that("addCT", { es <- lapply(ls, \(.) x[.,1][[.]][[1]]) .check_data <- \(z, x) { expect_true("." %in% CTname(z)) - ct <- CTdata(z)[CTname(z) == ".", ] + ct <- CTdata(z)[[which(CTname(z) == ".")]] expect_identical(ct[[t]][[1]], x) } for (y in es) { From ec760b88f05ad38394cc270352dd15cca8e45a1d Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 08:54:24 +0200 Subject: [PATCH 028/151] use anndataR for reading tables --- vignettes/SpatialData.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/SpatialData.Rmd b/vignettes/SpatialData.Rmd index 956096ff..b2dc6dfe 100644 --- a/vignettes/SpatialData.Rmd +++ b/vignettes/SpatialData.Rmd @@ -49,7 +49,7 @@ For demonstration, we read in a toy dataset that is available through the packag ```{r blobs-read} x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -(x <- readSpatialData(x, anndataR=FALSE)) # VJC Dec 8, test effect of absence of anndataR +(x <- readSpatialData(x, anndataR=TRUE)) ``` `SpatialData` object behave like a list, thereby supporting flexible accession, From 2375434bbbfde3a7753c7c97a173720925541cc1 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 09:51:00 +0200 Subject: [PATCH 029/151] improved Zattrs show method (cf., PR #117) --- R/Zattrs.R | 40 ++ R/coord_utils.R | 5 +- inst/NEWS | 1 + man/SpatialData.Rd | 8 +- tests/testthat/test-zattrs.R | 14 +- vignettes/SpatialData.html | 725 ++++++++++++++++++++++++++++------- 6 files changed, 644 insertions(+), 149 deletions(-) diff --git a/R/Zattrs.R b/R/Zattrs.R index 854443cf..b86e78a6 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -33,3 +33,43 @@ Zattrs <- \(x=list()) { #' @rdname Zattrs #' @exportMethod $ setMethod("$", "Zattrs", \(x, name) x[[name]]) + +.showZattrs <- function(object) { + cat("class: Zattrs\n") + cat(sprintf("axes(%d):\n", length(x))) + if (!is.null(x <- axes(object))) { + cat("- name:", unlist(x), "\n") + } else { + cat("- name:", vapply(x, \(.) .$name, character(1)), "\n") + cat("- type:", vapply(x, \(.) .$type, character(1)), "\n") + } + cat(sprintf("coordTrans(%d):\n", n <- length(CTname(object)))) + g <- \(.) { + . <- paste(unlist(.), collapse=",") + if (!grepl(",", .)) return(.) + sprintf("[%s]", .) + } + f <- \(.) { + if (is.null(.)) return("") + paste0(":", g(lapply(., g))) + } + for (i in seq_len(n)) + cat(sprintf("- %s: (%s%s)\n", + CTname(object)[i], + CTtype(object)[i], + f(CTdata(object)[[i]][[CTtype(object)[i]]]))) + ms <- object$multiscales[[1]] + if (!is.null(ms)) { + ds <- ms$datasets + ps <- vapply(ds, \(.) .$path, character(1)) + coolcat("datasets(%d): %s\n", ps) + for (i in seq_along(ds)) { + ct <- ds[[i]]$coordinateTransformations[[1]] + cat(sprintf("- %s: (%s:%s)\n", + ps[i], ct$type, g(ct[[ct$type]]))) + } + } + cs <- unlist(object$omero$channels) + if (!is.null(cs)) coolcat("channels(%d): %s\n", cs) +} +setMethod("show", "Zattrs", .showZattrs) diff --git a/R/coord_utils.R b/R/coord_utils.R index 87dc137f..03714971 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -55,8 +55,7 @@ NULL setMethod("axes", "Zattrs", \(x, ...) { if (!is.null(ms <- x$multiscales)) x <- ms[[1]] if (is.null(x <- x$axes)) stop("couldn't find 'axes'") - if (is.null(names(x[[1]]))) return(unlist(x)) - data.frame(do.call(rbind, x)) + return(x) }) #' @rdname coord-utils @@ -251,7 +250,7 @@ setMethod("addCT", "SpatialDataElement", \(x, name, type, data) { x@meta <- addCT(meta(x), name, type, data); x }) .check_ct <- \(x, type, data) { - d <- ifelse(is.character(a <- axes(x)), length(a), nrow(a)) + d <- length(axes(x)) f <- \(t) stop("invalid 'data' for transformation of 'type' ", dQuote(t)) t <- match.arg(type, c("identity", "scale", "rotate", "translation", "affine")) . <- switch(t, diff --git a/inst/NEWS b/inst/NEWS index daa29301..48b32dea 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,5 +1,6 @@ changes in version 0.99.25 +- improved Zattrs show method (cf., PR #117) - replace jsonlite::fromJSON() with Rarr::read_zarr_attributes() for reading .zattrs & rewrite code-/test-base accordingly diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 9521d85c..e760cfd6 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index de5b1c1f..60c1de00 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -5,20 +5,22 @@ x <- readSpatialData(x, anndataR=FALSE) test_that("axes", { # image y <- axes(image(x)) - expect_is(y, "data.frame") - expect_equal(dim(y), c(3, 2)) + expect_is(y, "list") + expect_length(y, 3) # label y <- axes(label(x)) - expect_is(y, "data.frame") - expect_equal(dim(y), c(2, 2)) + expect_is(y, "list") + expect_length(y, 2) # shape y <- axes(shape(x)) - expect_is(y, "character") + expect_is(y, "list") expect_length(y, 2) + expect_equal(unlist(y), c("x", "y")) # point y <- axes(point(x)) - expect_is(y, "character") + expect_is(y, "list") expect_length(y, 2) + expect_equal(unlist(y), c("x", "y")) }) test_that("rmvCT", { diff --git a/vignettes/SpatialData.html b/vignettes/SpatialData.html index cf711c6f..6c3fea83 100644 --- a/vignettes/SpatialData.html +++ b/vignettes/SpatialData.html @@ -10,7 +10,7 @@ - + SpatialData @@ -702,9 +702,9 @@

SpatialData

Helena Lucia Crowell, Louise Deconinck, Artür Manukyan, Dario Righelli, Estella Dong and Vince Carey

-

November 24, 2024

+

April 01, 2026

Package

-

SpatialData 0.99.19

+

SpatialData 0.99.25

@@ -717,14 +717,11 @@

Contents

  • 1.2 Annotations
  • 1.3 Transformations
  • -
  • 2 Datasets
  • -
  • 3 Session info
  • +
  • 2 Session info
  • -
    library(ggplot2)
    -library(ggnewscale)
    -library(SpatialData)
    +
    library(SpatialData)
     library(SingleCellExperiment)

    1 Introduction

    @@ -733,11 +730,11 @@

    1 Introduction

    .zarr files that follow OME-NGFF specs.

    Each SpatialData object is composed of five layers: images, labels, shapes, points, and tables. Each layer may contain an arbitrary number of elements.

    -

    Images and labels are represented as ZarrArrays (Rarr). +

    Images and labels are represented as ZarrArrays (Rarr). Points and shapes are represented as arrow objects linked to an on-disk .parquet file. As such, all data are represented out of memory.

    Element annotation as well as cross-layer summarizations (e.g., count matrices) -are represented as SingleCellExperiment as tables.

    +are represented as SingleCellExperiment as tables.

    1.1 Handling

    For demonstration, we read in a toy dataset that is available through the package:

    @@ -792,20 +789,11 @@

    1.1 Handling

    ## ## See $metadata for additional Schema metadata
    meta(shape(x)) 
    -
    ## An object of class "Zattrs"
    -## [[1]]
    -## [1] "x" "y"
    -## 
    -## [[2]]
    -##     input.axes input.name  output.axes output.name     type
    -## 1 c("x", "....         xy c("x", "....      global identity
    -## 
    -## [[3]]
    -## [1] "ngff:shapes"
    -## 
    -## [[4]]
    -## [[4]]$version
    -## [1] "0.2"
    +
    ## class: Zattrs
    +## axes(0):
    +## - name: x y 
    +## coordTrans(1):
    +## - global: (identity)

    1.2 Annotations

    @@ -855,12 +843,12 @@

    1.2 Annotations

    ## DataFrame with 6 rows and 1 column
     ##           n
     ##   <numeric>
    -## 1 0.0741157
    -## 2 0.2648403
    -## 3 0.5950422
    -## 4 0.6883271
    -## 5 0.0452319
    -## 6 0.1401131
    +## 1 0.179334 +## 2 0.570150 +## 3 0.280183 +## 4 0.058967 +## 5 0.105632 +## 6 0.181170
    # ...using a list of data generating functions
     f <- list(
         numbers=\(n) runif(n),
    @@ -871,12 +859,12 @@ 

    1.2 Annotations

    ## DataFrame with 6 rows and 2 columns
     ##     numbers     letters
     ##   <numeric> <character>
    -## 1  0.156174           m
    -## 2  0.998399           m
    -## 3  0.418688           d
    -## 4  0.302950           k
    -## 5  0.319555           x
    -## 6  0.443932           l
    +## 1 0.9456882 l +## 2 0.0721818 r +## 3 0.7813973 i +## 4 0.1646447 t +## 5 0.0205351 y +## 6 0.2196956 b

    1.3 Transformations

    @@ -888,22 +876,518 @@

    1.3 Transformations

    z <- meta(label(x))
     axes(z)
    -
    ##   name  type
    -## 1    y space
    -## 2    x space
    +
    ## [[1]]
    +## [[1]]$name
    +## [1] "y"
    +## 
    +## [[1]]$type
    +## [1] "space"
    +## 
    +## 
    +## [[2]]
    +## [[2]]$name
    +## [1] "x"
    +## 
    +## [[2]]$type
    +## [1] "space"
    CTdata(z)
    -
    ##     input.axes input.name  output.axes output.name        type scale
    -## 1 c("y", "....         yx c("y", "....      global    identity      
    -## 2 c("y", "....         yx c("y", "....       scale       scale  3, 2
    -## 3 c("y", "....         yx c("y", ".... translation translation      
    -## 4 c("y", "....         yx c("x", "....      affine      affine      
    -## 5 c("y", "....         yx c("y", "....    sequence    sequence      
    -##   translation       affine transformations
    -## 1                                         
    -## 2                                         
    -## 3     -50, 10                             
    -## 4             20, 50, ....                
    -## 5                             list(axe....
    +
    ## [[1]]
    +## [[1]]$input
    +## [[1]]$input$axes
    +## [[1]]$input$axes[[1]]
    +## [[1]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[1]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[1]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[1]]$input$axes[[2]]
    +## [[1]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[1]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[1]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[1]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[1]]$output
    +## [[1]]$output$axes
    +## [[1]]$output$axes[[1]]
    +## [[1]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[1]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[1]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[1]]$output$axes[[2]]
    +## [[1]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[1]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[1]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[1]]$output$name
    +## [1] "global"
    +## 
    +## 
    +## [[1]]$type
    +## [1] "identity"
    +## 
    +## 
    +## [[2]]
    +## [[2]]$input
    +## [[2]]$input$axes
    +## [[2]]$input$axes[[1]]
    +## [[2]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[2]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[2]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[2]]$input$axes[[2]]
    +## [[2]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[2]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[2]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[2]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[2]]$output
    +## [[2]]$output$axes
    +## [[2]]$output$axes[[1]]
    +## [[2]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[2]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[2]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[2]]$output$axes[[2]]
    +## [[2]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[2]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[2]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[2]]$output$name
    +## [1] "scale"
    +## 
    +## 
    +## [[2]]$scale
    +## [[2]]$scale[[1]]
    +## [1] 3
    +## 
    +## [[2]]$scale[[2]]
    +## [1] 2
    +## 
    +## 
    +## [[2]]$type
    +## [1] "scale"
    +## 
    +## 
    +## [[3]]
    +## [[3]]$input
    +## [[3]]$input$axes
    +## [[3]]$input$axes[[1]]
    +## [[3]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[3]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[3]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[3]]$input$axes[[2]]
    +## [[3]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[3]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[3]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[3]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[3]]$output
    +## [[3]]$output$axes
    +## [[3]]$output$axes[[1]]
    +## [[3]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[3]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[3]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[3]]$output$axes[[2]]
    +## [[3]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[3]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[3]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[3]]$output$name
    +## [1] "translation"
    +## 
    +## 
    +## [[3]]$translation
    +## [[3]]$translation[[1]]
    +## [1] -50
    +## 
    +## [[3]]$translation[[2]]
    +## [1] 10
    +## 
    +## 
    +## [[3]]$type
    +## [1] "translation"
    +## 
    +## 
    +## [[4]]
    +## [[4]]$affine
    +## [[4]]$affine[[1]]
    +## [[4]]$affine[[1]][[1]]
    +## [1] 20
    +## 
    +## [[4]]$affine[[1]][[2]]
    +## [1] 10
    +## 
    +## [[4]]$affine[[1]][[3]]
    +## [1] 30
    +## 
    +## 
    +## [[4]]$affine[[2]]
    +## [[4]]$affine[[2]][[1]]
    +## [1] 50
    +## 
    +## [[4]]$affine[[2]][[2]]
    +## [1] 40
    +## 
    +## [[4]]$affine[[2]][[3]]
    +## [1] 60
    +## 
    +## 
    +## 
    +## [[4]]$input
    +## [[4]]$input$axes
    +## [[4]]$input$axes[[1]]
    +## [[4]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[4]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[4]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[4]]$input$axes[[2]]
    +## [[4]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[4]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[4]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[4]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[4]]$output
    +## [[4]]$output$axes
    +## [[4]]$output$axes[[1]]
    +## [[4]]$output$axes[[1]]$name
    +## [1] "x"
    +## 
    +## [[4]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[4]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[4]]$output$axes[[2]]
    +## [[4]]$output$axes[[2]]$name
    +## [1] "y"
    +## 
    +## [[4]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[4]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[4]]$output$name
    +## [1] "affine"
    +## 
    +## 
    +## [[4]]$type
    +## [1] "affine"
    +## 
    +## 
    +## [[5]]
    +## [[5]]$input
    +## [[5]]$input$axes
    +## [[5]]$input$axes[[1]]
    +## [[5]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$input$axes[[2]]
    +## [[5]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[5]]$output
    +## [[5]]$output$axes
    +## [[5]]$output$axes[[1]]
    +## [[5]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$output$axes[[2]]
    +## [[5]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$output$name
    +## [1] "sequence"
    +## 
    +## 
    +## [[5]]$transformations
    +## [[5]]$transformations[[1]]
    +## [[5]]$transformations[[1]]$input
    +## [[5]]$transformations[[1]]$input$axes
    +## [[5]]$transformations[[1]]$input$axes[[1]]
    +## [[5]]$transformations[[1]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$transformations[[1]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[1]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$transformations[[1]]$input$axes[[2]]
    +## [[5]]$transformations[[1]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$transformations[[1]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[1]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$transformations[[1]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[5]]$transformations[[1]]$output
    +## [[5]]$transformations[[1]]$output$axes
    +## [[5]]$transformations[[1]]$output$axes[[1]]
    +## [[5]]$transformations[[1]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$transformations[[1]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[1]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$transformations[[1]]$output$axes[[2]]
    +## [[5]]$transformations[[1]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$transformations[[1]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[1]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$transformations[[1]]$output$name
    +## [1] "global"
    +## 
    +## 
    +## [[5]]$transformations[[1]]$scale
    +## [[5]]$transformations[[1]]$scale[[1]]
    +## [1] 3
    +## 
    +## [[5]]$transformations[[1]]$scale[[2]]
    +## [1] 2
    +## 
    +## 
    +## [[5]]$transformations[[1]]$type
    +## [1] "scale"
    +## 
    +## 
    +## [[5]]$transformations[[2]]
    +## [[5]]$transformations[[2]]$input
    +## [[5]]$transformations[[2]]$input$axes
    +## [[5]]$transformations[[2]]$input$axes[[1]]
    +## [[5]]$transformations[[2]]$input$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$transformations[[2]]$input$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[2]]$input$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$transformations[[2]]$input$axes[[2]]
    +## [[5]]$transformations[[2]]$input$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$transformations[[2]]$input$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[2]]$input$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$transformations[[2]]$input$name
    +## [1] "yx"
    +## 
    +## 
    +## [[5]]$transformations[[2]]$output
    +## [[5]]$transformations[[2]]$output$axes
    +## [[5]]$transformations[[2]]$output$axes[[1]]
    +## [[5]]$transformations[[2]]$output$axes[[1]]$name
    +## [1] "y"
    +## 
    +## [[5]]$transformations[[2]]$output$axes[[1]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[2]]$output$axes[[1]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## [[5]]$transformations[[2]]$output$axes[[2]]
    +## [[5]]$transformations[[2]]$output$axes[[2]]$name
    +## [1] "x"
    +## 
    +## [[5]]$transformations[[2]]$output$axes[[2]]$type
    +## [1] "space"
    +## 
    +## [[5]]$transformations[[2]]$output$axes[[2]]$unit
    +## [1] "unit"
    +## 
    +## 
    +## 
    +## [[5]]$transformations[[2]]$output$name
    +## [1] "global"
    +## 
    +## 
    +## [[5]]$transformations[[2]]$translation
    +## [[5]]$transformations[[2]]$translation[[1]]
    +## [1] -50
    +## 
    +## [[5]]$transformations[[2]]$translation[[2]]
    +## [1] 10
    +## 
    +## 
    +## [[5]]$transformations[[2]]$type
    +## [1] "translation"
    +## 
    +## 
    +## 
    +## [[5]]$type
    +## [1] "sequence"
    CTname(rmvCT(z, "scale"))
    ## [1] "global"      "translation" "affine"      "sequence"
    CTname(addCT(z, 
    @@ -919,7 +1403,7 @@ 

    1.3 Transformations

    ## Number of Nodes = 14 ## Number of Edges = 13
    plotCoordGraph(g)
    -

    +

    The above representation greatly facilitates queries of the transformation(s) required to spatially align elements. blobs_labels, for example, requires a sequential transformation (scaling and translation) for the sequence space:

    @@ -931,7 +1415,12 @@

    1.3 Transformations

    invisible(CTpath(y, j))
    ## [[1]]
     ## [[1]]$data
    -## [1] 3 2
    +## [[1]]$data[[1]]
    +## [1] 3
    +## 
    +## [[1]]$data[[2]]
    +## [1] 2
    +## 
     ## 
     ## [[1]]$type
     ## [1] "scale"
    @@ -939,55 +1428,26 @@ 

    1.3 Transformations

    ## ## [[2]] ## [[2]]$data -## [1] -50 10 +## [[2]]$data[[1]] +## [1] -50 +## +## [[2]]$data[[2]] +## [1] 10 +## ## ## [[2]]$type ## [1] "translation"
    -
    -

    2 Datasets

    -

    Data from a variety of technologies has been made available as SpatialData .zarr stores -here. -These, in turn, have been deposited in Bioconductor’s NSF Open Storage Network bucket, -and can be retrieved with caching support using BiocFileCache.

    -

    We can interrogate the bucket for available (zipped) .zarr archives:

    -
    available_spd_zarr_zips()
    -
    ## [1] "mcmicro_io.zip"                         
    -## [2] "merfish.zarr.zip"                       
    -## [3] "mibitof.zip"                            
    -## [4] "steinbock_io.zip"                       
    -## [5] "visium_associated_xenium_io_aligned.zip"
    -## [6] "visium_hd_3.0.0_io.zip"
    -

    Any of the above can be retrieved (once) into some location, and read into R; for example:

    -
    dir.create(td <- tempfile())
    -pa <- unzip_spd_demo(
    -    zipname="merfish.zarr.zip", 
    -    dest=td, source="biocOSN")
    -(x <- readSpatialData(pa))
    -
    ## class: SpatialData
    -## - images(1):
    -##   - rasterized (1,522,575)
    -## - labels(0):
    -## - points(1):
    -##   - single_molecule (3714642)
    -## - shapes(2):
    -##   - anatomical (6,polygon)
    -##   - cells (2389,circle)
    -## - tables(1):
    -##   - table (268,2389)
    -## coordinate systems:
    -## - global(4): rasterized anatomical cells single_molecule
    -
    -
    -

    3 Session info

    -
    ## R version 4.4.1 Patched (2024-07-08 r86893)
    -## Platform: aarch64-apple-darwin20
    -## Running under: macOS Sonoma 14.2.1
    +
    +

    2 Session info

    +
    ## R version 4.6.0 alpha (2026-03-26 r89725)
    +## Platform: aarch64-apple-darwin23
    +## Running under: macOS Sequoia 15.6.1
     ## 
     ## Matrix products: default
    -## BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
    -## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
    +## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib 
    +## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
     ## 
     ## locale:
     ## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
    @@ -1000,49 +1460,42 @@ 

    3 Session info

    ## [8] base ## ## other attached packages: -## [1] Rarr_1.6.0 DelayedArray_0.32.0 -## [3] SparseArray_1.6.0 S4Arrays_1.6.0 -## [5] abind_1.4-8 Matrix_1.7-1 -## [7] SingleCellExperiment_1.28.0 SummarizedExperiment_1.36.0 -## [9] Biobase_2.66.0 GenomicRanges_1.58.0 -## [11] GenomeInfoDb_1.42.0 IRanges_2.40.0 -## [13] S4Vectors_0.44.0 BiocGenerics_0.52.0 -## [15] MatrixGenerics_1.18.0 matrixStats_1.4.1 -## [17] SpatialData_0.99.19 ggnewscale_0.5.0 -## [19] ggplot2_3.5.1 BiocStyle_2.34.0 +## [1] SingleCellExperiment_1.33.2 SummarizedExperiment_1.41.1 +## [3] Biobase_2.71.0 GenomicRanges_1.63.1 +## [5] Seqinfo_1.1.0 IRanges_2.45.0 +## [7] S4Vectors_0.49.0 BiocGenerics_0.57.0 +## [9] generics_0.1.4 MatrixGenerics_1.23.0 +## [11] matrixStats_1.5.0 SpatialData_0.99.25 +## [13] BiocStyle_2.39.0 ## ## loaded via a namespace (and not attached): -## [1] DBI_1.2.3 RBGL_1.82.0 anndataR_0.99.0 -## [4] rlang_1.1.4 magrittr_2.0.3 e1071_1.7-16 -## [7] compiler_4.4.1 RSQLite_2.3.8 dir.expiry_1.14.0 -## [10] paws.storage_0.7.0 png_0.1-8 vctrs_0.6.5 -## [13] stringr_1.5.1 wk_0.9.4 pkgconfig_2.0.3 -## [16] crayon_1.5.3 fastmap_1.2.0 magick_2.8.5 -## [19] dbplyr_2.5.0 XVector_0.46.0 paws.common_0.7.7 -## [22] utf8_1.2.4 rmarkdown_2.29 graph_1.84.0 -## [25] UCSC.utils_1.2.0 tinytex_0.54 purrr_1.0.2 -## [28] bit_4.5.0 xfun_0.49 zlibbioc_1.52.0 -## [31] cachem_1.1.0 jsonlite_1.8.9 blob_1.2.4 -## [34] parallel_4.4.1 R6_2.5.1 bslib_0.8.0 -## [37] stringi_1.8.4 reticulate_1.40.0 jquerylib_0.1.4 -## [40] Rcpp_1.0.13-1 bookdown_0.41 assertthat_0.2.1 -## [43] knitr_1.49 R.utils_2.12.3 tidyselect_1.2.1 -## [46] rstudioapi_0.17.1 yaml_2.3.10 zellkonverter_1.16.0 -## [49] curl_6.0.1 lattice_0.22-6 tibble_3.2.1 -## [52] basilisk.utils_1.18.0 withr_3.0.2 evaluate_1.0.1 -## [55] sf_1.0-19 units_0.8-5 proxy_0.4-27 -## [58] BiocFileCache_2.14.0 xml2_1.3.6 pillar_1.9.0 -## [61] BiocManager_1.30.25 filelock_1.0.3 KernSmooth_2.23-24 -## [64] pizzarr_0.1.0 generics_0.1.3 nanoarrow_0.6.0 -## [67] munsell_0.5.1 scales_1.3.0 class_7.3-22 -## [70] glue_1.8.0 tools_4.4.1 grid_4.4.1 -## [73] colorspace_2.1-1 paws_0.7.0 GenomeInfoDbData_1.2.13 -## [76] basilisk_1.18.0 cli_3.6.3 fansi_1.0.6 -## [79] arrow_17.0.0.1 dplyr_1.1.4 geoarrow_0.2.1 -## [82] Rgraphviz_2.50.0 gtable_0.3.6 R.methodsS3_1.8.2 -## [85] sass_0.4.9 digest_0.6.37 classInt_0.4-10 -## [88] memoise_2.0.1 htmltools_0.5.8.1 R.oo_1.27.0 -## [91] lifecycle_1.0.4 httr_1.4.7 bit64_4.5.2
    +## [1] tidyselect_1.2.1 dplyr_1.2.0 filelock_1.0.3 +## [4] arrow_23.0.1.2 R.utils_2.13.0 fastmap_1.2.0 +## [7] digest_0.6.39 lifecycle_1.0.5 sf_1.1-0 +## [10] paws.storage_0.9.0 magrittr_2.0.4 compiler_4.6.0 +## [13] rlang_1.1.7 sass_0.4.10 tools_4.6.0 +## [16] yaml_2.3.12 knitr_1.51 S4Arrays_1.11.1 +## [19] bit_4.6.0 classInt_0.4-11 curl_7.0.0 +## [22] reticulate_1.45.0 DelayedArray_0.37.0 KernSmooth_2.23-26 +## [25] abind_1.4-8 withr_3.0.2 purrr_1.2.1 +## [28] R.oo_1.27.1 grid_4.6.0 e1071_1.7-17 +## [31] tinytex_0.59 cli_3.6.5 rmarkdown_2.31 +## [34] crayon_1.5.3 otel_0.2.0 rstudioapi_0.18.0 +## [37] DBI_1.3.0 cachem_1.1.0 proxy_0.4-29 +## [40] assertthat_0.2.1 parallel_4.6.0 BiocManager_1.30.27 +## [43] XVector_0.51.0 geoarrow_0.4.2 basilisk_1.23.0 +## [46] vctrs_0.7.2 Matrix_1.7-5 jsonlite_2.0.0 +## [49] dir.expiry_1.19.0 bookdown_0.46 bit64_4.6.0-1 +## [52] RBGL_1.87.0 Rgraphviz_2.55.0 magick_2.9.1 +## [55] jquerylib_0.1.4 units_1.0-1 glue_1.8.0 +## [58] ZarrArray_0.99.1 Rarr_1.11.32 tibble_3.3.1 +## [61] pillar_1.11.1 rappdirs_0.3.4 nanoarrow_0.8.0 +## [64] htmltools_0.5.9 graph_1.89.1 R6_2.6.1 +## [67] httr2_1.2.2 wk_0.9.5 evaluate_1.0.5 +## [70] lattice_0.22-9 R.methodsS3_1.8.2 png_0.1-9 +## [73] paws.common_0.8.9 bslib_0.10.0 class_7.3-23 +## [76] Rcpp_1.1.1 SparseArray_1.11.11 anndataR_1.1.2 +## [79] xfun_0.57 pkgconfig_2.0.3
    From ba297fb65debddbce090b30272f5917b4f89f65e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 10:06:06 +0200 Subject: [PATCH 030/151] prettify Zattrs printing --- vignettes/SpatialData.Rmd | 4 +- vignettes/SpatialData.html | 552 ++----------------------------------- 2 files changed, 26 insertions(+), 530 deletions(-) diff --git a/vignettes/SpatialData.Rmd b/vignettes/SpatialData.Rmd index b2dc6dfe..125e2ed6 100644 --- a/vignettes/SpatialData.Rmd +++ b/vignettes/SpatialData.Rmd @@ -119,9 +119,7 @@ To facilitate .zattrs handling, we provide a set of functions to access and modi - `rmv/addCT` to remove, add, or append coordinate transformations ```{r cs-methods} -z <- meta(label(x)) -axes(z) -CTdata(z) +(z <- meta(label(x))) CTname(rmvCT(z, "scale")) CTname(addCT(z, name="D'Artagnan", diff --git a/vignettes/SpatialData.html b/vignettes/SpatialData.html index 6c3fea83..f14611b5 100644 --- a/vignettes/SpatialData.html +++ b/vignettes/SpatialData.html @@ -843,12 +843,12 @@

    1.2 Annotations

    ## DataFrame with 6 rows and 1 column
     ##           n
     ##   <numeric>
    -## 1  0.179334
    -## 2  0.570150
    -## 3  0.280183
    -## 4  0.058967
    -## 5  0.105632
    -## 6  0.181170
    +## 1 0.771350 +## 2 0.260140 +## 3 0.960250 +## 4 0.779658 +## 5 0.460154 +## 6 0.408606
    # ...using a list of data generating functions
     f <- list(
         numbers=\(n) runif(n),
    @@ -859,12 +859,12 @@ 

    1.2 Annotations

    ## DataFrame with 6 rows and 2 columns
     ##     numbers     letters
     ##   <numeric> <character>
    -## 1 0.9456882           l
    -## 2 0.0721818           r
    -## 3 0.7813973           i
    -## 4 0.1646447           t
    -## 5 0.0205351           y
    -## 6 0.2196956           b
    +## 1 0.428206 w +## 2 0.947090 c +## 3 0.260630 z +## 4 0.759213 i +## 5 0.418562 y +## 6 0.554403 q

    1.3 Transformations

    @@ -874,520 +874,18 @@

    1.3 Transformations

  • CTdata/name/type to access coordinate transformation components
  • rmv/addCT to remove, add, or append coordinate transformations
  • -
    z <- meta(label(x))
    -axes(z)
    -
    ## [[1]]
    -## [[1]]$name
    -## [1] "y"
    -## 
    -## [[1]]$type
    -## [1] "space"
    -## 
    -## 
    -## [[2]]
    -## [[2]]$name
    -## [1] "x"
    -## 
    -## [[2]]$type
    -## [1] "space"
    -
    CTdata(z)
    -
    ## [[1]]
    -## [[1]]$input
    -## [[1]]$input$axes
    -## [[1]]$input$axes[[1]]
    -## [[1]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[1]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[1]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[1]]$input$axes[[2]]
    -## [[1]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[1]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[1]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[1]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[1]]$output
    -## [[1]]$output$axes
    -## [[1]]$output$axes[[1]]
    -## [[1]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[1]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[1]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[1]]$output$axes[[2]]
    -## [[1]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[1]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[1]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[1]]$output$name
    -## [1] "global"
    -## 
    -## 
    -## [[1]]$type
    -## [1] "identity"
    -## 
    -## 
    -## [[2]]
    -## [[2]]$input
    -## [[2]]$input$axes
    -## [[2]]$input$axes[[1]]
    -## [[2]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[2]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[2]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[2]]$input$axes[[2]]
    -## [[2]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[2]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[2]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[2]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[2]]$output
    -## [[2]]$output$axes
    -## [[2]]$output$axes[[1]]
    -## [[2]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[2]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[2]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[2]]$output$axes[[2]]
    -## [[2]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[2]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[2]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[2]]$output$name
    -## [1] "scale"
    -## 
    -## 
    -## [[2]]$scale
    -## [[2]]$scale[[1]]
    -## [1] 3
    -## 
    -## [[2]]$scale[[2]]
    -## [1] 2
    -## 
    -## 
    -## [[2]]$type
    -## [1] "scale"
    -## 
    -## 
    -## [[3]]
    -## [[3]]$input
    -## [[3]]$input$axes
    -## [[3]]$input$axes[[1]]
    -## [[3]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[3]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[3]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[3]]$input$axes[[2]]
    -## [[3]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[3]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[3]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[3]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[3]]$output
    -## [[3]]$output$axes
    -## [[3]]$output$axes[[1]]
    -## [[3]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[3]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[3]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[3]]$output$axes[[2]]
    -## [[3]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[3]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[3]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[3]]$output$name
    -## [1] "translation"
    -## 
    -## 
    -## [[3]]$translation
    -## [[3]]$translation[[1]]
    -## [1] -50
    -## 
    -## [[3]]$translation[[2]]
    -## [1] 10
    -## 
    -## 
    -## [[3]]$type
    -## [1] "translation"
    -## 
    -## 
    -## [[4]]
    -## [[4]]$affine
    -## [[4]]$affine[[1]]
    -## [[4]]$affine[[1]][[1]]
    -## [1] 20
    -## 
    -## [[4]]$affine[[1]][[2]]
    -## [1] 10
    -## 
    -## [[4]]$affine[[1]][[3]]
    -## [1] 30
    -## 
    -## 
    -## [[4]]$affine[[2]]
    -## [[4]]$affine[[2]][[1]]
    -## [1] 50
    -## 
    -## [[4]]$affine[[2]][[2]]
    -## [1] 40
    -## 
    -## [[4]]$affine[[2]][[3]]
    -## [1] 60
    -## 
    -## 
    -## 
    -## [[4]]$input
    -## [[4]]$input$axes
    -## [[4]]$input$axes[[1]]
    -## [[4]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[4]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[4]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[4]]$input$axes[[2]]
    -## [[4]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[4]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[4]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[4]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[4]]$output
    -## [[4]]$output$axes
    -## [[4]]$output$axes[[1]]
    -## [[4]]$output$axes[[1]]$name
    -## [1] "x"
    -## 
    -## [[4]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[4]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[4]]$output$axes[[2]]
    -## [[4]]$output$axes[[2]]$name
    -## [1] "y"
    -## 
    -## [[4]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[4]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[4]]$output$name
    -## [1] "affine"
    -## 
    -## 
    -## [[4]]$type
    -## [1] "affine"
    -## 
    -## 
    -## [[5]]
    -## [[5]]$input
    -## [[5]]$input$axes
    -## [[5]]$input$axes[[1]]
    -## [[5]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$input$axes[[2]]
    -## [[5]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[5]]$output
    -## [[5]]$output$axes
    -## [[5]]$output$axes[[1]]
    -## [[5]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$output$axes[[2]]
    -## [[5]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$output$name
    -## [1] "sequence"
    -## 
    -## 
    -## [[5]]$transformations
    -## [[5]]$transformations[[1]]
    -## [[5]]$transformations[[1]]$input
    -## [[5]]$transformations[[1]]$input$axes
    -## [[5]]$transformations[[1]]$input$axes[[1]]
    -## [[5]]$transformations[[1]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$transformations[[1]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[1]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$transformations[[1]]$input$axes[[2]]
    -## [[5]]$transformations[[1]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$transformations[[1]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[1]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$transformations[[1]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[5]]$transformations[[1]]$output
    -## [[5]]$transformations[[1]]$output$axes
    -## [[5]]$transformations[[1]]$output$axes[[1]]
    -## [[5]]$transformations[[1]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$transformations[[1]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[1]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$transformations[[1]]$output$axes[[2]]
    -## [[5]]$transformations[[1]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$transformations[[1]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[1]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$transformations[[1]]$output$name
    -## [1] "global"
    -## 
    -## 
    -## [[5]]$transformations[[1]]$scale
    -## [[5]]$transformations[[1]]$scale[[1]]
    -## [1] 3
    -## 
    -## [[5]]$transformations[[1]]$scale[[2]]
    -## [1] 2
    -## 
    -## 
    -## [[5]]$transformations[[1]]$type
    -## [1] "scale"
    -## 
    -## 
    -## [[5]]$transformations[[2]]
    -## [[5]]$transformations[[2]]$input
    -## [[5]]$transformations[[2]]$input$axes
    -## [[5]]$transformations[[2]]$input$axes[[1]]
    -## [[5]]$transformations[[2]]$input$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$transformations[[2]]$input$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[2]]$input$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$transformations[[2]]$input$axes[[2]]
    -## [[5]]$transformations[[2]]$input$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$transformations[[2]]$input$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[2]]$input$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$transformations[[2]]$input$name
    -## [1] "yx"
    -## 
    -## 
    -## [[5]]$transformations[[2]]$output
    -## [[5]]$transformations[[2]]$output$axes
    -## [[5]]$transformations[[2]]$output$axes[[1]]
    -## [[5]]$transformations[[2]]$output$axes[[1]]$name
    -## [1] "y"
    -## 
    -## [[5]]$transformations[[2]]$output$axes[[1]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[2]]$output$axes[[1]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## [[5]]$transformations[[2]]$output$axes[[2]]
    -## [[5]]$transformations[[2]]$output$axes[[2]]$name
    -## [1] "x"
    -## 
    -## [[5]]$transformations[[2]]$output$axes[[2]]$type
    -## [1] "space"
    -## 
    -## [[5]]$transformations[[2]]$output$axes[[2]]$unit
    -## [1] "unit"
    -## 
    -## 
    -## 
    -## [[5]]$transformations[[2]]$output$name
    -## [1] "global"
    -## 
    -## 
    -## [[5]]$transformations[[2]]$translation
    -## [[5]]$transformations[[2]]$translation[[1]]
    -## [1] -50
    -## 
    -## [[5]]$transformations[[2]]$translation[[2]]
    -## [1] 10
    -## 
    -## 
    -## [[5]]$transformations[[2]]$type
    -## [1] "translation"
    -## 
    -## 
    -## 
    -## [[5]]$type
    -## [1] "sequence"
    +
    (z <- meta(label(x)))
    +
    ## class: Zattrs
    +## axes(0):
    +## - name: y space x space 
    +## coordTrans(5):
    +## - global: (identity)
    +## - scale: (scale:[3,2])
    +## - translation: (translation:[-50,10])
    +## - affine: (affine:[[20,10,30],[50,40,60]])
    +## - sequence: (sequence)
    +## datasets(1): 0
    +## - 0: (scale:[1,1])
    CTname(rmvCT(z, "scale"))
    ## [1] "global"      "translation" "affine"      "sequence"
    CTname(addCT(z, 
    @@ -1403,7 +901,7 @@ 

    1.3 Transformations

    ## Number of Nodes = 14 ## Number of Edges = 13
    plotCoordGraph(g)
    -

    +

    The above representation greatly facilitates queries of the transformation(s) required to spatially align elements. blobs_labels, for example, requires a sequential transformation (scaling and translation) for the sequence space:

    From 4585ab588d2756c7f58d26b3e40157371632d241 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 17:30:55 +0200 Subject: [PATCH 031/151] tmpfix base::table ambuguity --- NAMESPACE | 1 + R/AllGenerics.R | 1 + R/ImageArray.R | 7 ++++++- R/Zattrs.R | 2 +- R/coord_utils.R | 30 ++++++++++++++++++++++----- R/table_utils.R | 11 +++++----- man/ImageArray.Rd | 5 ++++- man/coord-utils.Rd | 10 +++++++-- tests/testthat/test-imagearray.R | 10 --------- tests/testthat/test-labelarray.R | 13 +----------- tests/testthat/test-methods.R | 29 +++++++++++++------------- tests/testthat/test-query.R | 4 ++-- tests/testthat/test-zattrs.R | 2 +- vignettes/SpatialData.html | 35 ++++++++++++++++---------------- 14 files changed, 87 insertions(+), 73 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index fd2742d5..9d8209f5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,6 +39,7 @@ exportMethods("table<-") exportMethods("tables<-") exportMethods(CTdata) exportMethods(CTgraph) +exportMethods(CTlist) exportMethods(CTname) exportMethods(CTpath) exportMethods(CTtype) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 6e0a9488..5c24c415 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -53,6 +53,7 @@ setGeneric("tables<-", \(x, value) standardGeneric("tables<-")) # trs ---- setGeneric("axes", \(x, ...) standardGeneric("axes")) +setGeneric("CTlist", \(x, ...) standardGeneric("CTlist")) setGeneric("CTdata", \(x, ...) standardGeneric("CTdata")) setGeneric("CTname", \(x, ...) standardGeneric("CTname")) setGeneric("CTtype", \(x, ...) standardGeneric("CTtype")) diff --git a/R/ImageArray.R b/R/ImageArray.R index 4355d22b..82ba3677 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -32,7 +32,12 @@ ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), ...) { #' @rdname ImageArray #' @aliases channels #' @export -setMethod("channels", "ImageArray", \(x, ...) meta(x)$omero$channels$label) +setMethod("channels", "Zattrs", \(x, ...) x$omero$channels) + +#' @rdname ImageArray +#' @aliases channels +#' @export +setMethod("channels", "ImageArray", \(x, ...) channels(meta(x))) #' @rdname ImageArray #' @export diff --git a/R/Zattrs.R b/R/Zattrs.R index b86e78a6..0ba7391c 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -69,7 +69,7 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) ps[i], ct$type, g(ct[[ct$type]]))) } } - cs <- unlist(object$omero$channels) + cs <- unlist(channels(object)) if (!is.null(cs)) coolcat("channels(%d): %s\n", cs) } setMethod("show", "Zattrs", .showZattrs) diff --git a/R/coord_utils.R b/R/coord_utils.R index 03714971..8c12a290 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -62,11 +62,11 @@ setMethod("axes", "Zattrs", \(x, ...) { #' @export setMethod("axes", "SpatialDataElement", \(x, ...) axes(meta(x))) -# CTdata/type/name() ---- +# CTlist/data/type/name() ---- #' @rdname coord-utils #' @export -setMethod("CTdata", "Zattrs", \(x, ...) { +setMethod("CTlist", "Zattrs", \(x, ...) { ms <- "multiscales" ct <- "coordinateTransformations" if (is.null(x[[ms]])) return(x[[ct]]) @@ -75,15 +75,35 @@ setMethod("CTdata", "Zattrs", \(x, ...) { #' @rdname coord-utils #' @export -setMethod("CTtype", "Zattrs", \(x, ...) vapply(CTdata(x), \(.) .$type, character(1))) +setMethod("CTdata", "Zattrs", \(x, i=1, ...) { + stopifnot(length(i) == 1) + if (is.character(i)) { + match.arg(i, CTname(x)) + i <- match(i, CTname(x)) + } else { + stopifnot( + i == round(i), + i %in% seq_along(CTlist(x))) + } + t <- CTtype(x)[i] + CTlist(x)[[i]][[t]] +}) + +#' @rdname coord-utils +#' @export +setMethod("CTtype", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$type, character(1))) + +#' @rdname coord-utils +#' @export +setMethod("CTname", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$output$name, character(1))) #' @rdname coord-utils #' @export -setMethod("CTname", "Zattrs", \(x, ...) vapply(CTdata(x), \(.) .$output$name, character(1))) +setMethod("CTlist", "SpatialDataElement", \(x, ...) CTlist(meta(x))) #' @rdname coord-utils #' @export -setMethod("CTdata", "SpatialDataElement", \(x, ...) CTdata(meta(x))) +setMethod("CTdata", "SpatialDataElement", \(x, i=1, ...) CTdata(meta(x), i)) #' @rdname coord-utils #' @export diff --git a/R/table_utils.R b/R/table_utils.R index b875a0e2..91b3d5ca 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -121,15 +121,15 @@ setMethod("getTable", c("SpatialData", "ANY"), \(x, i, drop=TRUE) .invalid_i()) setMethod("getTable", c("SpatialData", "character"), \(x, i, drop=TRUE) { stopifnot(isTRUE(drop) || isFALSE(drop)) # get 'table' annotating 'i', if any - t <- table(x, hasTable(x, i, name=TRUE)) + t <- SpatialData::table(x, hasTable(x, i, name=TRUE)) # only keep observations belonging to 'i' (optional) if (drop) { rk <- meta(t)$region_key # TODO: check the replacement below, search colData as well? # t <- t[, int_colData(t)[[rk]] == i] - coldata <- - if(rk %in% names(cd <- int_colData(t))) cd[[rk]] else colData(t)[[rk]] - t <- t[, coldata == i] + int <- rk %in% names(cd <- int_colData(t)) + cd <- if (int) cd[[rk]] else t[[rk]] + t <- t[, cd == i] } return(t) }) @@ -221,8 +221,7 @@ setMethod("setTable", int_colData(sce) <- cbind(int_colData(sce), icd) md <- list(region=i, region_key=rk, instance_key=ik) int_metadata(sce)[[sda]] <- md - table(x, name) <- sce - return(x) + SpatialData::`table<-`(x, i=name, value=sce) }) # val ---- diff --git a/man/ImageArray.Rd b/man/ImageArray.Rd index 96c1b35a..5d237904 100644 --- a/man/ImageArray.Rd +++ b/man/ImageArray.Rd @@ -2,14 +2,17 @@ % Please edit documentation in R/ImageArray.R \name{ImageArray} \alias{ImageArray} -\alias{channels,ImageArray-method} +\alias{channels,Zattrs-method} \alias{channels} +\alias{channels,ImageArray-method} \alias{channels,ANY-method} \alias{[,ImageArray,ANY,ANY,ANY-method} \title{The `ImageArray` class} \usage{ ImageArray(data = list(), meta = Zattrs(), metadata = list(), ...) +\S4method{channels}{Zattrs}(x, ...) + \S4method{channels}{ImageArray}(x, ...) \S4method{channels}{ANY}(x, ...) diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index ee2a91b5..77896781 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -12,9 +12,11 @@ \alias{rmvCT} \alias{axes,Zattrs-method} \alias{axes,SpatialDataElement-method} +\alias{CTlist,Zattrs-method} \alias{CTdata,Zattrs-method} \alias{CTtype,Zattrs-method} \alias{CTname,Zattrs-method} +\alias{CTlist,SpatialDataElement-method} \alias{CTdata,SpatialDataElement-method} \alias{CTtype,SpatialDataElement-method} \alias{CTname,SpatialDataElement-method} @@ -34,13 +36,17 @@ \S4method{axes}{SpatialDataElement}(x, ...) -\S4method{CTdata}{Zattrs}(x, ...) +\S4method{CTlist}{Zattrs}(x, ...) + +\S4method{CTdata}{Zattrs}(x, i = 1, ...) \S4method{CTtype}{Zattrs}(x, ...) \S4method{CTname}{Zattrs}(x, ...) -\S4method{CTdata}{SpatialDataElement}(x, ...) +\S4method{CTlist}{SpatialDataElement}(x, ...) + +\S4method{CTdata}{SpatialDataElement}(x, i = 1, ...) \S4method{CTtype}{SpatialDataElement}(x, ...) diff --git a/tests/testthat/test-imagearray.R b/tests/testthat/test-imagearray.R index ae70feb9..ea212f90 100644 --- a/tests/testthat/test-imagearray.R +++ b/tests/testthat/test-imagearray.R @@ -30,13 +30,3 @@ test_that("data(),ImageArray", { expect_error(data(img, "")) expect_error(data(img, c(1,2))) }) - -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, tables=FALSE) - -test_that("[,ImageArray", { - y <- image(x, i <- "blobs_image") - y <- y[,seq_len(32)] # subset to make things harder -}) - diff --git a/tests/testthat/test-labelarray.R b/tests/testthat/test-labelarray.R index 00cfca11..a1c31547 100644 --- a/tests/testthat/test-labelarray.R +++ b/tests/testthat/test-labelarray.R @@ -16,7 +16,7 @@ test_that("LabelArray()", { lys <- lapply(dim, \(.) array(sample(arr, prod(.), replace=TRUE), dim=.)) expect_silent(LabelArray(lys)) }) -de + test_that("data(),LabelArray", { dim <- lapply(c(8, 4, 2), \(.) c(3, rep(., 2))) lys <- lapply(dim, \(.) array(0, dim=.)) @@ -30,14 +30,3 @@ test_that("data(),LabelArray", { expect_error(data(lab, "")) expect_error(data(lab, c(1,2))) }) - -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, tables=FALSE) - -test_that("[,LabelArray", { - y <- label(x, i <- "blobs_labels") - y <- y[,seq_len(32)] # subset to make things harder - y <- label(x, i <- "blobs_multiscale_labels") - y <- y[,seq_len(32)] # subset to make things harder -}) \ No newline at end of file diff --git a/tests/testthat/test-methods.R b/tests/testthat/test-methods.R index 8f312669..ebe9a90d 100644 --- a/tests/testthat/test-methods.R +++ b/tests/testthat/test-methods.R @@ -4,8 +4,9 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x) -sdtable <- SpatialData::table # skirt ambiguity and limitations of get() -"sdtable<-" = "SpatialData::table<-" # skirt ambiguity and limitations of get() +# skirt base::table ambiguity +sdtable <- SpatialData::table +`sdtable<-` <- `SpatialData::table<-` sdtables <- SpatialData::tables fun <- c("image", "label", "shape", "point", "sdtable") @@ -53,19 +54,19 @@ test_that("get one", { # i=numeric mapply(f=fun, t=typ, \(f, t) expect_is(get(f)(x, i=1), t)) - # i=character -- VJC Dec 8 2024 -- ambiguity of table()? -# mapply(f=fun, t=typ, n=nms, \(f, t, n) -# expect_is(get(f)(x, i=n), t)) + # i=character + mapply(f=fun, t=typ, n=nms, \(f, t, n) + expect_is(get(f)(x, i=n), t)) # i=invalid -# for (f in fun) { -# expect_error(get(f)(x, 0)) -# expect_error(get(f)(x, ".")) -# expect_error(get(f)(x, c(1,1))) -# expect_silent(y <- get(f)(x, Inf)) -# set <- get(paste0(f, "s<-")) -# y <- set(x, list()) -# expect_error(get(f)(y, 1)) -# } + for (f in fun) { + expect_error(get(f)(x, 0)) + expect_error(get(f)(x, ".")) + expect_error(get(f)(x, c(1,1))) + expect_silent(y <- get(f)(x, Inf)) + set <- get(paste0(f, "s<-")) + y <- set(x, list()) + expect_error(get(f)(y, 1)) + } }) # set ---- diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index c2b51413..ad46ca48 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -24,12 +24,12 @@ test_that("query,ImageArray", { # crop but don't shift j <- query(i, xmin=0, xmax=w <- d[3]/2, ymin=0, ymax=h <- d[2]/4) expect_equal(dim(j), c(3, h, w)) - expect_identical(CTdata(i), CTdata(j)) + expect_identical(CTlist(i), CTlist(j)) # crop and shift j <- query(i, xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) expect_equal(dim(j), c(3, 1+h-2, 1+w-1)) expect_equal(CTtype(j), t <- "translation") - expect_equivalent(CTdata(j)[[1]][[t]][[1]], c(0, 2, 1)) + expect_equivalent(CTlist(j)[[1]][[t]][[1]], c(0, 2, 1)) }) test_that("query,PointFrame", { diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index 60c1de00..9959bb01 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -43,7 +43,7 @@ test_that("addCT", { es <- lapply(ls, \(.) x[.,1][[.]][[1]]) .check_data <- \(z, x) { expect_true("." %in% CTname(z)) - ct <- CTdata(z)[[which(CTname(z) == ".")]] + ct <- CTlist(z)[[which(CTname(z) == ".")]] expect_identical(ct[[t]][[1]], x) } for (y in es) { diff --git a/vignettes/SpatialData.html b/vignettes/SpatialData.html index f14611b5..b7468ccf 100644 --- a/vignettes/SpatialData.html +++ b/vignettes/SpatialData.html @@ -843,12 +843,12 @@

    1.2 Annotations

    ## DataFrame with 6 rows and 1 column
     ##           n
     ##   <numeric>
    -## 1  0.771350
    -## 2  0.260140
    -## 3  0.960250
    -## 4  0.779658
    -## 5  0.460154
    -## 6  0.408606
    +## 1 0.237921 +## 2 0.921502 +## 3 0.895984 +## 4 0.697727 +## 5 0.825932 +## 6 0.573446
    # ...using a list of data generating functions
     f <- list(
         numbers=\(n) runif(n),
    @@ -859,12 +859,12 @@ 

    1.2 Annotations

    ## DataFrame with 6 rows and 2 columns
     ##     numbers     letters
     ##   <numeric> <character>
    -## 1  0.428206           w
    -## 2  0.947090           c
    -## 3  0.260630           z
    -## 4  0.759213           i
    -## 5  0.418562           y
    -## 6  0.554403           q
    +## 1 0.0232648 b +## 2 0.9720179 j +## 3 0.7792513 d +## 4 0.3494052 a +## 5 0.3148688 w +## 6 0.1408869 e

    1.3 Transformations

    @@ -880,9 +880,9 @@

    1.3 Transformations

    ## - name: y space x space ## coordTrans(5): ## - global: (identity) -## - scale: (scale:[3,2]) -## - translation: (translation:[-50,10]) -## - affine: (affine:[[20,10,30],[50,40,60]]) +## - scale: (scale) +## - translation: (translation) +## - affine: (affine) ## - sequence: (sequence) ## datasets(1): 0 ## - 0: (scale:[1,1]) @@ -892,8 +892,7 @@

    1.3 Transformations

    name="D'Artagnan", type="scale", data=c(19, 94))) -
    ## [1] "global"      "scale"       "translation" "affine"      "sequence"   
    -## [6] "D'Artagnan"
    +
    ## [1] "D'Artagnan"

    Zattrs specify an explicit relationship between elements and coordinate systems. We can represent these are a graph as follows:

    (g <- CTgraph(x))
    @@ -901,7 +900,7 @@

    1.3 Transformations

    ## Number of Nodes = 14 ## Number of Edges = 13
    plotCoordGraph(g)
    -

    +

    The above representation greatly facilitates queries of the transformation(s) required to spatially align elements. blobs_labels, for example, requires a sequential transformation (scaling and translation) for the sequence space:

    From 9b67c37d0f0ad688ec9cfac46102169cb5232f5e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 17:45:31 +0200 Subject: [PATCH 032/151] simplify table retrieval --- R/table_utils.R | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/R/table_utils.R b/R/table_utils.R index 91b3d5ca..87515cd0 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -98,15 +98,11 @@ setMethod("hasTable", c("SpatialData", "character"), \(x, i, name=FALSE) { match.arg(i, unlist(nms[idx])) # count occurrences t <- lapply(tables(x), \(t) meta(t)$region) - n <- vapply(seq_along(t), \(.) i %in% t[[.]], numeric(1)) - nan <- all(n == 0) + ok <- rapply(t, \(.) i %in% ., "character") # failure when no/many matches - if (name) { - dup <- length(unique(n)) != length(n) - if (nan) stop("no 'table' found for 'i'") - if (dup) stop("multiple 'table's found for 'i'") - return(names(t)[n == 1]) - } else return(!nan) + if (!name) return(any(ok)) + if (!any(ok)) stop("no 'table' found for 'i'") + if (sum(ok) > 0) stop("multiple 'table's found for 'i'") }) # get ---- From e6c2346c6b9ecc6533b9207d6097f9e78f9c9a62 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 1 Apr 2026 17:51:36 +0200 Subject: [PATCH 033/151] fix tests --- R/table_utils.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/R/table_utils.R b/R/table_utils.R index 87515cd0..8e604bf6 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -100,9 +100,12 @@ setMethod("hasTable", c("SpatialData", "character"), \(x, i, name=FALSE) { t <- lapply(tables(x), \(t) meta(t)$region) ok <- rapply(t, \(.) i %in% ., "character") # failure when no/many matches - if (!name) return(any(ok)) - if (!any(ok)) stop("no 'table' found for 'i'") - if (sum(ok) > 0) stop("multiple 'table's found for 'i'") + if (name) { + if (!any(ok)) stop("no 'table' found for 'i'") + if (sum(ok) > 1) stop("multiple 'table's found for 'i'") + return(names(t)[ok]) + } + return(any(ok)) }) # get ---- From 8eb6574c7dd7ebcd55c68d6af4cf632f4e490a46 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 2 Apr 2026 09:17:41 +0200 Subject: [PATCH 034/151] bug fix Zattrs show --- R/Zattrs.R | 17 +++++++++-------- R/table_utils.R | 10 ++++------ man/Zattrs.Rd | 6 +++--- vignettes/SpatialData.html | 37 +++++++++++++++++++------------------ 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/R/Zattrs.R b/R/Zattrs.R index 0ba7391c..017dcb61 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -11,11 +11,11 @@ #' x <- system.file(x, package="SpatialData") #' x <- readSpatialData(x, tables=FALSE) #' -#' z <- meta(label(x)) -#' axes(z) -#' CTdata(z) +#' (z <- meta(label(x))) +#' #' CTname(z) #' CTtype(z) +#' CTdata(z, "scale") #' #' @export Zattrs <- \(x=list()) { @@ -36,12 +36,13 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) .showZattrs <- function(object) { cat("class: Zattrs\n") - cat(sprintf("axes(%d):\n", length(x))) - if (!is.null(x <- axes(object))) { - cat("- name:", unlist(x), "\n") + ax <- axes(object) + cat(sprintf("axes(%d):\n", length(ax))) + if (is.character(ax[[1]])) { + cat("- name:", unlist(ax), "\n") } else { - cat("- name:", vapply(x, \(.) .$name, character(1)), "\n") - cat("- type:", vapply(x, \(.) .$type, character(1)), "\n") + cat("- name:", vapply(ax, \(.) .$name, character(1)), "\n") + cat("- type:", vapply(ax, \(.) .$type, character(1)), "\n") } cat(sprintf("coordTrans(%d):\n", n <- length(CTname(object)))) g <- \(.) { diff --git a/R/table_utils.R b/R/table_utils.R index 8e604bf6..78d81783 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -100,12 +100,10 @@ setMethod("hasTable", c("SpatialData", "character"), \(x, i, name=FALSE) { t <- lapply(tables(x), \(t) meta(t)$region) ok <- rapply(t, \(.) i %in% ., "character") # failure when no/many matches - if (name) { - if (!any(ok)) stop("no 'table' found for 'i'") - if (sum(ok) > 1) stop("multiple 'table's found for 'i'") - return(names(t)[ok]) - } - return(any(ok)) + if (!name) return(any(ok)) + if (!any(ok)) stop("no 'table' found for 'i'") + if (sum(ok) > 1) stop("multiple 'table's found for 'i'") + return(names(t)[ok]) }) # get ---- diff --git a/man/Zattrs.Rd b/man/Zattrs.Rd index b736e2e7..366844b1 100644 --- a/man/Zattrs.Rd +++ b/man/Zattrs.Rd @@ -25,10 +25,10 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) -z <- meta(label(x)) -axes(z) -CTdata(z) +(z <- meta(label(x))) + CTname(z) CTtype(z) +CTdata(z, "scale") } diff --git a/vignettes/SpatialData.html b/vignettes/SpatialData.html index b7468ccf..d5ef9378 100644 --- a/vignettes/SpatialData.html +++ b/vignettes/SpatialData.html @@ -10,7 +10,7 @@ - + SpatialData @@ -702,7 +702,7 @@

    SpatialData

    Helena Lucia Crowell, Louise Deconinck, Artür Manukyan, Dario Righelli, Estella Dong and Vince Carey

    -

    April 01, 2026

    +

    April 02, 2026

    Package

    SpatialData 0.99.25

    @@ -790,7 +790,7 @@

    1.1 Handling

    ## See $metadata for additional Schema metadata
    meta(shape(x)) 
    ## class: Zattrs
    -## axes(0):
    +## axes(2):
     ## - name: x y 
     ## coordTrans(1):
     ## - global: (identity)
    @@ -843,12 +843,12 @@

    1.2 Annotations

    ## DataFrame with 6 rows and 1 column
     ##           n
     ##   <numeric>
    -## 1  0.237921
    -## 2  0.921502
    -## 3  0.895984
    -## 4  0.697727
    -## 5  0.825932
    -## 6  0.573446
    +## 1 0.493024 +## 2 0.316782 +## 3 0.187876 +## 4 0.733530 +## 5 0.515164 +## 6 0.173435
    # ...using a list of data generating functions
     f <- list(
         numbers=\(n) runif(n),
    @@ -859,12 +859,12 @@ 

    1.2 Annotations

    ## DataFrame with 6 rows and 2 columns
     ##     numbers     letters
     ##   <numeric> <character>
    -## 1 0.0232648           b
    -## 2 0.9720179           j
    -## 3 0.7792513           d
    -## 4 0.3494052           a
    -## 5 0.3148688           w
    -## 6 0.1408869           e
    +## 1 0.553310 z +## 2 0.614395 y +## 3 0.929654 d +## 4 0.371769 h +## 5 0.786802 k +## 6 0.689226 p

    1.3 Transformations

    @@ -876,8 +876,9 @@

    1.3 Transformations

    (z <- meta(label(x)))
    ## class: Zattrs
    -## axes(0):
    -## - name: y space x space 
    +## axes(2):
    +## - name: y x 
    +## - type: space space 
     ## coordTrans(5):
     ## - global: (identity)
     ## - scale: (scale)
    @@ -900,7 +901,7 @@ 

    1.3 Transformations

    ## Number of Nodes = 14 ## Number of Edges = 13
    plotCoordGraph(g)
    -

    +

    The above representation greatly facilitates queries of the transformation(s) required to spatially align elements. blobs_labels, for example, requires a sequential transformation (scaling and translation) for the sequence space:

    From 5169febb2bff1719e598cbc9df06ea928a49ec48 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 2 Apr 2026 09:35:53 +0200 Subject: [PATCH 035/151] anndataR=TRUE everywhere --- R/SpatialData.R | 2 +- R/read.R | 16 ++++++++-------- R/table_utils.R | 2 +- man/SpatialData.Rd | 2 +- man/readSpatialData.Rd | 2 +- man/table-utils.Rd | 2 +- tests/testthat/test-PointFrame.R | 5 +++-- tests/testthat/test-zattrs.R | 2 +- vignettes/SpatialData.html | 30 +++++++++++++++--------------- 9 files changed, 32 insertions(+), 31 deletions(-) diff --git a/R/SpatialData.R b/R/SpatialData.R index 3f203c28..bacbfe6f 100644 --- a/R/SpatialData.R +++ b/R/SpatialData.R @@ -30,7 +30,7 @@ #' @examples #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") -#' (x <- readSpatialData(x, anndataR=FALSE)) +#' (x <- readSpatialData(x, anndataR=TRUE)) #' #' # subsetting #' # layers are taken in order of appearance diff --git a/R/read.R b/R/read.R index 2a85f616..95be1117 100644 --- a/R/read.R +++ b/R/read.R @@ -81,7 +81,7 @@ NULL #' @importFrom Rarr read_zarr_attributes #' @importFrom ZarrArray ZarrArray -readsdlayer <- function(x, ...) { +.readArray <- function(x, ...) { md <- read_zarr_attributes(x) ps <- .get_multiscales_dataset_paths(md) ps <- file.path(x, as.character(ps)) @@ -92,15 +92,15 @@ readsdlayer <- function(x, ...) { #' @rdname readSpatialData #' @export readImage <- function(x, ...) { - lyrs <- readsdlayer(x, ...) - ImageArray(data=lyrs$array, meta=Zattrs(lyrs$md), ...) + l <- .readArray(x, ...) + ImageArray(data=l$array, meta=Zattrs(l$md), ...) } #' @rdname readSpatialData #' @export readLabel <- function(x, ...) { - lyrs <- readsdlayer(x, ...) - LabelArray(data=lyrs$array, meta=Zattrs(lyrs$md), ...) + l <- .readArray(x, ...) + LabelArray(data=l$array, meta=Zattrs(l$md), ...) } #' @rdname readSpatialData @@ -119,10 +119,10 @@ readPoint <- function(x, ...) { #' @import geoarrow #' @export readShape <- function(x, ...) { - requireNamespace("geoarrow", quietly=TRUE) - md <- read_zarr_attributes(x) # TODO: previously had read_parquet(), # but that doesn't work with geoparquet? + #requireNamespace("geoarrow", quietly=TRUE) + md <- read_zarr_attributes(x) pq <- list.files(x, "\\.parquet$", full.names=TRUE) ShapeFrame(data=open_dataset(pq), meta=Zattrs(md)) } @@ -199,7 +199,7 @@ readTable <- function(x) { #' @export readSpatialData <- function(x, images=TRUE, labels=TRUE, points=TRUE, - shapes=TRUE, tables=TRUE, anndataR=FALSE) { + shapes=TRUE, tables=TRUE, anndataR=TRUE) { if (!anndataR) tables <- FALSE # will do manually below args <- as.list(environment())[.LAYERS] skip <- vapply(args, isFALSE, logical(1)) diff --git a/R/table_utils.R b/R/table_utils.R index 78d81783..720b4231 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -22,7 +22,7 @@ #' library(SingleCellExperiment) #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") -#' x <- readSpatialData(x, anndataR=FALSE) +#' x <- readSpatialData(x, anndataR=TRUE) #' #' # check if element has a 'table' #' hasTable(x, "blobs_points") diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index e760cfd6..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -126,7 +126,7 @@ or NULL/\code{list()} to remove an element/layer completely.} \examples{ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -(x <- readSpatialData(x, anndataR=FALSE)) +(x <- readSpatialData(x, anndataR=TRUE)) # subsetting # layers are taken in order of appearance diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index f0d16ae7..4557dd6b 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -26,7 +26,7 @@ readSpatialData( points = TRUE, shapes = TRUE, tables = TRUE, - anndataR = FALSE + anndataR = TRUE ) } \arguments{ diff --git a/man/table-utils.Rd b/man/table-utils.Rd index 709cd7c4..b83b2f8c 100644 --- a/man/table-utils.Rd +++ b/man/table-utils.Rd @@ -63,7 +63,7 @@ specifies which \code{assay} to use when \code{j} is a row name.} library(SingleCellExperiment) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, anndataR=FALSE) +x <- readSpatialData(x, anndataR=TRUE) # check if element has a 'table' hasTable(x, "blobs_points") diff --git a/tests/testthat/test-PointFrame.R b/tests/testthat/test-PointFrame.R index 2761a6ac..d9397525 100644 --- a/tests/testthat/test-PointFrame.R +++ b/tests/testthat/test-PointFrame.R @@ -45,7 +45,8 @@ test_that("select", { replicate(3, { n <- sample(ncol(p), 1) i <- sample(names(p), n) - y <- select(p, i); z <- data(p)[, i] + y <- select(p, all_of(i)) + z <- data(p)[, i] expect_equal(collect(data(y)), collect(z)) }) }) @@ -56,4 +57,4 @@ test_that("as.data.frame", { expect_equal(dim(y), dim(p)) expect_equal(names(y), names(p)) expect_identical(y, (. <- collect(data(p)))[, !grepl("dask", names(.))]) -}) \ No newline at end of file +}) diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index 9959bb01..ab0cd84c 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -1,6 +1,6 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, anndataR=FALSE) +x <- readSpatialData(x, anndataR=TRUE) test_that("axes", { # image diff --git a/vignettes/SpatialData.html b/vignettes/SpatialData.html index d5ef9378..0f8bfcff 100644 --- a/vignettes/SpatialData.html +++ b/vignettes/SpatialData.html @@ -841,14 +841,14 @@

    1.2 Annotations

    y <- setTable(x, i, df) head(colData(getTable(y, i)))
    ## DataFrame with 6 rows and 1 column
    -##           n
    -##   <numeric>
    -## 1  0.493024
    -## 2  0.316782
    -## 3  0.187876
    -## 4  0.733530
    -## 5  0.515164
    -## 6  0.173435
    +## n +## <numeric> +## 1 0.00353685 +## 2 0.94029116 +## 3 0.78284891 +## 4 0.97153644 +## 5 0.81024104 +## 6 0.72704077
    # ...using a list of data generating functions
     f <- list(
         numbers=\(n) runif(n),
    @@ -859,12 +859,12 @@ 

    1.2 Annotations

    ## DataFrame with 6 rows and 2 columns
     ##     numbers     letters
     ##   <numeric> <character>
    -## 1  0.553310           z
    -## 2  0.614395           y
    -## 3  0.929654           d
    -## 4  0.371769           h
    -## 5  0.786802           k
    -## 6  0.689226           p
    +## 1 0.9760238 f +## 2 0.8935270 p +## 3 0.0271508 l +## 4 0.6642598 a +## 5 0.3210180 o +## 6 0.4329136 d

    1.3 Transformations

    @@ -901,7 +901,7 @@

    1.3 Transformations

    ## Number of Nodes = 14 ## Number of Edges = 13
    plotCoordGraph(g)
    -

    +

    The above representation greatly facilitates queries of the transformation(s) required to spatially align elements. blobs_labels, for example, requires a sequential transformation (scaling and translation) for the sequence space:

    From 3ee8ef25e0c777eef9115c7c88ed44726f7e2e07 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 2 Apr 2026 09:44:39 +0200 Subject: [PATCH 036/151] add misc examples to doc --- R/misc.R | 21 +++++++++++++++++---- man/misc.Rd | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/R/misc.R b/R/misc.R index 3ca30006..ad128704 100644 --- a/R/misc.R +++ b/R/misc.R @@ -1,5 +1,5 @@ #' @name misc -#' @title Miscellaneous `Miro` methods +#' @title Miscellaneous `SpatialData` methods #' @description ... #' #' @param object \code{\link{SpatialData}} object or one of its @@ -10,7 +10,19 @@ #' @author Helena L. Crowell #' #' @examples -#' # TODO +#' zs <- file.path("extdata", "blobs.zarr") +#' zs <- system.file(zs, package="SpatialData") +#' (sd <- readSpatialData(zs, anndataR=TRUE)) +#' +#' # show element +#' image(sd) +#' label(sd) +#' point(sd) +#' shape(sd) +#' +#' # show .zattrs +#' meta(label(sd)) +#' meta(image(sd, 2)) NULL #' @importFrom RBGL sp.between @@ -55,11 +67,12 @@ NULL for (. in seq_along(t)) cat(sprintf(" - %s (%s)\n", t[.], d[.])) # spaces - cat("coordinate systems:\n") e <- c(i, l, s, p) g <- CTgraph(object) t <- nodeData(g, nodes(g), "type") - for (c in nodes(g)[t == "space"]) { + n <- sum(i <- (t == "space")) + cat(sprintf("coordinate systems(%s):\n", n)) + for (c in nodes(g)[i]) { pa <- suppressWarnings(sp.between(g, e, c)) ss <- strsplit(names(pa), ":") ss <- ss[vapply(pa, \(.) !is.na(.$length), logical(1))] diff --git a/man/misc.Rd b/man/misc.Rd index 32e489b2..a67f7ec8 100644 --- a/man/misc.Rd +++ b/man/misc.Rd @@ -6,7 +6,7 @@ \alias{show,sdArray-method} \alias{show,PointFrame-method} \alias{show,ShapeFrame-method} -\title{Miscellaneous `Miro` methods} +\title{Miscellaneous `SpatialData` methods} \usage{ \S4method{show}{SpatialData}(object) @@ -27,7 +27,19 @@ elements, i.e., an Image/LabelArray or Point/ShapeFrame.} ... } \examples{ -# TODO +zs <- file.path("extdata", "blobs.zarr") +zs <- system.file(zs, package="SpatialData") +(sd <- readSpatialData(zs, anndataR=TRUE)) + +# show element +image(sd) +label(sd) +point(sd) +shape(sd) + +# show .zattrs +meta(label(sd)) +meta(image(sd, 2)) } \author{ Helena L. Crowell From 0cff1617f7959e7870b34de874534fc07874a092 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 2 Apr 2026 09:52:32 +0200 Subject: [PATCH 037/151] bug fix new CT accession --- R/coord_utils.R | 15 +++++++++------ man/coord-utils.Rd | 8 ++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/R/coord_utils.R b/R/coord_utils.R index 8c12a290..0aa9f965 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -34,12 +34,12 @@ #' #' # add #' addCT(z, "scale", "scale", c(12, 34)) # overwrite -#' CTdata(addCT(z, "new", "translation", c(12, 34))) +#' CTname(addCT(z, "new", "translation", c(12, 34))) #' #' # rmv -#' CTdata(rmvCT(z, 2)) # by index -#' CTdata(rmvCT(z, "scale")) # by name -#' CTdata(rmvCT(z, 1)) # identity is protected +#' CTname(rmvCT(z, 2)) # by index +#' CTname(rmvCT(z, "scale")) # by name +#' CTname(rmvCT(z, "global")) # identity is protected NULL # TODO: currently applying transformations only on 'data.frame's for plotting, @@ -248,7 +248,10 @@ setMethod("rmvCT", "Zattrs", \(x, i) { i <- match(i, nms) # protect against dropping identity i <- i[CTtype(x)[i] != "identity"] - if (!length(i)) stop("can't drop identity") + if (!length(i)) { + warning("can't drop identity") + return(x) + } ms <- "multiscales" ct <- "coordinateTransformations" if (length(i)) { @@ -290,7 +293,7 @@ setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { is.character(type), length(type) == 1) .check_ct(x, type, data) # use existing as skeleton - old <- CTdata(x) + old <- CTlist(x) new <- old[[1]][c("input", "output", "type")] new$type <- type new$output$name <- name diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index 77896781..b1d3efd7 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -114,10 +114,10 @@ CTname(z <- meta(label(x))) # add addCT(z, "scale", "scale", c(12, 34)) # overwrite -CTdata(addCT(z, "new", "translation", c(12, 34))) +CTname(addCT(z, "new", "translation", c(12, 34))) # rmv -CTdata(rmvCT(z, 2)) # by index -CTdata(rmvCT(z, "scale")) # by name -CTdata(rmvCT(z, 1)) # identity is protected +CTname(rmvCT(z, 2)) # by index +CTname(rmvCT(z, "scale")) # by name +CTname(rmvCT(z, "global")) # identity is protected } From abc41a7b65e89c6f838e69e2ce84e06791e1290a Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sun, 5 Apr 2026 17:18:13 +0200 Subject: [PATCH 038/151] fix .zattrs accession --- R/ImageArray.R | 5 +++-- R/table_utils.R | 2 +- R/trans.R | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/R/ImageArray.R b/R/ImageArray.R index 82ba3677..c3b86d8f 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -32,7 +32,7 @@ ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), ...) { #' @rdname ImageArray #' @aliases channels #' @export -setMethod("channels", "Zattrs", \(x, ...) x$omero$channels) +setMethod("channels", "Zattrs", \(x, ...) unlist(x$omero$channels)) #' @rdname ImageArray #' @aliases channels @@ -48,7 +48,8 @@ setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) # validate 'multiscales' .validate_multiscales_dataset_path(md) # get & validate 'path's - ps <- md$multiscales[[1]]$datasets[[1]]$path + ds <- md$multiscales[[1]]$datasets + ps <- vapply(ds, \(.) .$path, character(1)) ps <- suppressWarnings(as.numeric(sort(ps, decreasing=FALSE))) if (length(ps)) { qs <- seq(min(ps), max(ps)) diff --git a/R/table_utils.R b/R/table_utils.R index 720b4231..784c7c47 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -98,7 +98,7 @@ setMethod("hasTable", c("SpatialData", "character"), \(x, i, name=FALSE) { match.arg(i, unlist(nms[idx])) # count occurrences t <- lapply(tables(x), \(t) meta(t)$region) - ok <- rapply(t, \(.) i %in% ., "character") + ok <- vapply(t, \(.) i %in% ., logical(1)) # failure when no/many matches if (!name) return(any(ok)) if (!any(ok)) stop("no 'table' found for 'i'") diff --git a/R/trans.R b/R/trans.R index 34ccf39e..f477746c 100644 --- a/R/trans.R +++ b/R/trans.R @@ -143,6 +143,7 @@ setMethod("translation", c("ShapeFrame", "numeric"), \(x, t, ...) { for (. in seq_along(ts)) { t <- ts[[.]]$type d <- ts[[.]]$data + d <- unlist(d) if (length(d) == 3) d <- d[-1] switch(t, From a24115a2423455f1a4b20febf489b6f9456b1f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BCr=20Manukyan?= Date: Sun, 5 Apr 2026 21:45:53 +0200 Subject: [PATCH 039/151] Modify GHA --- .github/workflows/check-bioc.yml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 91cae76c..5fefcaa8 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -52,8 +52,8 @@ jobs: fail-fast: false matrix: config: - - { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - - { os: ubuntu-latest, r: '4.6', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } + - { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://p3m.dev/cran/__linux__/noble/latest" } + - { os: ubuntu-latest, r: 'devel', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://p3m.dev/cran/__linux__/noble/latest" } - { os: macOS-latest, r: 'devel', bioc: 'devel'} - { os: windows-latest, r: 'devel', bioc: 'devel'} ## Check https://github.com/r-lib/actions/tree/master/examples @@ -117,13 +117,6 @@ jobs: key: ${{ env.cache-version }}-${{ runner.os }}-biocversion-devel-r-4.5-${{ hashFiles('.github/depends.Rds') }} restore-keys: ${{ env.cache-version }}-${{ runner.os }}-biocversion-devel-r-4.5- - # - name: Install Linux system dependencies - # if: runner.os == 'Linux' - # run: | - # sysreqs=$(Rscript -e 'cat("apt-get update -y && apt-get install -y", paste(gsub("apt-get install -y ", "", remotes::system_requirements("ubuntu", "20.04")), collapse = " "))') - # echo $sysreqs - # sudo -s eval "$sysreqs" - - name: Install macOS system dependencies if: matrix.config.os == 'macOS-latest' run: | @@ -131,10 +124,6 @@ jobs: brew install libxml2 echo "XML_CONFIG=/usr/local/opt/libxml2/bin/xml2-config" >> $GITHUB_ENV - ## ## Required to install magick as noted at - ## ## https://github.com/r-lib/usethis/commit/f1f1e0d10c1ebc75fd4c18fa7e2de4551fd9978f#diff-9bfee71065492f63457918efcd912cf2 - ## brew install imagemagick@6 - ## For textshaping, required by ragg, and required by pkgdown brew install harfbuzz fribidi @@ -172,7 +161,11 @@ jobs: ## For running the checks message(paste('****', Sys.time(), 'installing rcmdcheck and BiocCheck ****')) - install.packages(c("rcmdcheck", "BiocCheck"), repos = BiocManager::repositories()) + install.packages(c("rcmdcheck", "BiocCheck", "remotes"), repos = BiocManager::repositories()) + + ## TEMP REMOVE LATER: + ## For now install Rarr from github since ZarrArray needs >= 1.11.33 + remotes::install_github("Huber-group-EMBL/Rarr") ## Pass #1 at installing dependencies message(paste('****', Sys.time(), 'pass number 1 at installing dependencies: local dependencies ****')) From 9b4a82a9307d0dbb6c9e5e5031ff66b7a078507f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 09:50:39 +0200 Subject: [PATCH 040/151] +data_type() method --- NAMESPACE | 3 +++ R/AllGenerics.R | 1 + R/sdArray.R | 13 ++++++++++++- man/Array-methods.Rd | 6 ++++++ man/SpatialData.Rd | 8 ++++---- tests/testthat/test-sdarray.R | 22 ++++++++++++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/test-sdarray.R diff --git a/NAMESPACE b/NAMESPACE index 9d8209f5..acc0530c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -49,6 +49,7 @@ exportMethods(axes) exportMethods(channels) exportMethods(colnames) exportMethods(data) +exportMethods(data_type) exportMethods(dim) exportMethods(element) exportMethods(getTable) @@ -94,6 +95,7 @@ importFrom(Matrix,sparseVector) importFrom(Matrix,t) importFrom(RBGL,sp.between) importFrom(Rarr,read_zarr_attributes) +importFrom(Rarr,zarr_overview) importFrom(S4Arrays,as.array.Array) importFrom(S4Vectors,"metadata<-") importFrom(S4Vectors,coolcat) @@ -110,6 +112,7 @@ importFrom(SummarizedExperiment,"colData<-") importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) +importFrom(ZarrArray,path) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) importFrom(basilisk,basiliskRun) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 5c24c415..3a18d600 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -81,6 +81,7 @@ setGeneric("query", \(x, ...) standardGeneric("query")) setGeneric("mask", \(x, i, j, ...) standardGeneric("mask")) setGeneric("channels", \(x, ...) standardGeneric("channels")) +setGeneric("data_type", \(x, ...) standardGeneric("data_type")) # tbl ---- diff --git a/R/sdArray.R b/R/sdArray.R index bc20d61d..956cdd8b 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -46,4 +46,15 @@ setMethod("dim", "sdArray", \(x) dim(data(x))) #' @rdname Array-methods #' @export -setMethod("length", "sdArray", \(x) length(data(x, NULL))) \ No newline at end of file +setMethod("length", "sdArray", \(x) length(data(x, NULL))) + +#' @rdname Array-methods +#' @export +setMethod("data_type", "sdArray", \(x) data_type(data(x))) + +#' @rdname Array-methods +#' @importFrom DelayedArray DelayedArray +#' @importFrom Rarr zarr_overview +#' @importFrom ZarrArray path +#' @export +setMethod("data_type", "DelayedArray", \(x) zarr_overview(path(x), as_data_frame=TRUE)$data_type) diff --git a/man/Array-methods.Rd b/man/Array-methods.Rd index 489c139a..6b609313 100644 --- a/man/Array-methods.Rd +++ b/man/Array-methods.Rd @@ -11,6 +11,8 @@ \alias{data,sdArray-method} \alias{dim,sdArray-method} \alias{length,sdArray-method} +\alias{data_type,sdArray-method} +\alias{data_type,DelayedArray-method} \title{Methods for `ImageArray` and `LabelArray` class} \usage{ \S4method{data}{sdArray}(x, k = 1) @@ -18,6 +20,10 @@ \S4method{dim}{sdArray}(x) \S4method{length}{sdArray}(x) + +\S4method{data_type}{sdArray}(x) + +\S4method{data_type}{DelayedArray}(x) } \arguments{ \item{x}{\code{ImageArray} or \code{LabelArray}} diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/tests/testthat/test-sdarray.R b/tests/testthat/test-sdarray.R new file mode 100644 index 00000000..de123f94 --- /dev/null +++ b/tests/testthat/test-sdarray.R @@ -0,0 +1,22 @@ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, anndataR=TRUE) + +test_that("data_type()", { + # image + za <- data(image(x)) + dt <- data_type(za) + expect_length(dt, 1) + expect_is(dt, "character") + expect_identical(dt, "float64") + expect_identical(dt, data_type(za[1,,])) + expect_identical(dt, data_type(image(x))) + # label + za <- data(label(x)) + dt <- data_type(za) + expect_length(dt, 1) + expect_is(dt, "character") + expect_identical(dt, "int16") + expect_identical(dt, data_type(head(za))) + expect_identical(dt, data_type(label(x))) +}) From 2d3c1669cf28196613f363680a8bd3e7b593f113 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 10:09:44 +0200 Subject: [PATCH 041/151] fix CTdata accession for type sequence --- NAMESPACE | 1 + R/coord_utils.R | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index acc0530c..a3778f80 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -89,6 +89,7 @@ importClassesFrom(S4Vectors,DFrame) importFrom(BiocGenerics,as.data.frame) importFrom(BiocGenerics,colnames) importFrom(BiocGenerics,rownames) +importFrom(DelayedArray,DelayedArray) importFrom(DelayedArray,realize) importFrom(Matrix,rowSums) importFrom(Matrix,sparseVector) diff --git a/R/coord_utils.R b/R/coord_utils.R index 0aa9f965..b0068066 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -86,7 +86,11 @@ setMethod("CTdata", "Zattrs", \(x, i=1, ...) { i %in% seq_along(CTlist(x))) } t <- CTtype(x)[i] - CTlist(x)[[i]][[t]] + if (t != "sequence") + return(CTlist(x)[[i]][[t]]) + ts <- CTlist(x)[[i]]$transformations + names(ts) <- vapply(ts, \(.) .$type, character(1)) + mapply(x=ts, i=names(ts), \(x, i) x[[i]], SIMPLIFY=FALSE) }) #' @rdname coord-utils From 22737443913a6c7f4290a81c4c5e5f1ac4b4cefa Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 10:19:45 +0200 Subject: [PATCH 042/151] fix double action trigger? --- .github/workflows/check-bioc.yml | 1 - R/trans.R | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 91cae76c..b5f53b7f 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -22,7 +22,6 @@ on: push: - pull_request: name: R-CMD-check-bioc diff --git a/R/trans.R b/R/trans.R index f477746c..2434db2d 100644 --- a/R/trans.R +++ b/R/trans.R @@ -45,8 +45,8 @@ NULL #' @rdname trans #' @export -setMethod("scale", c("ImageArray", "numeric"), \(x, j, t, ...) { - stopifnot(length(t) == 3, t > 0) +setMethod("scale", c("sdArray", "numeric"), \(x, j, t, ...) { + stopifnot(length(t) == length(dim(x)), t > 0) if (all(t == 1)) return(x) if (is.numeric(j)) j <- CTname(x) j <- match.arg(j, CTname(x)) From a2bf8f51546e65589576a0424f90e7d35135bd94 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 10:20:25 +0200 Subject: [PATCH 043/151] skip R v4.5 --- .github/workflows/check-bioc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index b5f53b7f..527b4a34 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -51,7 +51,7 @@ jobs: fail-fast: false matrix: config: - - { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } + #- { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - { os: ubuntu-latest, r: '4.6', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest" } - { os: macOS-latest, r: 'devel', bioc: 'devel'} - { os: windows-latest, r: 'devel', bioc: 'devel'} From 4ba36175ebc5f127c6da3c7a752fbed9cf983d1b Mon Sep 17 00:00:00 2001 From: "Helena L. Crowell" Date: Mon, 6 Apr 2026 10:30:03 +0200 Subject: [PATCH 044/151] Update R/coord_utils.R Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- R/coord_utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/coord_utils.R b/R/coord_utils.R index b0068066..d2b587f8 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -260,7 +260,7 @@ setMethod("rmvCT", "Zattrs", \(x, i) { ct <- "coordinateTransformations" if (length(i)) { if (is.null(x[[ms]])) { - x[[ct]] <- x[[ct]][i] + x[[ct]] <- x[[ct]][-i] } else { y <- x[[ms]][[1]][[ct]][-i] x[[ms]][[1]][[ct]] <- y From 276f6ba921311e5b23eac6a3f31ff668d8679e82 Mon Sep 17 00:00:00 2001 From: "Helena L. Crowell" Date: Mon, 6 Apr 2026 10:30:30 +0200 Subject: [PATCH 045/151] Update R/ImageArray.R Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- R/ImageArray.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/ImageArray.R b/R/ImageArray.R index c3b86d8f..ba1b8c4d 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -53,7 +53,7 @@ setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) ps <- suppressWarnings(as.numeric(sort(ps, decreasing=FALSE))) if (length(ps)) { qs <- seq(min(ps), max(ps)) - if (!all.equal(ps, qs)) + if (!isTRUE(all.equal(ps, qs))) stop("ImageArray paths are ill-defined, should", " be an integer sequence, e.g., 0,1,...,n") } From c07abb842b7891cfc4510c2282dbee7d103269f2 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 11:05:10 +0200 Subject: [PATCH 046/151] fix typo --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 14317366..85239e41 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,7 +3,7 @@ Title: Representation of Python's SpatialData in R Depends: R (>= 4.5) Version: 0.99.25 Description: Interface to Python's 'SpatialData', currently including: - reticulate-based use of 'spatialdata-io' for reading of manufracturer + reticulate-based use of 'spatialdata-io' for reading of manufacturer data and writing to .zarr, on-disk representation of images/labels as 'ZarrArray's ('Rarr') and shapes/points as 'arrow' objects, and method drafts for visualization and coordinate system handling. From b42b94717f0bb39556aae4fe8024b80e07d6822f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 11:05:22 +0200 Subject: [PATCH 047/151] rmv duplicated SetMethod's --- R/AllGenerics.R | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 3a18d600..c92b0aa0 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -22,18 +22,6 @@ setGeneric("shapeNames", \(x, ...) standardGeneric("shapeNames")) setGeneric("pointNames", \(x, ...) standardGeneric("pointNames")) setGeneric("tableNames", \(x, ...) standardGeneric("tableNames")) -setMethod("images", "SpatialData", \(x) x$images) -setMethod("labels", "SpatialData", \(x) x$labels) -setMethod("shapes", "SpatialData", \(x) x$shapes) -setMethod("points", "SpatialData", \(x) x$points) -setMethod("tables", "SpatialData", \(x) x$tables) - -setMethod("imageNames", "SpatialData", \(x) names(images(x))) -setMethod("labelNames", "SpatialData", \(x) names(labels(x))) -setMethod("shapeNames", "SpatialData", \(x) names(shapes(x))) -setMethod("pointNames", "SpatialData", \(x) names(points(x))) -setMethod("tableNames", "SpatialData", \(x) names(tables(x))) - # set one ----- setGeneric("image<-", \(x, i, ..., value) standardGeneric("image<-")) From 0000232dac171009f391f63fb753c9e8aff0522f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 11:05:39 +0200 Subject: [PATCH 048/151] implement copi suggestions --- R/ShapeFrame.R | 2 +- R/coord_utils.R | 2 +- R/read.R | 2 +- R/validity.R | 4 ++-- man/trans.Rd | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index 7e8bdb55..ca88b8bf 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -82,7 +82,7 @@ setMethod("[", c("ShapeFrame", "missing", "missing"), #' @export setMethod("[", c("ShapeFrame", "numeric", "numeric"), \(x, i, j, ...) { i <- seq_len(nrow(x))[i] - j <- seq_len(nrow(x))[j] + j <- seq_len(ncol(x))[j] x@data <- x@data[i, j] return(x) }) diff --git a/R/coord_utils.R b/R/coord_utils.R index d2b587f8..83451fc4 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -247,7 +247,7 @@ setMethod("rmvCT", "Zattrs", \(x, i) { } nan <- setdiff(i, nms) if (length(nan)) stop( - "couln't find 'coordTrans' of name(s) ", + "couldn't find 'coordTrans' of name(s) ", paste(dQuote(nan), collapse=",")) i <- match(i, nms) # protect against dropping identity diff --git a/R/read.R b/R/read.R index 95be1117..f415960e 100644 --- a/R/read.R +++ b/R/read.R @@ -211,7 +211,7 @@ readSpatialData <- function(x, if (is.numeric(opt) && opt > (. <- length(j))) stop("'", i, "=", opt, "', but only ", ., " elements found") if (is.character(opt) && length(. <- setdiff(opt, basename(j)))) - stop("couln't find ", i, " of name", .) + stop("couldn't find ", i, " of name", .) j <- j[opt] } f <- get(paste0("read", toupper(substr(i, 1, 1)), substr(i, 2, nchar(i)-1))) diff --git a/R/validity.R b/R/validity.R index 5da54752..75287b9f 100644 --- a/R/validity.R +++ b/R/validity.R @@ -38,9 +38,9 @@ .validateImageArray <- function(object) { msg <- c() if (ni <- length(images(object))) { - for (i in seq_along(ni)) { + for (i in seq_len(ni)) { ai <- as.array(aperm(data(image(object,1))/255, perm=c(3,2,1))) - for (j in dim(ai)[3]) { + for (j in seq_len(dim(ai)[3])) { if (!all(vapply(ai[,,j], is.numeric, logical(1)))) { msg <- c(msg, paste0("Image ", i, " channel ", j, " not numeric")) } diff --git a/man/trans.Rd b/man/trans.Rd index 7bc36c57..deeb1e0c 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -5,7 +5,7 @@ \alias{scale} \alias{rotate} \alias{translation} -\alias{scale,ImageArray,numeric-method} +\alias{scale,sdArray,numeric-method} \alias{scale,PointFrame,numeric-method} \alias{rotate,PointFrame,numeric-method} \alias{translation,PointFrame,numeric-method} @@ -14,7 +14,7 @@ \alias{translation,ShapeFrame,numeric-method} \title{Transformations} \usage{ -\S4method{scale}{ImageArray,numeric}(x, j, t, ...) +\S4method{scale}{sdArray,numeric}(x, j, t, ...) \S4method{scale}{PointFrame,numeric}(x, t, ...) From f90c91d67c75f5efd7d7055ad37ce2c72cd603a4 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 13:48:32 +0200 Subject: [PATCH 049/151] get started on image/label transformations --- NAMESPACE | 2 ++ R/trans.R | 46 +++++++++++++++++++++++++++++++- man/trans.Rd | 7 +++-- tests/testthat/test-trans.R | 53 +++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/test-trans.R diff --git a/NAMESPACE b/NAMESPACE index a3778f80..02e0c15f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -90,6 +90,8 @@ importFrom(BiocGenerics,as.data.frame) importFrom(BiocGenerics,colnames) importFrom(BiocGenerics,rownames) importFrom(DelayedArray,DelayedArray) +importFrom(DelayedArray,cbind) +importFrom(DelayedArray,rbind) importFrom(DelayedArray,realize) importFrom(Matrix,rowSums) importFrom(Matrix,sparseVector) diff --git a/R/trans.R b/R/trans.R index 2434db2d..3451bbf8 100644 --- a/R/trans.R +++ b/R/trans.R @@ -55,7 +55,51 @@ setMethod("scale", c("sdArray", "numeric"), \(x, j, t, ...) { # label ---- -#TODO +#' @rdname trans +#' @importFrom DelayedArray cbind rbind +#' @importFrom methods as +#' @export +setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { + #x <- label(sd, 2); t <- c(64,0) + stopifnot( + length(t) == length(dim(x)), + t == round(t), all(is.finite(t))) + if (all(t == 0)) return(x) + ys <- data(x, NULL) + 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) + } + x@data <- lapply(seq_along(ys), \(k) { + t <- ts[[k]] + y <- as(ys[[k]], "DelayedArray") + # TODO: no 'abind' support so that we + # permute, 'c/rbind', and back-permute; + # surely there has to be a better way? + if (length(dim(y)) == 2) { + n <- NULL + } else { + t <- t[-1] + n <- dim(x)[1] + y <- aperm(y, c(2,3,1)) + } + if (t[2] != 0) { + d <- c(nrow(y), abs(t[2]), n) + 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)) + y <- if (t[1] > 0) rbind(z, y) else rbind(y, z) + } + if (!is.null(n)) aperm(y, c(3,1,2)) else y + }) + return(x) +}) # point ---- diff --git a/man/trans.Rd b/man/trans.Rd index 7bc36c57..b132b1d3 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -5,7 +5,8 @@ \alias{scale} \alias{rotate} \alias{translation} -\alias{scale,ImageArray,numeric-method} +\alias{scale,sdArray,numeric-method} +\alias{translation,sdArray,numeric-method} \alias{scale,PointFrame,numeric-method} \alias{rotate,PointFrame,numeric-method} \alias{translation,PointFrame,numeric-method} @@ -14,7 +15,9 @@ \alias{translation,ShapeFrame,numeric-method} \title{Transformations} \usage{ -\S4method{scale}{ImageArray,numeric}(x, j, t, ...) +\S4method{scale}{sdArray,numeric}(x, j, t, ...) + +\S4method{translation}{sdArray,numeric}(x, t, ...) \S4method{scale}{PointFrame,numeric}(x, t, ...) diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R new file mode 100644 index 00000000..4be23273 --- /dev/null +++ b/tests/testthat/test-trans.R @@ -0,0 +1,53 @@ +zs <- file.path("extdata", "blobs.zarr") +zs <- system.file(zs, package="SpatialData") +sd <- readSpatialData(zs, tables=FALSE) + +test_that("translation,imageArray", { + x <- image(sd, 1) + y <- translation(x, c(0,0,0)) + expect_identical(x, y) + expect_error(translation(x, numeric(2))) + expect_error(translation(x, numeric(4))) + expect_error(translation(x, character(3))) + # 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) +}) + +test_that("translation,labelArray", { + x <- label(sd, 1) + y <- translation(x, c(0,0)) + expect_identical(x, y) + expect_error(translation(x, numeric(1))) + expect_error(translation(x, numeric(3))) + expect_error(translation(x, character(2))) + # row + t <- c(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, 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) + # multiscale + x <- label(sd, 2) + t <- c(n <- nrow(x), 0) + y <- translation(x, t) + dx <- vapply(data(x, NULL), dim, integer(2)) + dy <- vapply(data(y, NULL), dim, integer(2)) + expect_equal(dx[1,], dy[1,]/2) + expect_identical(dx[2,], dy[2,]) +}) From c13dbab029acb6ea004b12035b0803385a85b25e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 14:28:30 +0200 Subject: [PATCH 050/151] update SpatialData.data retrieval --- NAMESPACE | 1 + R/ImageArray.R | 5 ++--- R/PointFrame.R | 5 ++--- R/ShapeFrame.R | 6 +++--- R/read.R | 3 +-- R/sdArray.R | 3 +-- R/trans.R | 6 +++--- man/Array-methods.Rd | 3 +-- man/ImageArray.Rd | 5 ++--- man/PointFrame.Rd | 5 ++--- man/ShapeFrame.Rd | 6 +++--- man/readSpatialData.Rd | 3 +-- 12 files changed, 22 insertions(+), 29 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 02e0c15f..6b3bfcdc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -89,6 +89,7 @@ importClassesFrom(S4Vectors,DFrame) importFrom(BiocGenerics,as.data.frame) importFrom(BiocGenerics,colnames) importFrom(BiocGenerics,rownames) +importFrom(DelayedArray,ConstantArray) importFrom(DelayedArray,DelayedArray) importFrom(DelayedArray,cbind) importFrom(DelayedArray,rbind) diff --git a/R/ImageArray.R b/R/ImageArray.R index ba1b8c4d..afc52939 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -15,9 +15,8 @@ #' #' @examples #' library(SpatialData.data) -#' dir.create(td <- tempfile()) -#' pa <- SpatialData.data:::.unzip_merfish_demo(td) -#' pa <- file.path(pa, "images", "rasterized") +#' zs <- get_demo_SDdata("merfish") +#' pa <- file.path(zs, "images", "rasterized") #' (ia <- readImage(pa)) #' #' @importFrom S4Vectors metadata<- diff --git a/R/PointFrame.R b/R/PointFrame.R index d8476e09..456d257c 100644 --- a/R/PointFrame.R +++ b/R/PointFrame.R @@ -32,9 +32,8 @@ #' #' @examples #' library(SpatialData.data) -#' dir.create(tf <- tempfile()) -#' base <- SpatialData.data:::.unzip_merfish_demo(tf) -#' x <- file.path(base, "points", "single_molecule") +#' zs <- get_demo_SDdata("merfish") +#' x <- file.path(zs, "points", "single_molecule") #' (p <- readPoint(x)) #' #' head(as.data.frame(data(p))) diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index ca88b8bf..b4a130ad 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -16,9 +16,9 @@ #' #' @examples #' library(SpatialData.data) -#' dir.create(tf <- tempfile()) -#' base <- SpatialData.data:::.unzip_merfish_demo(tf) -#' y <- file.path(base, "shapes", "cells") +#' zs <- get_demo_SDdata("merfish") +#' +#' y <- file.path(zs, "shapes", "cells") #' (s <- readShape(y)) #' plot(sf::st_as_sf(data(s)), cex=0.2) #' diff --git a/R/read.R b/R/read.R index f415960e..f0dbec78 100644 --- a/R/read.R +++ b/R/read.R @@ -64,8 +64,7 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", #' #' @examples #' library(SpatialData.data) -#' dir.create(tf <- tempfile()) -#' zs <- SpatialData.data:::.unzip_merfish_demo(tf) +#' zs <- get_demo_SDdata("merfish") #' #' # read complete Zarr store #' (sd <- readSpatialData(zs, anndataR=TRUE)) diff --git a/R/sdArray.R b/R/sdArray.R index 956cdd8b..8e005e5f 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -16,8 +16,7 @@ #' #' @examples #' library(SpatialData.data) -#' dir.create(tf <- tempfile()) -#' zs <- SpatialData.data:::.unzip_merfish_demo(tf) +#' zs <- get_demo_SDdata("merfish") #' #' # helper that gets path to first element in layer 'l' #' fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] diff --git a/R/trans.R b/R/trans.R index 3451bbf8..448ce192 100644 --- a/R/trans.R +++ b/R/trans.R @@ -56,7 +56,7 @@ setMethod("scale", c("sdArray", "numeric"), \(x, j, t, ...) { # label ---- #' @rdname trans -#' @importFrom DelayedArray cbind rbind +#' @importFrom DelayedArray cbind rbind ConstantArray #' @importFrom methods as #' @export setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { @@ -88,12 +88,12 @@ setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { } if (t[2] != 0) { d <- c(nrow(y), abs(t[2]), n) - z <- DelayedArray(array(0, d)) + z <- 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 <- DelayedArray(array(0, d)) + z <- ConstantArray(0, dim=d) y <- if (t[1] > 0) rbind(z, y) else rbind(y, z) } if (!is.null(n)) aperm(y, c(3,1,2)) else y diff --git a/man/Array-methods.Rd b/man/Array-methods.Rd index 6b609313..1990122e 100644 --- a/man/Array-methods.Rd +++ b/man/Array-methods.Rd @@ -38,8 +38,7 @@ Methods for `ImageArray` and `LabelArray` class } \examples{ library(SpatialData.data) -dir.create(tf <- tempfile()) -zs <- SpatialData.data:::.unzip_merfish_demo(tf) +zs <- get_demo_SDdata("merfish") # helper that gets path to first element in layer 'l' fn <- \(l) list.files(file.path(zs, l), full.names=TRUE)[1] diff --git a/man/ImageArray.Rd b/man/ImageArray.Rd index 5d237904..936e9966 100644 --- a/man/ImageArray.Rd +++ b/man/ImageArray.Rd @@ -45,9 +45,8 @@ The `ImageArray` class } \examples{ library(SpatialData.data) -dir.create(td <- tempfile()) -pa <- SpatialData.data:::.unzip_merfish_demo(td) -pa <- file.path(pa, "images", "rasterized") +zs <- get_demo_SDdata("merfish") +pa <- file.path(zs, "images", "rasterized") (ia <- readImage(pa)) } diff --git a/man/PointFrame.Rd b/man/PointFrame.Rd index b2a140b0..1830a9b2 100644 --- a/man/PointFrame.Rd +++ b/man/PointFrame.Rd @@ -79,9 +79,8 @@ Currently defined methods (here, \code{x} is a \code{PointFrame}): } \examples{ library(SpatialData.data) -dir.create(tf <- tempfile()) -base <- SpatialData.data:::.unzip_merfish_demo(tf) -x <- file.path(base, "points", "single_molecule") +zs <- get_demo_SDdata("merfish") +x <- file.path(zs, "points", "single_molecule") (p <- readPoint(x)) head(as.data.frame(data(p))) diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index bb940c2d..c9b5eafb 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -57,9 +57,9 @@ The `ShapeFrame` class } \examples{ library(SpatialData.data) -dir.create(tf <- tempfile()) -base <- SpatialData.data:::.unzip_merfish_demo(tf) -y <- file.path(base, "shapes", "cells") +zs <- get_demo_SDdata("merfish") + +y <- file.path(zs, "shapes", "cells") (s <- readShape(y)) plot(sf::st_as_sf(data(s)), cex=0.2) diff --git a/man/readSpatialData.Rd b/man/readSpatialData.Rd index 4557dd6b..812a51dc 100644 --- a/man/readSpatialData.Rd +++ b/man/readSpatialData.Rd @@ -57,8 +57,7 @@ Reading `SpatialData` } \examples{ library(SpatialData.data) -dir.create(tf <- tempfile()) -zs <- SpatialData.data:::.unzip_merfish_demo(tf) +zs <- get_demo_SDdata("merfish") # read complete Zarr store (sd <- readSpatialData(zs, anndataR=TRUE)) From d77d514b5ece942f8613013af4e31422e4e29028 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 14:42:59 +0200 Subject: [PATCH 051/151] +translation,PointFrame tests --- R/trans.R | 2 ++ tests/testthat/test-trans.R | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/R/trans.R b/R/trans.R index 448ce192..8d0aadf8 100644 --- a/R/trans.R +++ b/R/trans.R @@ -133,6 +133,8 @@ setMethod("rotate", c("PointFrame", "numeric"), \(x, t, ...) { #' @importFrom dplyr mutate select #' @export setMethod("translation", c("PointFrame", "numeric"), \(x, t, ...) { + stopifnot(is.numeric(t), length(t) == 2, all(is.finite(t))) + if (all(t == 0)) return(x) y <- NULL # R CMD check x@data <- x@data |> mutate(x=x+t[1]) |> diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index 4be23273..b8ba3db8 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -51,3 +51,26 @@ test_that("translation,labelArray", { expect_equal(dx[1,], dy[1,]/2) expect_identical(dx[2,], dy[2,]) }) + +test_that("translation,PointFrame", { + x <- point(sd, 1) + y <- translation(x, c(0,0)) + expect_identical(x, y) + # invalid + expect_error(translation(x, numeric(1))) + expect_error(translation(x, numeric(3))) + expect_error(translation(x, logical(2))) + expect_error(translation(x, c(Inf, Inf))) + expect_error(translation(x, character(2))) + expect_error(translation(x, NA*numeric(2))) + # valid + i <- setdiff(names(x), c("x", "y")) + f <- \() sample(33, 1)*sample(c(-1, 1), 1) + 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]) + }) +}) From b35dd2c6d075f4dabe29d38bdd41c523109d4ba7 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 14:56:51 +0200 Subject: [PATCH 052/151] +rotate,PointFrame tests --- R/trans.R | 31 ++++++++++++++++--------------- tests/testthat/test-trans.R | 24 +++++++++++++++++++++++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/R/trans.R b/R/trans.R index 8d0aadf8..abc966c9 100644 --- a/R/trans.R +++ b/R/trans.R @@ -118,6 +118,8 @@ setMethod("scale", c("PointFrame", "numeric"), \(x, t, ...) { #' @importFrom dplyr mutate select #' @export setMethod("rotate", c("PointFrame", "numeric"), \(x, t, ...) { + stopifnot(is.numeric(t), length(t) == 1, is.finite(t)) + if (t %% 360 == 0) return(x) y <- .y <- .x <- NULL # R CMD check R <- .R(t*pi/180) x@data <- x@data |> @@ -133,7 +135,7 @@ setMethod("rotate", c("PointFrame", "numeric"), \(x, t, ...) { #' @importFrom dplyr mutate select #' @export setMethod("translation", c("PointFrame", "numeric"), \(x, t, ...) { - stopifnot(is.numeric(t), length(t) == 2, all(is.finite(t))) + stopifnot(is.numeric(t), length(t) == 2, is.finite(t)) if (all(t == 0)) return(x) y <- NULL # R CMD check x@data <- x@data |> @@ -144,11 +146,23 @@ setMethod("translation", c("PointFrame", "numeric"), \(x, t, ...) { # shape ---- +# TODO: do this w/o realizing +#' @importFrom sf st_as_sf st_geometry st_geometry<- +.trans_s <- \(x, f) { + y <- st_as_sf(data(x)) + xy <- st_coordinates(y) + xy <- data.frame(f(xy)) + xy <- st_as_sf(xy, coords=names(xy)) + st_geometry(y) <- st_geometry(xy) + x@data <- y + return(x) +} + #' @rdname trans #' @importFrom sf st_as_sf st_coordinates #' @export setMethod("scale", c("ShapeFrame", "numeric"), \(x, t, ...) { - stopifnot(is.numeric(t), length(t) == 2, t > 0, all(is.finite(t))) + stopifnot(is.numeric(t), length(t) == 2, t > 0, is.finite(t)) .trans_s(x, \(xy) sweep(xy, 2, t, `*`)) }) @@ -210,16 +224,3 @@ setMethod("translation", c("ShapeFrame", "numeric"), \(x, t, ...) { } return(xy) } - -# transform 'ShapeFrame' by realizing, -# and updating 'sf' geometry coordinates -#' @importFrom sf st_as_sf st_geometry st_geometry<- -.trans_s <- \(x, f) { - y <- st_as_sf(data(x)) - xy <- st_coordinates(y) - xy <- data.frame(f(xy)) - xy <- st_as_sf(xy, coords=names(xy)) - st_geometry(y) <- st_geometry(xy) - x@data <- y - return(x) -} \ No newline at end of file diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index b8ba3db8..39418408 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -71,6 +71,28 @@ test_that("translation,PointFrame", { 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]) + expect_identical(x[,i], y[,i]) + }) +}) + +test_that("rotate,PointFrame", { + x <- point(sd, 1) + y <- rotate(x, 0) + expect_identical(x, y) + # invalid + expect_error(rotate(x, Inf)) + expect_error(rotate(x, numeric(2))) + expect_error(rotate(x, logical(1))) + expect_error(rotate(x, character(1))) + expect_error(rotate(x, NA*numeric(1))) + # valid + i <- setdiff(names(x), c("x", "y")) + f <- \() sample(360, 1)*sample(c(-1, 1), 1) + g <- \(x) cbind(x$x, x$y) + replicate(10, \() { + y <- rotate(x, t <- f()) + R <- .R(t*base::pi/180) + expect_identical(g(x) %*% R, g(y)) + for (. in i) expect_identical(x[[.]], y[[.]]) }) }) From 34ffa0034d4ec89c1ad66407149980f8a9cf2593 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 15:23:01 +0200 Subject: [PATCH 053/151] unskip tests --- R/trans.R | 13 ++++++------- tests/testthat/test-trans.R | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/R/trans.R b/R/trans.R index abc966c9..fdeea8a1 100644 --- a/R/trans.R +++ b/R/trans.R @@ -120,14 +120,13 @@ setMethod("scale", c("PointFrame", "numeric"), \(x, t, ...) { setMethod("rotate", c("PointFrame", "numeric"), \(x, t, ...) { stopifnot(is.numeric(t), length(t) == 1, is.finite(t)) if (t %% 360 == 0) return(x) - y <- .y <- .x <- NULL # R CMD check + y <- a <- b <- c <- d <- NULL # R CMD check R <- .R(t*pi/180) x@data <- x@data |> - mutate(.x=x*R[1,1], .y=y*R[1,2]) |> - mutate(x=.x+.y) |> - mutate(.x=x*R[2,1], .y=y*R[2,2]) |> - mutate(y=.x+.y) |> - select(-.x, -.y) + mutate(a=x*R[1,1], b=y*R[1,2]) |> + mutate(c=x*R[2,1], d=y*R[2,2]) |> + mutate(x=a+b, y=c+d) |> + select(-c(a,b, c,d)) return(x) }) @@ -185,7 +184,7 @@ setMethod("translation", c("ShapeFrame", "numeric"), \(x, t, ...) { # utils ---- # rotation matrix to rotate points counter-clockwise through an angle 't' -.R <- function(t) matrix(c(cos(t), -sin(t), sin(t), cos(t)), 2, 2) +.R <- \(t) matrix(c(cos(t), sin(t), -sin(t), cos(t)), 2, 2) # count occurrences of each coordinate space; # return most frequent (in order of appearance) diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index 39418408..b9bbe1cc 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -66,12 +66,12 @@ test_that("translation,PointFrame", { # valid i <- setdiff(names(x), c("x", "y")) f <- \() sample(33, 1)*sample(c(-1, 1), 1) - replicate(10, \() { + 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]) + expect_equal(x$x+n, y$x) + expect_equal(x$y+m, y$y) + for (. in i) expect_identical(x[[.]], y[[.]]) }) }) @@ -88,11 +88,11 @@ test_that("rotate,PointFrame", { # valid i <- setdiff(names(x), c("x", "y")) f <- \() sample(360, 1)*sample(c(-1, 1), 1) - g <- \(x) cbind(x$x, x$y) - replicate(10, \() { + g <- \(.) cbind(.$x, .$y) + replicate(10, { y <- rotate(x, t <- f()) R <- .R(t*base::pi/180) - expect_identical(g(x) %*% R, g(y)) + expect_equal(t(R %*% t(g(x))), g(y)) for (. in i) expect_identical(x[[.]], y[[.]]) }) }) From 176f9fa9407bd1204aab45469fcef216710494f2 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 15:44:39 +0200 Subject: [PATCH 054/151] +scale,pointFrame tests --- R/trans.R | 2 ++ tests/testthat/test-trans.R | 31 +++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/R/trans.R b/R/trans.R index fdeea8a1..4deeadba 100644 --- a/R/trans.R +++ b/R/trans.R @@ -107,6 +107,8 @@ setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { #' @importFrom dplyr mutate #' @export setMethod("scale", c("PointFrame", "numeric"), \(x, t, ...) { + stopifnot(is.numeric(t), length(t) == 2, t > 0, is.finite(t)) + if (all(t == 1)) return(x) y <- NULL # R CMD check x@data <- x@data |> mutate(x=x*t[1]) |> diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index b9bbe1cc..a43d240d 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -42,7 +42,7 @@ test_that("translation,labelArray", { expect_equal(dim(y), dim(x)+t) expect_is(data(y), "DelayedArray") expect_true(sum(data(y)[,seq_len(n)]) == 0) - # multiscale + # res x <- label(sd, 2) t <- c(n <- nrow(x), 0) y <- translation(x, t) @@ -66,7 +66,7 @@ test_that("translation,PointFrame", { # valid i <- setdiff(names(x), c("x", "y")) f <- \() sample(33, 1)*sample(c(-1, 1), 1) - replicate(10, { + replicate(5, { n <- f(); m <- f() y <- translation(x, c(n,m)) expect_equal(x$x+n, y$x) @@ -75,6 +75,29 @@ test_that("translation,PointFrame", { }) }) +test_that("scale,PointFrame", { + x <- point(sd, 1) + y <- scale(x, c(1, 1)) + expect_identical(x, y) + # invalid + expect_error(scale(x, -c(1, 1))) + expect_error(scale(x, c(1, -1))) + expect_error(scale(x, numeric(1))) + expect_error(scale(x, numeric(3))) + expect_error(scale(x, logical(2))) + expect_error(scale(x, character(2))) + expect_error(scale(x, NA*numeric(2))) + expect_error(scale(x, c(Inf, Inf))) + # valid + i <- setdiff(names(x), c("x", "y")) + f <- \() replicate(2, runif(1, 0.1, 2)) + g <- \(.) cbind(.$x, .$y) + replicate(5, { + y <- scale(x, t <- f()) + expect_equal(sweep(g(x), 2, t, `*`), g(y)) + }) +}) + test_that("rotate,PointFrame", { x <- point(sd, 1) y <- rotate(x, 0) @@ -87,9 +110,9 @@ test_that("rotate,PointFrame", { expect_error(rotate(x, NA*numeric(1))) # valid i <- setdiff(names(x), c("x", "y")) - f <- \() sample(360, 1)*sample(c(-1, 1), 1) + f <- \() sample(777, 1)*sample(c(-1, 1), 1) g <- \(.) cbind(.$x, .$y) - replicate(10, { + replicate(5, { y <- rotate(x, t <- f()) R <- .R(t*base::pi/180) expect_equal(t(R %*% t(g(x))), g(y)) From 311d400b96f4f2f6902b85fdd5a159cb67917a16 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 15:44:44 +0200 Subject: [PATCH 055/151] fix doc typo --- R/ShapeFrame.R | 2 +- man/ShapeFrame.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index b4a130ad..e33b2aab 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -22,7 +22,7 @@ #' (s <- readShape(y)) #' plot(sf::st_as_sf(data(s)), cex=0.2) #' -#' y <- file.path(base, "shapes", "anatomical") +#' y <- file.path(zs, "shapes", "anatomical") #' (s <- readShape(y)) #' plot(sf::st_as_sf(data(s)), cex=0.2) #' diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index c9b5eafb..5f252f0b 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -63,7 +63,7 @@ y <- file.path(zs, "shapes", "cells") (s <- readShape(y)) plot(sf::st_as_sf(data(s)), cex=0.2) -y <- file.path(base, "shapes", "anatomical") +y <- file.path(zs, "shapes", "anatomical") (s <- readShape(y)) plot(sf::st_as_sf(data(s)), cex=0.2) From 8e8d1f92cf6a75d5f42fb0bdbce2de5b4b6f1203 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 17:17:51 +0200 Subject: [PATCH 056/151] update docs --- man/SpatialData.Rd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From bf0f42ae850ea4328c58bcc27c289a59007ecd02 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:29:03 +0200 Subject: [PATCH 057/151] init per-layer validity --- NAMESPACE | 1 + R/validity.R | 83 ++++++++++++++++++-------------- tests/testthat/test-labelarray.R | 12 ++--- tests/testthat/test-validity.R | 36 ++++++++++++++ 4 files changed, 89 insertions(+), 43 deletions(-) create mode 100644 tests/testthat/test-validity.R diff --git a/NAMESPACE b/NAMESPACE index 6b3bfcdc..e0822d18 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -117,6 +117,7 @@ importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) +importFrom(ZarrArray,type) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) importFrom(basilisk,basiliskRun) diff --git a/R/validity.R b/R/validity.R index 75287b9f..e2a3fdfa 100644 --- a/R/validity.R +++ b/R/validity.R @@ -17,38 +17,46 @@ return(msg) } -.validatePointFrame <- function(object) { - msg <- NULL - # Checks if the points have the x,y coordinates, as they are hard-coded - # in the plot functions - if (length(points(object))) { # there are some cases where the points are empty - if (!is.null(data(point(object)))) { - np <- length(points(object)) - for (i in seq_len(np)) { - dfi <- data(point(object, i)) - if (!all(c("x", "y") %in% names(dfi))) { - msg <- c(msg, paste0("'x' and 'y' missing in data point ", i)) - } - } - } - } +.validatePointFrame <- \(object) { + msg <- c() + if (!length(object)) return(msg) + if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") + if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") return(msg) } +#' @importFrom S4Vectors setValidity2 +setValidity2("PointFrame", .validatePointFrame) -.validateImageArray <- function(object) { +.validateImageArray <- \(object) { msg <- c() - if (ni <- length(images(object))) { - for (i in seq_len(ni)) { - ai <- as.array(aperm(data(image(object,1))/255, perm=c(3,2,1))) - for (j in seq_len(dim(ai)[3])) { - if (!all(vapply(ai[,,j], is.numeric, logical(1)))) { - msg <- c(msg, paste0("Image ", i, " channel ", j, " not numeric")) - } - } - } + res <- length(object) + for (k in seq_len(res)) { + x <- data(object, k) + if (length(dim(x)) != 3) msg <- c(msg, paste( + "'ImageArray' resolution", k, "is not 3D")) + if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( + "'ImageArray' resolution", k, "is not of type double or integer")) } - return(msg) + if (length(msg)) return(msg) else return(TRUE) } +#' @importFrom S4Vectors setValidity2 +setValidity2("ImageArray", .validateImageArray) + +#' @importFrom ZarrArray type +.validateLabelArray <- \(object) { + msg <- c() + res <- length(object) + for (k in seq_len(res)) { + x <- data(object, k) + if (length(dim(x)) != 2) msg <- c(msg, paste( + "'LabelArray' resolution", k, "is not 2D")) + if (type(x) != "integer") msg <- c(msg, paste( + "'LabelArray' resolution", k, "is not of type integer")) + } + if (length(msg)) return(msg) else return(TRUE) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("LabelArray", .validateLabelArray) #' @importFrom methods is .validateSpatialData <- \(x) { @@ -64,17 +72,20 @@ msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) msg <- c(msg, .validatePointFrame(x)) msg <- c(msg, .validateTable(x)) - for (y in labels(x)) .validateZattrsLabelArray(y) - if (length(msg)) - return(msg) - return(TRUE) + for (y in labels(x)) { + ok <- .validateLabelArray(y) + if (!isTRUE(ok)) msg <- c(msg, ok) + ok <- .validateZattrsLabelArray(y) + if (!isTRUE(ok)) msg <- c(msg, ok) + } + if (length(msg)) return(msg) else return(TRUE) } #' @importFrom S4Vectors setValidity2 setValidity2("SpatialData", .validateSpatialData) .validateZattrs_multiscales <- \(x, msg) { - if (is.null(ms <- x$multiscales)) + if (is.null(ms <- x$multiscales[[1]])) msg <- c(msg, "missing 'multiscales'") # MUST contain for (. in c("axes", "datasets")) @@ -96,17 +107,17 @@ setValidity2("SpatialData", .validateSpatialData) .validateZattrs_coordTrans <- \(x, msg) { if (!is.list(ct <- x$coordinateTransformations)) msg <- c(msg, "missing or non-list 'coordTrans'") - ct <- ct[[1]] - for (. in c("input", "output", "type")) - if (is.null(ct[[.]])) - msg <- c(msg, sprintf("'coordTrans' missing '%s'", .)) + for (i in seq_along(ct)) + for (j in c("input", "output", "type")) + if (is.null(ct[[i]][[j]])) + msg <- c(msg, sprintf("'coordTrans' %s missing '%s'", i, j)) return(msg) } .validateZattrsLabelArray <- \(x) { msg <- c() za <- meta(x) msg <- .validateZattrs_multiscales(za, msg) - ms <- za$multiscales + ms <- za$multiscales[[1]] msg <- .validateZattrs_axes(ms, msg) msg <- .validateZattrs_coordTrans(ms, msg) if (length(msg)) return(msg) else return(TRUE) diff --git a/tests/testthat/test-labelarray.R b/tests/testthat/test-labelarray.R index a1c31547..e8f2b7f7 100644 --- a/tests/testthat/test-labelarray.R +++ b/tests/testthat/test-labelarray.R @@ -1,7 +1,5 @@ -arr <- seq_len(12) - test_that("LabelArray()", { - val <- sample(arr, 20*20, replace=TRUE) + val <- sample(seq_len(12), 20*20, replace=TRUE) mat <- array(val, dim=c(20, 20)) # invalid expect_error(LabelArray(mat)) @@ -12,14 +10,14 @@ test_that("LabelArray()", { expect_silent(LabelArray(list(mat))) expect_silent(LabelArray(list(mat), Zattrs())) # multiscale - dim <- lapply(c(20, 10, 5), \(.) c(3, rep(., 2))) - lys <- lapply(dim, \(.) array(sample(arr, prod(.), replace=TRUE), dim=.)) + dim <- lapply(c(20, 10, 5), \(.) rep(., 2)) + lys <- lapply(dim, \(.) array(sample(seq_len(12), prod(.), replace=TRUE), dim=.)) expect_silent(LabelArray(lys)) }) test_that("data(),LabelArray", { - dim <- lapply(c(8, 4, 2), \(.) c(3, rep(., 2))) - lys <- lapply(dim, \(.) array(0, dim=.)) + dim <- lapply(c(8, 4, 2), \(.) rep(., 2)) + lys <- lapply(dim, \(.) array(0L, dim=.)) lab <- LabelArray(lys) for (. in seq_along(lys)) expect_identical(data(lab, .), lys[[.]]) diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R new file mode 100644 index 00000000..c718ef5f --- /dev/null +++ b/tests/testthat/test-validity.R @@ -0,0 +1,36 @@ +library(SpatialData.plot) +zs <- file.path("extdata", "blobs.zarr") +zs <- system.file(zs, package="SpatialData") +sd <- readSpatialData(zs, tables=FALSE) + +test_that("validity,ImageArray", { + # all resolutions should be numbers + # (note: logical gets coerces to binary) + expect_error(ImageArray(list(v <- character(1)))) + x <- image(sd,1); x@data[[1]][1,1,1] <- v; expect_error(validObject(x)) + x <- image(sd,2); x@data[[2]][1,1,1] <- v; expect_error(validObject(x)) + # there should be two dimensions + expect_error(ImageArray(list(a <- array(numeric(1), c(1,1))))) + x <- image(sd,1); x@data[[1]] <- a; expect_error(validObject(x)) + x <- image(sd,2); x@data[[2]] <- a; expect_error(validObject(x)) +}) + +test_that("validity,LabelArray", { + # all resolutions should be of type integer + for (v in list(logical(1), character(1), numeric(1))) { + expect_error(LabelArray(list(v))) + x <- label(sd,1); x@data[[1]][1,1] <- v; expect_error(validObject(x)) + x <- label(sd,2); x@data[[2]][1,1] <- v; expect_error(validObject(x)) + } + # there should be two dimensions + expect_error(LabelArray(list(a <- array(integer(1), c(1,1,1))))) + x <- label(sd,1); x@data[[1]] <- a; expect_error(validObject(x)) + x <- label(sd,2); x@data[[2]] <- a; expect_error(validObject(x)) +}) + +test_that("validity,PointFrame", { + x <- point(sd,1) + expect_error(validObject(select(x, -x))) + expect_error(validObject(select(x, -y))) + expect_silent(validObject(select(x, -c(x, y))[0,])) +}) From 767fd75f2ce7f3572c4cf0aba33e92db4c23424e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:39:30 +0200 Subject: [PATCH 058/151] +validity,ShapeFrame --- R/validity.R | 29 +++++++++++++++++++---------- tests/testthat/test-validity.R | 12 ++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/R/validity.R b/R/validity.R index e2a3fdfa..23147843 100644 --- a/R/validity.R +++ b/R/validity.R @@ -17,16 +17,6 @@ return(msg) } -.validatePointFrame <- \(object) { - msg <- c() - if (!length(object)) return(msg) - if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") - if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") - return(msg) -} -#' @importFrom S4Vectors setValidity2 -setValidity2("PointFrame", .validatePointFrame) - .validateImageArray <- \(object) { msg <- c() res <- length(object) @@ -58,6 +48,25 @@ setValidity2("ImageArray", .validateImageArray) #' @importFrom S4Vectors setValidity2 setValidity2("LabelArray", .validateLabelArray) +.validatePointFrame <- \(object) { + msg <- c() + if (!length(object)) return(msg) + if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") + if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("PointFrame", .validatePointFrame) + +.validateShapeFrame <- \(object) { + msg <- c() + if (!nrow(object)) return(msg) + if (!"geometry" %in% names(object)) msg <- c(msg, "'ShapeFrame' missing 'geometry'.") + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("ShapeFrame", .validateShapeFrame) + #' @importFrom methods is .validateSpatialData <- \(x) { typ <- c( diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R index c718ef5f..8162b0ee 100644 --- a/tests/testthat/test-validity.R +++ b/tests/testthat/test-validity.R @@ -34,3 +34,15 @@ test_that("validity,PointFrame", { expect_error(validObject(select(x, -y))) expect_silent(validObject(select(x, -c(x, y))[0,])) }) + +test_that("validity,ShapeFrame", { + x <- shape(sd,1) + x@data <- select(data(x), -radius) + expect_silent(validObject(x)) + x <- shape(sd,1) + x@data <- filter(data(x), radius == Inf) + expect_silent(validObject(x)) + x <- shape(sd,1) + x@data <- select(data(x), -geometry) + expect_error(validObject(x)) +}) From 98108e89c2204a6d6d6454b6ba1f26c538fd58c5 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:39:59 +0200 Subject: [PATCH 059/151] require dplyr for testing --- tests/testthat/test-validity.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R index 8162b0ee..2d97f88d 100644 --- a/tests/testthat/test-validity.R +++ b/tests/testthat/test-validity.R @@ -1,4 +1,4 @@ -library(SpatialData.plot) +require(dplyr, quietly=TRUE) zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) From a573807f3d2d384a0a2870c6d3cbe4a28ac2567c Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:40:51 +0200 Subject: [PATCH 060/151] fix comment typo --- tests/testthat/test-validity.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R index 2d97f88d..675485f2 100644 --- a/tests/testthat/test-validity.R +++ b/tests/testthat/test-validity.R @@ -9,7 +9,7 @@ test_that("validity,ImageArray", { expect_error(ImageArray(list(v <- character(1)))) x <- image(sd,1); x@data[[1]][1,1,1] <- v; expect_error(validObject(x)) x <- image(sd,2); x@data[[2]][1,1,1] <- v; expect_error(validObject(x)) - # there should be two dimensions + # there should be three dimensions (channels + spatial) expect_error(ImageArray(list(a <- array(numeric(1), c(1,1))))) x <- image(sd,1); x@data[[1]] <- a; expect_error(validObject(x)) x <- image(sd,2); x@data[[2]] <- a; expect_error(validObject(x)) From 3f2058d86e2bbf9b7d240c65b510143df3a67e53 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:45:44 +0200 Subject: [PATCH 061/151] validity,SpatialData --- R/validity.R | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/R/validity.R b/R/validity.R index 23147843..15b36d52 100644 --- a/R/validity.R +++ b/R/validity.R @@ -27,7 +27,7 @@ if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( "'ImageArray' resolution", k, "is not of type double or integer")) } - if (length(msg)) return(msg) else return(TRUE) + return(msg) } #' @importFrom S4Vectors setValidity2 setValidity2("ImageArray", .validateImageArray) @@ -43,7 +43,7 @@ setValidity2("ImageArray", .validateImageArray) if (type(x) != "integer") msg <- c(msg, paste( "'LabelArray' resolution", k, "is not of type integer")) } - if (length(msg)) return(msg) else return(TRUE) + return(msg) } #' @importFrom S4Vectors setValidity2 setValidity2("LabelArray", .validateLabelArray) @@ -69,25 +69,26 @@ setValidity2("ShapeFrame", .validateShapeFrame) #' @importFrom methods is .validateSpatialData <- \(x) { + msg <- c() typ <- c( images="ImageArray", labels="LabelArray", points="PointFrame", shapes="ShapeFrame", tables="SingleCellExperiment") - msg <- NULL for (. in names(typ)) if (length(x[[.]])) if (!all(vapply(x[[.]], \(y) is(y, typ[.]), logical(1)))) msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) - msg <- c(msg, .validatePointFrame(x)) - msg <- c(msg, .validateTable(x)) + # TODO: validate .zattrs across all layers for (y in labels(x)) { - ok <- .validateLabelArray(y) - if (!isTRUE(ok)) msg <- c(msg, ok) - ok <- .validateZattrsLabelArray(y) - if (!isTRUE(ok)) msg <- c(msg, ok) + msg <- c(msg, .validateLabelArray(y)) + msg <- c(msg, .validateZattrsLabelArray(y)) } - if (length(msg)) return(msg) else return(TRUE) + for (y in images(x)) msg <- c(msg, .validateImageArray(y)) + for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) + for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) + msg <- c(msg, .validateTable(x)) + return(msg) } #' @importFrom S4Vectors setValidity2 @@ -129,5 +130,5 @@ setValidity2("SpatialData", .validateSpatialData) ms <- za$multiscales[[1]] msg <- .validateZattrs_axes(ms, msg) msg <- .validateZattrs_coordTrans(ms, msg) - if (length(msg)) return(msg) else return(TRUE) + return(msg) } From 31c8189301a74396d7060737fce094c741f65cd5 Mon Sep 17 00:00:00 2001 From: "Helena L. Crowell" Date: Mon, 6 Apr 2026 18:51:53 +0200 Subject: [PATCH 062/151] fix comment typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/testthat/test-validity.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R index 675485f2..e08c8393 100644 --- a/tests/testthat/test-validity.R +++ b/tests/testthat/test-validity.R @@ -5,7 +5,7 @@ sd <- readSpatialData(zs, tables=FALSE) test_that("validity,ImageArray", { # all resolutions should be numbers - # (note: logical gets coerces to binary) + # (note: logical gets coerced to binary) expect_error(ImageArray(list(v <- character(1)))) x <- image(sd,1); x@data[[1]][1,1,1] <- v; expect_error(validObject(x)) x <- image(sd,2); x@data[[2]][1,1,1] <- v; expect_error(validObject(x)) From 79af773f336f26f32613eb405688966e45e5abbe Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:53:02 +0200 Subject: [PATCH 063/151] track v0.99.26 changes --- inst/NEWS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inst/NEWS b/inst/NEWS index 48b32dea..4c35e07a 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,9 @@ +changes in version 0.99.26 + +- added unit tests for existing transformations +- implemented minimal layer-wise validity checks + (Image/LabelArray and Shape/PointFrame elements) + changes in version 0.99.25 - improved Zattrs show method (cf., PR #117) From da875c24c1dde99358bb4be4441bb2acbec3e450 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:53:15 +0200 Subject: [PATCH 064/151] v0.99.26 bump --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 85239e41..a3c52e55 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R Depends: R (>= 4.5) -Version: 0.99.25 +Version: 0.99.26 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufacturer data and writing to .zarr, on-disk representation of images/labels as From 84a431d57e5afeff557790d853cece7520d20cd6 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 18:58:16 +0200 Subject: [PATCH 065/151] fix show bug --- R/Zattrs.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/Zattrs.R b/R/Zattrs.R index 017dcb61..bff3360e 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -58,7 +58,7 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) cat(sprintf("- %s: (%s%s)\n", CTname(object)[i], CTtype(object)[i], - f(CTdata(object)[[i]][[CTtype(object)[i]]]))) + f(CTlist(object)[[i]][[CTtype(object)[i]]]))) ms <- object$multiscales[[1]] if (!is.null(ms)) { ds <- ms$datasets From 9e785cbdd64e2677bebd58033067445066264bfc Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 19:08:23 +0200 Subject: [PATCH 066/151] add todo note --- R/Zattrs.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/Zattrs.R b/R/Zattrs.R index bff3360e..2d69664c 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -44,6 +44,7 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) cat("- name:", vapply(ax, \(.) .$name, character(1)), "\n") cat("- type:", vapply(ax, \(.) .$type, character(1)), "\n") } + # TODO: more detailed 'sequence' display cat(sprintf("coordTrans(%d):\n", n <- length(CTname(object)))) g <- \(.) { . <- paste(unlist(.), collapse=",") From d9e6ff76fa408198eddc30ceec1ae37302e5b907 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 20:03:27 +0200 Subject: [PATCH 067/151] use sf_crop for query on shapes --- NAMESPACE | 1 + R/query.R | 79 +++++++++++++++---------------------- man/query.Rd | 14 +++---- tests/testthat/test-query.R | 10 ++--- 4 files changed, 43 insertions(+), 61 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index e0822d18..d4318fe4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -151,6 +151,7 @@ importFrom(reticulate,import) importFrom(sf,"st_geometry<-") importFrom(sf,st_as_sf) importFrom(sf,st_coordinates) +importFrom(sf,st_crop) importFrom(sf,st_distance) importFrom(sf,st_geometry) importFrom(sf,st_geometry_type) diff --git a/R/query.R b/R/query.R index d75183a6..8bbb0a88 100644 --- a/R/query.R +++ b/R/query.R @@ -8,21 +8,24 @@ #' @return same as input #' #' @examples -#' x <- file.path("extdata", "blobs.zarr") -#' x <- system.file(x, package="SpatialData") -#' x <- readSpatialData(x, tables=FALSE) +#' zs <- file.path("extdata", "blobs.zarr") +#' zs <- system.file(zs, package="SpatialData") +#' sd <- readSpatialData(zs, tables=FALSE) #' -#' image(x, "box") <- query(image(x), xmin=0, xmax=30, ymin=30, ymax=50) +#' image(sd, "box") <- query(image(sd), xmin=0, xmax=30, ymin=30, ymax=50) #' -#' image(x) -#' image(x, "box") +#' image(sd) +#' image(sd, "box") NULL +# TODO: query with polygonal boundary region + .check_bb <- \(args) { m <- match(names(args), c("xmin", "xmax", "ymin", "ymax")) if (any(is.na(m)) || !identical(sort(m), seq_len(4))) stop("currently only supporting bounding box query;", " please provide 'xmin/xmax/ymin/ymax' as ...") + stopifnot(length(args) == 4, is.numeric(unlist(args))) } #' @rdname query @@ -52,33 +55,15 @@ setMethod("query", "SpatialData", \(x, j=NULL, ...) { #' @rdname query #' @export -setMethod("query", "ImageArray", \(x, j, ...) { - qu <- list(...) - .check_bb(qu) - if (missing(j)) j <- 1 - if (is.numeric(j)) j <- CTname(x)[j] - stopifnot(length(j) == 1) - . <- grep(j, CTname(x)) - if (!length(.) || is.na(.)) stop("invalid 'j'") - # transform query into target space - ts <- CTpath(x, j) - xy <- list(c(qu$xmin, qu$xmax), c(qu$ymin, qu$ymax)) - xy <- data.frame(xy); names(xy) <- c("x", "y") - xy <- .trans_xy(xy, ts, TRUE) - xy <- lapply(xy, \(.) as.list(round(.))) - x <- x[, # crop (i.e., subset) array dimensions 2-3 - do.call(seq, xy[[2]]), - do.call(seq, xy[[1]])] - # transform array dimensions into target space - os <- data.frame(x=c(0, dim(x)[3]), y=c(0, dim(x)[2])) - os <- vapply(.trans_xy(os, ts), min, numeric(1)) - # add transformation of type translation as to - # offset origin by difference between new & old - os <- unlist(qu[c("xmin", "ymin")]) - os - if (!all(os == 0)) x <- addCT(x, - name=j, type="translation", - data=c(0, os[2], os[1])) - return(x) +setMethod("query", "ImageArray", \(x, ...) { + args <- list(...) + .check_bb(args) + d <- dim(x) + args$ymax <- min(args$ymax, d[2]) + args$xmax <- min(args$xmax, d[3]) + j <- seq(args$ymin, args$ymax) + k <- seq(args$xmin, args$xmax) + return(x[, j, k]) }) #' @rdname query @@ -87,28 +72,26 @@ setMethod("query", "LabelArray", \(x, ...) { args <- list(...) .check_bb(args) d <- dim(x) - if (args$ymax > d[1]) args$ymax <- d[1] - if (args$xmax > d[2]) args$xmax <- d[2] - a <- data(x)[ - seq(args$ymin, args$ymax), - seq(args$xmin, args$xmax)] - x@data <- a - return(x) + args$ymax <- min(args$ymax, d[1]) + args$xmax <- min(args$xmax, d[2]) + i <- seq(args$ymin, args$ymax) + j <- seq(args$xmin, args$xmax) + return(x[i, j]) }) #' @rdname query +#' @importFrom sf st_as_sf st_crop #' @export setMethod("query", "ShapeFrame", \(x, ...) { + # TODO: this will drop geometries where any coordinate + # is out of bounds; keep but crop to boundary region? args <- list(...) .check_bb(args) - df <- st_as_sf(data(x)) - xy <- st_coordinates(df) - i <- - xy[, 1] >= args$xmin & - xy[, 1] <= args$xmax & - xy[, 2] >= args$ymin & - xy[, 2] <= args$ymax - x@data <- data(x)[which(i), ] + sf <- st_as_sf(data(x)) + bb <- st_bbox(unlist(args)) + # note: non-spatial attributes (e.g., radius) gives warnings? + suppressWarnings(sf <- st_crop(sf, bb)) + x@data <- sf[names(x)] return(x) }) diff --git a/man/query.Rd b/man/query.Rd index 66b8b488..bc799592 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -11,7 +11,7 @@ \usage{ \S4method{query}{SpatialData}(x, j = NULL, ...) -\S4method{query}{ImageArray}(x, j, ...) +\S4method{query}{ImageArray}(x, ...) \S4method{query}{LabelArray}(x, ...) @@ -33,12 +33,12 @@ same as input spatial queries } \examples{ -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, tables=FALSE) +zs <- file.path("extdata", "blobs.zarr") +zs <- system.file(zs, package="SpatialData") +sd <- readSpatialData(zs, tables=FALSE) -image(x, "box") <- query(image(x), xmin=0, xmax=30, ymin=30, ymax=50) +image(sd, "box") <- query(image(sd), xmin=0, xmax=30, ymin=30, ymax=50) -image(x) -image(x, "box") +image(sd) +image(sd, "box") } diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index ad46ca48..bdee8e41 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -1,4 +1,4 @@ -suppressPackageStartupMessages(library(sf)) +require(sf, quietly=TRUE) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) @@ -28,8 +28,6 @@ test_that("query,ImageArray", { # crop and shift j <- query(i, xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) expect_equal(dim(j), c(3, 1+h-2, 1+w-1)) - expect_equal(CTtype(j), t <- "translation") - expect_equivalent(CTlist(j)[[1]][[t]][[1]], c(0, 2, 1)) }) test_that("query,PointFrame", { @@ -59,10 +57,10 @@ test_that("query,ShapeFrame", { n <- length(s <- shape(x)) # mock query without any effect t <- query(s, xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) - expect_equal(collect(data(s)), collect(data(t))) + expect_equal(nrow(data(t)), nrow(data(s))) # this should drop everything - t <- query(s, xmin=Inf, xmax=-Inf, ymin=Inf, ymax=-Inf) - expect_equal(nrow(collect(data(t))), 0) + t <- query(s, xmin=0, xmax=1e-3, ymin=0, ymax=1e-3) + expect_equal(nrow(t), 0) # proper query xy <- st_coordinates(st_as_sf(data(s))) xy <- data.frame(xy); names(xy) <- c("x", "y") From 9a07c576642c7e0da9d632874010dfdd9cc96c00 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 20:06:02 +0200 Subject: [PATCH 068/151] import sf::st_bbox --- NAMESPACE | 1 + R/query.R | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index d4318fe4..ddf360b1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -150,6 +150,7 @@ importFrom(methods,setReplaceMethod) importFrom(reticulate,import) importFrom(sf,"st_geometry<-") importFrom(sf,st_as_sf) +importFrom(sf,st_bbox) importFrom(sf,st_coordinates) importFrom(sf,st_crop) importFrom(sf,st_distance) diff --git a/R/query.R b/R/query.R index 8bbb0a88..ea4bf4e7 100644 --- a/R/query.R +++ b/R/query.R @@ -80,7 +80,7 @@ setMethod("query", "LabelArray", \(x, ...) { }) #' @rdname query -#' @importFrom sf st_as_sf st_crop +#' @importFrom sf st_as_sf st_bbox st_crop #' @export setMethod("query", "ShapeFrame", \(x, ...) { # TODO: this will drop geometries where any coordinate From a8670050dbcb3b8547cb67e47eab4228cef952d4 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 20:06:23 +0200 Subject: [PATCH 069/151] fix comm typo --- R/query.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/query.R b/R/query.R index ea4bf4e7..64b38f67 100644 --- a/R/query.R +++ b/R/query.R @@ -89,7 +89,7 @@ setMethod("query", "ShapeFrame", \(x, ...) { .check_bb(args) sf <- st_as_sf(data(x)) bb <- st_bbox(unlist(args)) - # note: non-spatial attributes (e.g., radius) gives warnings? + # note: non-spatial attributes (e.g., radius) give warnings? suppressWarnings(sf <- st_crop(sf, bb)) x@data <- sf[names(x)] return(x) From 27a1b5c8f8a0f28abd0ac8351b6694d88a2a13bd Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 22:24:37 +0200 Subject: [PATCH 070/151] +polygon query for point/shape --- NAMESPACE | 2 ++ R/query.R | 54 ++++++++++++++++++++++++++++++++++++++++------------ man/query.Rd | 2 +- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index ddf360b1..1699d4d5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -156,7 +156,9 @@ importFrom(sf,st_crop) importFrom(sf,st_distance) importFrom(sf,st_geometry) importFrom(sf,st_geometry_type) +importFrom(sf,st_intersects) importFrom(sf,st_point) +importFrom(sf,st_polygon) importFrom(sf,st_sfc) importFrom(utils,.DollarNames) importFrom(utils,head) diff --git a/R/query.R b/R/query.R index 64b38f67..ce331df8 100644 --- a/R/query.R +++ b/R/query.R @@ -20,20 +20,34 @@ NULL # TODO: query with polygonal boundary region -.check_bb <- \(args) { +.check_box <- \(args) { m <- match(names(args), c("xmin", "xmax", "ymin", "ymax")) if (any(is.na(m)) || !identical(sort(m), seq_len(4))) stop("currently only supporting bounding box query;", " please provide 'xmin/xmax/ymin/ymax' as ...") stopifnot(length(args) == 4, is.numeric(unlist(args))) } +.check_pol <- \(mx) { + ok <- c( + is.matrix(mx), is.numeric(mx), + nrow(mx) >= 3, ncol(mx) == 2) + if (!all(ok)) stop( + "Invalid polygon query; should be a numeric matrix ", + "with ≥ 3 rows and 2 columns (= xy-coordinates)") + # ensure polygon is closed + top <- mx[1, ] + bot <- mx[nrow(mx), ] + if (!all(top == bot)) + mx <- rbind(mx, top) + return(mx) +} #' @rdname query #' @export setMethod("query", "SpatialData", \(x, j=NULL, ...) { # check validity of dots args <- list(...) - .check_bb(args) + .check_box(args) # guess coordinate space stopifnot(length(j) == 1) j <- if (is.null(j)) { @@ -57,7 +71,7 @@ setMethod("query", "SpatialData", \(x, j=NULL, ...) { #' @export setMethod("query", "ImageArray", \(x, ...) { args <- list(...) - .check_bb(args) + .check_box(args) d <- dim(x) args$ymax <- min(args$ymax, d[2]) args$xmax <- min(args$xmax, d[3]) @@ -70,7 +84,7 @@ setMethod("query", "ImageArray", \(x, ...) { #' @export setMethod("query", "LabelArray", \(x, ...) { args <- list(...) - .check_bb(args) + .check_box(args) d <- dim(x) args$ymax <- min(args$ymax, d[1]) args$xmax <- min(args$xmax, d[2]) @@ -80,13 +94,20 @@ setMethod("query", "LabelArray", \(x, ...) { }) #' @rdname query -#' @importFrom sf st_as_sf st_bbox st_crop +#' @importFrom sf st_as_sf st_intersects st_polygon st_bbox st_crop #' @export setMethod("query", "ShapeFrame", \(x, ...) { # TODO: this will drop geometries where any coordinate # is out of bounds; keep but crop to boundary region? args <- list(...) - .check_bb(args) + if (length(args) == 1) { + mx <- .check_pol(mx <- args[[1]]) + sf <- st_as_sf(data(x)) + ok <- st_intersects(sf, st_polygon(list(mx)), sparse=FALSE) + x@data <- x@data[which(ok), ] + return(x) + } + .check_box(args) sf <- st_as_sf(data(x)) bb <- st_bbox(unlist(args)) # note: non-spatial attributes (e.g., radius) give warnings? @@ -96,13 +117,22 @@ setMethod("query", "ShapeFrame", \(x, ...) { }) #' @rdname query +#' @importFrom sf st_as_sf st_polygon st_intersects +#' @importFrom dplyr collect #' @export -setMethod("query", "PointFrame", \(x, j, ...) { +setMethod("query", "PointFrame", \(x, ...) { args <- list(...) - .check_bb(args) - y <- filter(x, - x >= args$xmin, x <= args$xmax, - y >= args$ymin, y <= args$ymax) - x@data <- y@data + if (length(args) == 1) { + mx <- .check_pol(mx <- args[[1]]) + xy <- st_as_sf(collect(data(x)[c("x", "y")]), coords=c("x", "y")) + ok <- st_intersects(xy, st_polygon(list(mx)), sparse=FALSE) + x@data <- x@data[which(ok), ] + } else { + .check_box(args) + y <- filter(x, + x >= args$xmin, x <= args$xmax, + y >= args$ymin, y <= args$ymax) + x@data <- y@data + } return(x) }) diff --git a/man/query.Rd b/man/query.Rd index bc799592..81a4fea2 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -17,7 +17,7 @@ \S4method{query}{ShapeFrame}(x, ...) -\S4method{query}{PointFrame}(x, j, ...) +\S4method{query}{PointFrame}(x, ...) } \arguments{ \item{x}{\code{SpatialData} element.} From 699897df15c066783eeb67610b3e66ccc4b18984 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 22:41:25 +0200 Subject: [PATCH 071/151] tests for poly query --- R/query.R | 4 +++- tests/testthat/test-query.R | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/R/query.R b/R/query.R index ce331df8..3e85ee38 100644 --- a/R/query.R +++ b/R/query.R @@ -101,6 +101,8 @@ setMethod("query", "ShapeFrame", \(x, ...) { # is out of bounds; keep but crop to boundary region? args <- list(...) if (length(args) == 1) { + # TODO: currently ignoring 'radius' for circles (i.e., + # query based on centroids only); what does Python do? mx <- .check_pol(mx <- args[[1]]) sf <- st_as_sf(data(x)) ok <- st_intersects(sf, st_polygon(list(mx)), sparse=FALSE) @@ -118,7 +120,7 @@ setMethod("query", "ShapeFrame", \(x, ...) { #' @rdname query #' @importFrom sf st_as_sf st_polygon st_intersects -#' @importFrom dplyr collect +#' @importFrom dplyr collect filter #' @export setMethod("query", "PointFrame", \(x, ...) { args <- list(...) diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index bdee8e41..7a3e63b1 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -30,7 +30,7 @@ test_that("query,ImageArray", { expect_equal(dim(j), c(3, 1+h-2, 1+w-1)) }) -test_that("query,PointFrame", { +test_that("query-box,PointFrame", { n <- length(p <- point(x)) # this shouldn't do anything q <- query(p, xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) @@ -53,7 +53,23 @@ test_that("query,PointFrame", { expect_identical(df[i, ], fd) }) -test_that("query,ShapeFrame", { +test_that("query-pol,PointFrame", { + n <- length(p <- point(x)) + # sample random points & + # query tiny polygon around them + replicate(5, { + i <- sample(n, 1) + xy <- c(p[i]$x, p[i]$y) + xy <- rbind( + xy+c(0, d <- 1e-6), + xy+c(-d,-d), xy+c(+d,-d)) + q <- query(p, xy) + expect_length(q, 1) + expect_equal(q, p[i]) + }) +}) + +test_that("query-box,ShapeFrame", { n <- length(s <- shape(x)) # mock query without any effect t <- query(s, xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) @@ -71,3 +87,20 @@ test_that("query,ShapeFrame", { t <- do.call(query, c(list(x=s), bb)) expect_equal(s[i], t) }) + +test_that("query-pol,ShapeFrame", { + n <- length(s <- shape(x)) + xy <- st_coordinates(st_as_sf(data(s))) + # sample random shapes & + # query tiny polygon around them + replicate(5, { + i <- sample(n, 1) + xy <- xy[i, ] + xy <- rbind( + xy+c(0, d <- 1e-6), + xy+c(-d,-d), xy+c(+d,-d)) + t <- query(s, xy) + expect_length(t, 1) + expect_equal(t, s[i]) + }) +}) From 557878865cb54029215a1f200513271739de1bd5 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 22:59:22 +0200 Subject: [PATCH 072/151] cleaner box/pol validity --- R/query.R | 19 +++++++++++-------- tests/testthat/test-query.R | 8 +++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/R/query.R b/R/query.R index 3e85ee38..ae296251 100644 --- a/R/query.R +++ b/R/query.R @@ -20,19 +20,22 @@ NULL # TODO: query with polygonal boundary region -.check_box <- \(args) { - m <- match(names(args), c("xmin", "xmax", "ymin", "ymax")) - if (any(is.na(m)) || !identical(sort(m), seq_len(4))) - stop("currently only supporting bounding box query;", - " please provide 'xmin/xmax/ymin/ymax' as ...") - stopifnot(length(args) == 4, is.numeric(unlist(args))) +.check_box <- \(bb) { + xy <- c("xmin", "xmax", "ymin", "ymax") + ok <- c( + length(bb) == 4, setequal(names(bb), xy), + is.numeric(bb <- unlist(bb)), !is.na(bb)) + if (!all(ok)) stop( + "Invalid bounding box query; should be length-4 ", + "numeric vector with names 'xmin/xmax/ymin/ymax'") } + .check_pol <- \(mx) { ok <- c( is.matrix(mx), is.numeric(mx), nrow(mx) >= 3, ncol(mx) == 2) if (!all(ok)) stop( - "Invalid polygon query; should be a numeric matrix ", + "Invalid polygon query; should be numeric matrix ", "with ≥ 3 rows and 2 columns (= xy-coordinates)") # ensure polygon is closed top <- mx[1, ] @@ -109,10 +112,10 @@ setMethod("query", "ShapeFrame", \(x, ...) { x@data <- x@data[which(ok), ] return(x) } + # note: non-spatial attributes (e.g., radius) give warnings? .check_box(args) sf <- st_as_sf(data(x)) bb <- st_bbox(unlist(args)) - # note: non-spatial attributes (e.g., radius) give warnings? suppressWarnings(sf <- st_crop(sf, bb)) x@data <- sf[names(x)] return(x) diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 7a3e63b1..0e27dede 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -55,6 +55,9 @@ test_that("query-box,PointFrame", { test_that("query-pol,PointFrame", { n <- length(p <- point(x)) + # mock all-inclusive query + xy <- rbind(c(0,0), c(0,1e6), c(1e6,0)) + expect_equal(query(p, xy), p) # sample random points & # query tiny polygon around them replicate(5, { @@ -90,9 +93,12 @@ test_that("query-box,ShapeFrame", { test_that("query-pol,ShapeFrame", { n <- length(s <- shape(x)) - xy <- st_coordinates(st_as_sf(data(s))) + # mock all-inclusive query + xy <- rbind(c(0,0), c(0,1e6), c(1e6,0)) + expect_equal(query(s, xy), s) # sample random shapes & # query tiny polygon around them + xy <- st_coordinates(st_as_sf(data(s))) replicate(5, { i <- sample(n, 1) xy <- xy[i, ] From ba6f8e028035eff257279ccb76e1b9ae47656907 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 23:13:18 +0200 Subject: [PATCH 073/151] bug fix test edge case --- tests/testthat/test-query.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 0e27dede..191c3ca5 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -63,12 +63,13 @@ test_that("query-pol,PointFrame", { replicate(5, { i <- sample(n, 1) xy <- c(p[i]$x, p[i]$y) + i <- p$x == xy[1] & p$y == xy[2] xy <- rbind( xy+c(0, d <- 1e-6), xy+c(-d,-d), xy+c(+d,-d)) q <- query(p, xy) - expect_length(q, 1) - expect_equal(q, p[i]) + expect_length(q, sum(i)) + expect_equal(q, p[which(i)]) }) }) From 9c083b91a2daf0ff9d95c56e09923b8ca506a309 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Mon, 6 Apr 2026 23:20:47 +0200 Subject: [PATCH 074/151] make copi happier --- R/query.R | 4 ++-- tests/testthat/test-query.R | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/R/query.R b/R/query.R index ae296251..3e4bdec1 100644 --- a/R/query.R +++ b/R/query.R @@ -131,13 +131,13 @@ setMethod("query", "PointFrame", \(x, ...) { mx <- .check_pol(mx <- args[[1]]) xy <- st_as_sf(collect(data(x)[c("x", "y")]), coords=c("x", "y")) ok <- st_intersects(xy, st_polygon(list(mx)), sparse=FALSE) - x@data <- x@data[which(ok), ] + return(x[which(ok[, 1])]) } else { .check_box(args) y <- filter(x, x >= args$xmin, x <= args$xmax, y >= args$ymin, y <= args$ymax) x@data <- y@data + return(x) } - return(x) }) diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 191c3ca5..7c3032b1 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -55,9 +55,10 @@ test_that("query-box,PointFrame", { test_that("query-pol,PointFrame", { n <- length(p <- point(x)) + f <- \(.) collect(data(.)) # mock all-inclusive query xy <- rbind(c(0,0), c(0,1e6), c(1e6,0)) - expect_equal(query(p, xy), p) + expect_identical(f(query(p, xy)), f(p)) # sample random points & # query tiny polygon around them replicate(5, { @@ -69,7 +70,7 @@ test_that("query-pol,PointFrame", { xy+c(-d,-d), xy+c(+d,-d)) q <- query(p, xy) expect_length(q, sum(i)) - expect_equal(q, p[which(i)]) + expect_identical(f(q), f(p[which(i)])) }) }) From 374a0664ddc106c27970b337e6e37bcb293334a3 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Tue, 7 Apr 2026 18:23:22 +0200 Subject: [PATCH 075/151] more tests; more validity; documentation --- R/query.R | 93 +++++++++++++++++++++---------------- man/query.Rd | 36 +++++++++----- tests/testthat/test-query.R | 82 +++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 74 deletions(-) diff --git a/R/query.R b/R/query.R index 3e4bdec1..f55faaca 100644 --- a/R/query.R +++ b/R/query.R @@ -1,9 +1,15 @@ #' @name query #' @title spatial queries #' +#' @description Spatial queries serve to subset \code{SpatialData} elements +#' according to a rectangular bounding box or arbitrary polygonal shapes. +#' Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for +#' spatial operations, so that points on the boundary are included as well. +#' #' @param x \code{SpatialData} element. -#' @param j scalar character or integer; index or name of coordinate space. -#' @param ... optional arguments passed to and from other methods. +#' @param y query specification; +#' bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' +#' (order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns. #' #' @return same as input #' @@ -12,28 +18,39 @@ #' zs <- system.file(zs, package="SpatialData") #' sd <- readSpatialData(zs, tables=FALSE) #' -#' image(sd, "box") <- query(image(sd), xmin=0, xmax=30, ymin=30, ymax=50) +#' # bounding box +#' y <- list(xmin=11, xmax=44, ymin=22, ymax=55) +#' q <- query(p <- point(sd), y) +#' +#' plot(data.frame(data(p))[c("x", "y")], asp=1) +#' points(data.frame(data(q))[c("x", "y")], col="red") +#' rect(y$xmin, y$ymin, y$xmax, y$ymax, border="blue") #' -#' image(sd) -#' image(sd, "box") +#' # polygon +#' y <- rbind(c(20,10), c(50,30), c(20,50), c(30,30)) +#' q <- query(p <- point(sd), y) +#' +#' plot(data.frame(data(p))[c("x", "y")], asp=1) +#' points(data.frame(data(q))[c("x", "y")], col="red") +#' lines(rbind(y, y[1, ]), col="blue") NULL -# TODO: query with polygonal boundary region - .check_box <- \(bb) { xy <- c("xmin", "xmax", "ymin", "ymax") - ok <- c( + ok <- c(is.list(bb), length(bb) == 4, setequal(names(bb), xy), + bb$xmin <= bb$xmax, bb$ymin <= bb$ymax, is.numeric(bb <- unlist(bb)), !is.na(bb)) if (!all(ok)) stop( "Invalid bounding box query; should be length-4 ", - "numeric vector with names 'xmin/xmax/ymin/ymax'") + "numeric list with names 'xmin/xmax/ymin/ymax'") } .check_pol <- \(mx) { ok <- c( is.matrix(mx), is.numeric(mx), - nrow(mx) >= 3, ncol(mx) == 2) + nrow(mx) >= 3, ncol(mx) == 2, + !is.na(mx), is.finite(mx)) if (!all(ok)) stop( "Invalid polygon query; should be numeric matrix ", "with ≥ 3 rows and 2 columns (= xy-coordinates)") @@ -42,6 +59,8 @@ NULL bot <- mx[nrow(mx), ] if (!all(top == bot)) mx <- rbind(mx, top) + dup <- any(duplicated(mx[-1, ])) + if (dup) stop("Invalid polygon query; found duplicated vertices") return(mx) } @@ -72,50 +91,47 @@ setMethod("query", "SpatialData", \(x, j=NULL, ...) { #' @rdname query #' @export -setMethod("query", "ImageArray", \(x, ...) { - args <- list(...) - .check_box(args) +setMethod("query", "ImageArray", \(x, y) { + .check_box(y) d <- dim(x) - args$ymax <- min(args$ymax, d[2]) - args$xmax <- min(args$xmax, d[3]) - j <- seq(args$ymin, args$ymax) - k <- seq(args$xmin, args$xmax) + y$ymax <- min(y$ymax, d[2]) + y$xmax <- min(y$xmax, d[3]) + j <- seq(y$ymin, y$ymax) + k <- seq(y$xmin, y$xmax) return(x[, j, k]) }) #' @rdname query #' @export -setMethod("query", "LabelArray", \(x, ...) { - args <- list(...) - .check_box(args) +setMethod("query", "LabelArray", \(x, y) { + .check_box(y) d <- dim(x) - args$ymax <- min(args$ymax, d[1]) - args$xmax <- min(args$xmax, d[2]) - i <- seq(args$ymin, args$ymax) - j <- seq(args$xmin, args$xmax) + y$ymax <- min(y$ymax, d[1]) + y$xmax <- min(y$xmax, d[2]) + i <- seq(y$ymin, y$ymax) + j <- seq(y$xmin, y$xmax) return(x[i, j]) }) #' @rdname query #' @importFrom sf st_as_sf st_intersects st_polygon st_bbox st_crop #' @export -setMethod("query", "ShapeFrame", \(x, ...) { +setMethod("query", "ShapeFrame", \(x, y) { # TODO: this will drop geometries where any coordinate # is out of bounds; keep but crop to boundary region? - args <- list(...) - if (length(args) == 1) { + if (is.matrix(y)) { # TODO: currently ignoring 'radius' for circles (i.e., # query based on centroids only); what does Python do? - mx <- .check_pol(mx <- args[[1]]) + mx <- .check_pol(y) sf <- st_as_sf(data(x)) ok <- st_intersects(sf, st_polygon(list(mx)), sparse=FALSE) x@data <- x@data[which(ok), ] return(x) } # note: non-spatial attributes (e.g., radius) give warnings? - .check_box(args) + .check_box(y) sf <- st_as_sf(data(x)) - bb <- st_bbox(unlist(args)) + bb <- st_bbox(unlist(y)) suppressWarnings(sf <- st_crop(sf, bb)) x@data <- sf[names(x)] return(x) @@ -125,19 +141,16 @@ setMethod("query", "ShapeFrame", \(x, ...) { #' @importFrom sf st_as_sf st_polygon st_intersects #' @importFrom dplyr collect filter #' @export -setMethod("query", "PointFrame", \(x, ...) { - args <- list(...) - if (length(args) == 1) { - mx <- .check_pol(mx <- args[[1]]) +setMethod("query", "PointFrame", \(x, y) { + if (is.matrix(y)) { + mx <- .check_pol(y) xy <- st_as_sf(collect(data(x)[c("x", "y")]), coords=c("x", "y")) ok <- st_intersects(xy, st_polygon(list(mx)), sparse=FALSE) return(x[which(ok[, 1])]) } else { - .check_box(args) - y <- filter(x, - x >= args$xmin, x <= args$xmax, - y >= args$ymin, y <= args$ymax) - x@data <- y@data - return(x) + .check_box(bb <- y) + filter(x, + x >= bb$xmin, x <= bb$xmax, + y >= bb$ymin, y <= bb$ymax) } }) diff --git a/man/query.Rd b/man/query.Rd index 81a4fea2..1931623c 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -11,34 +11,48 @@ \usage{ \S4method{query}{SpatialData}(x, j = NULL, ...) -\S4method{query}{ImageArray}(x, ...) +\S4method{query}{ImageArray}(x, y) -\S4method{query}{LabelArray}(x, ...) +\S4method{query}{LabelArray}(x, y) -\S4method{query}{ShapeFrame}(x, ...) +\S4method{query}{ShapeFrame}(x, y) -\S4method{query}{PointFrame}(x, ...) +\S4method{query}{PointFrame}(x, y) } \arguments{ \item{x}{\code{SpatialData} element.} -\item{j}{scalar character or integer; index or name of coordinate space.} - -\item{...}{optional arguments passed to and from other methods.} +\item{y}{query specification; +bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' +(order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns.} } \value{ same as input } \description{ -spatial queries +Spatial queries serve to subset \code{SpatialData} elements +according to a rectangular bounding box or arbitrary polygonal shapes. +Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for +spatial operations, so that points on the boundary are included as well. } \examples{ zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) -image(sd, "box") <- query(image(sd), xmin=0, xmax=30, ymin=30, ymax=50) +# bounding box +y <- list(xmin=11, xmax=44, ymin=22, ymax=55) +q <- query(p <- point(sd), y) + +plot(data.frame(data(p))[c("x", "y")], asp=1) +points(data.frame(data(q))[c("x", "y")], col="red") +rect(y$xmin, y$ymin, y$xmax, y$ymax, border="blue") + +# polygon +y <- rbind(c(20,10), c(50,30), c(20,50), c(30,30)) +q <- query(p <- point(sd), y) -image(sd) -image(sd, "box") +plot(data.frame(data(p))[c("x", "y")], asp=1) +points(data.frame(data(q))[c("x", "y")], col="red") +lines(rbind(y, y[1, ]), col="blue") } diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 7c3032b1..5b2ae3f5 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -3,41 +3,76 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) -test_that("query,...", { - # extract 1st element from every layer - lys <- lapply(seq_len(4), \(.) x[.,1][[.]][[1]]) - for (y in lys) { - # missing bounding box coordinates - expect_error(query(y, xmin=0, xmax=1, ymin=0)) - # # invalid coordinate space - # expect_error(query(y, ".", xmin=0, xmax=1, ymin=0, ymax=1)) - # expect_error(query(y, 100, xmin=0, xmax=1, ymin=0, ymax=1)) - } +test_that("query,.check_box", { + # valid + q <- list( + list(xmin=0, xmax=1, ymin=0, ymax=1), + list(xmin=-1, xmax=0, ymin=-1, ymax=0), + list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf)) + for (. in q) expect_silent(.check_box(.)) + # invalid + q <- list( + list(xmin=0, xmax=1, ymin=0), + list(xmin=1, xmax=0, ymin=1, ymax=0), + list(xmin=0, xmax=-1, ymin=0, ymax=-1), + list(xmin=0, xmax=1, ymin=10, ymax=NA), + list(xmin=Inf, xmax=-Inf, ymin=Inf, ymax=-Inf)) + for (. in q) expect_error(.check_box(.)) }) +test_that("query,.check_pol", { + # valid + q <- list( + m <- matrix(seq_len(8), 4, 2), + rbind(c(1,1), c(2,2), c(3,3)), # open + rbind(c(1,1), c(2,2), c(3,3), c(1,1))) + for (. in q) expect_silent(.check_pol(.)) + # invalid + q <- list( + `[<-`(m, i=1, j=1, value=NA), # missing value + `[<-`(m, i=1, j=1, value=Inf), # not finite + matrix(numeric(6), 3, 2), # duplicates + matrix(seq_len(6), 2, 3)) # wrong dim. + for (. in q) expect_error(.check_pol(.)) +}) + +# test_that("query,...", { +# # extract 1st element from every layer +# lys <- lapply(seq_len(4), \(.) x[.,1][[.]][[1]]) +# for (y in lys) { +# # missing bounding box coordinates +# expect_error(query(y, xmin=0, xmax=1, ymin=0)) +# # # invalid coordinate space +# # expect_error(query(y, ".", xmin=0, xmax=1, ymin=0, ymax=1)) +# # expect_error(query(y, 100, xmin=0, xmax=1, ymin=0, ymax=1)) +# } +# }) + test_that("query,ImageArray", { d <- dim(i <- image(x)) # neither crop nor shift - expect_identical(query(i, xmin=0, xmax=d[3], ymin=0, ymax=d[2]), i) + y <- list(xmin=0, xmax=d[3], ymin=0, ymax=d[2]) + expect_identical(query(i, y), i) # order is irrelevant - expect_identical(query(i, ymax=d[2], xmax=d[3], xmin=0, ymin=0), i) + y <- list(ymax=d[2], xmax=d[3], xmin=0, ymin=0) + expect_identical(query(i, y), i) # crop but don't shift - j <- query(i, xmin=0, xmax=w <- d[3]/2, ymin=0, ymax=h <- d[2]/4) - expect_equal(dim(j), c(3, h, w)) + y <- list(xmin=0, xmax=w <- d[3]/2, ymin=0, ymax=h <- d[2]/4) + expect_equal(dim(j <- query(i, y)), c(3, h, w)) expect_identical(CTlist(i), CTlist(j)) # crop and shift - j <- query(i, xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) - expect_equal(dim(j), c(3, 1+h-2, 1+w-1)) + y <- list(xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) + expect_equal(dim(query(i, y)), c(3, 1+h-2, 1+w-1)) }) test_that("query-box,PointFrame", { n <- length(p <- point(x)) # this shouldn't do anything - q <- query(p, xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) + q <- query(p, list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf)) expect_is(data(q), "arrow_dplyr_query") expect_identical(collect(data(p)), collect(data(q))) # this should drop everything - q <- query(p, xmin=Inf, xmax=-Inf, ymin=Inf, ymax=-Inf) + q <- query(p, list(xmin=0, xmax=1e-3, ymin=0, ymax=1e-3)) expect_equal(nrow(collect(data(q))), 0) # proper query bb <- lapply(c("x", "y"), \(.) { @@ -45,10 +80,11 @@ test_that("query-box,PointFrame", { d <- c((d <- diff(range(v)))/4, d/2) names(d) <- paste0(., c("min", "max")) as.list(d) }) |> Reduce(f=c) - q <- do.call(query, c(list(x=p), bb)) + q <- do.call(query, c(list(x=p), list(bb))) df <- collect(data(p)) fd <- collect(data(q)) - i <- df$x >= bb$xmin & df$x <= bb$xmax & + i <- + df$x >= bb$xmin & df$x <= bb$xmax & df$y >= bb$ymin & df$y <= bb$ymax expect_identical(df[i, ], fd) }) @@ -77,10 +113,10 @@ test_that("query-pol,PointFrame", { test_that("query-box,ShapeFrame", { n <- length(s <- shape(x)) # mock query without any effect - t <- query(s, xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) + t <- query(s, list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf)) expect_equal(nrow(data(t)), nrow(data(s))) # this should drop everything - t <- query(s, xmin=0, xmax=1e-3, ymin=0, ymax=1e-3) + t <- query(s, list(xmin=0, xmax=1e-3, ymin=0, ymax=1e-3)) expect_equal(nrow(t), 0) # proper query xy <- st_coordinates(st_as_sf(data(s))) @@ -89,7 +125,7 @@ test_that("query-box,ShapeFrame", { bb <- lapply(xy, \(.) c(.-1e-9, .+1e-9)) bb <- data.frame(t(unlist(bb))) names(bb) <- c("xmin", "xmax", "ymin", "ymax") - t <- do.call(query, c(list(x=s), bb)) + t <- do.call(query, c(list(x=s), list(bb))) expect_equal(s[i], t) }) From 29f51a31e6dcfced4f2bbb177e08528ccc66d05a Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Tue, 7 Apr 2026 18:48:40 +0200 Subject: [PATCH 076/151] more docs --- R/query.R | 16 ++++++++++++++++ man/query.Rd | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/R/query.R b/R/query.R index f55faaca..daa768ba 100644 --- a/R/query.R +++ b/R/query.R @@ -5,6 +5,9 @@ #' according to a rectangular bounding box or arbitrary polygonal shapes. #' Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for #' spatial operations, so that points on the boundary are included as well. +#' Note: shape queries ignore non-spatial attributes (e.g., radius for circles) +#' such that a circle is included if its centroid intersects the query region; +#' similarly, a polygon is included if any vertex intersects the query region. #' #' @param x \code{SpatialData} element. #' @param y query specification; @@ -33,6 +36,19 @@ #' plot(data.frame(data(p))[c("x", "y")], asp=1) #' points(data.frame(data(q))[c("x", "y")], col="red") #' lines(rbind(y, y[1, ]), col="blue") +#' +#' # shapes that intersect the query region are kept +#' y <- rbind(c(30,45), c(40,45), c(35,50)) +#' t <- query(s <- shape(sd, 3), y) +#' +#' require(sf, quietly=TRUE) +#' df <- st_coordinates(st_as_sf(data(s))) +#' fd <- st_coordinates(st_as_sf(data(t))) +#' plot( +#' asp=1, xlim=c(15, 60), ylim=c(15, 60), +#' rbind(pol, pol[1, ]), type="l", col="blue") +#' foo <- by(df, df[, "L2"], \(x) points(x, type="b", col="black")) +#' foo <- by(fd, fd[, "L2"], \(x) points(x, type="b", col="red")) NULL .check_box <- \(bb) { diff --git a/man/query.Rd b/man/query.Rd index 1931623c..2076a792 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -34,6 +34,9 @@ Spatial queries serve to subset \code{SpatialData} elements according to a rectangular bounding box or arbitrary polygonal shapes. Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for spatial operations, so that points on the boundary are included as well. +Note: shape queries ignore non-spatial attributes (e.g., radius for circles) +such that a circle is included if its centroid intersects the query region; +similarly, a polygon is included if any vertex intersects the query region. } \examples{ zs <- file.path("extdata", "blobs.zarr") @@ -55,4 +58,17 @@ q <- query(p <- point(sd), y) plot(data.frame(data(p))[c("x", "y")], asp=1) points(data.frame(data(q))[c("x", "y")], col="red") lines(rbind(y, y[1, ]), col="blue") + +# shapes that intersect the query region are kept +y <- rbind(c(30,45), c(40,45), c(35,50)) +t <- query(s <- shape(sd, 3), y) + +require(sf, quietly=TRUE) +df <- st_coordinates(st_as_sf(data(s))) +fd <- st_coordinates(st_as_sf(data(t))) +plot( + asp=1, xlim=c(15, 60), ylim=c(15, 60), + rbind(pol, pol[1, ]), type="l", col="blue") +foo <- by(df, df[, "L2"], \(x) points(x, type="b", col="black")) +foo <- by(fd, fd[, "L2"], \(x) points(x, type="b", col="red")) } From d8130ed97429bf947a28e478f632397d3ad0afd3 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Tue, 7 Apr 2026 19:24:14 +0200 Subject: [PATCH 077/151] cleaner docs --- R/query.R | 45 ++++++++++--------------------------- man/query.Rd | 21 +++++++++-------- tests/testthat/test-query.R | 12 ---------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/R/query.R b/R/query.R index daa768ba..fdf5fe54 100644 --- a/R/query.R +++ b/R/query.R @@ -4,10 +4,9 @@ #' @description Spatial queries serve to subset \code{SpatialData} elements #' according to a rectangular bounding box or arbitrary polygonal shapes. #' Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for -#' spatial operations, so that points on the boundary are included as well. -#' Note: shape queries ignore non-spatial attributes (e.g., radius for circles) -#' such that a circle is included if its centroid intersects the query region; -#' similarly, a polygon is included if any vertex intersects the query region. +#' spatial operations (i.e., instances that intersect the query region +#' in any way are kept). For circle shapes, radii are currently ignored +#' (i.e., a circle is kept if its centroid intersects the query region). #' #' @param x \code{SpatialData} element. #' @param y query specification; @@ -21,20 +20,23 @@ #' zs <- system.file(zs, package="SpatialData") #' sd <- readSpatialData(zs, tables=FALSE) #' +#' # helper for visualizing point coordinates +#' .xy <- \(.) data.frame(data(.)[c("x", "y")]) +#' #' # bounding box #' y <- list(xmin=11, xmax=44, ymin=22, ymax=55) #' q <- query(p <- point(sd), y) #' -#' plot(data.frame(data(p))[c("x", "y")], asp=1) -#' points(data.frame(data(q))[c("x", "y")], col="red") +#' plot(.xy(p), asp=1) +#' points(.xy(q), col="red") #' rect(y$xmin, y$ymin, y$xmax, y$ymax, border="blue") #' #' # polygon #' y <- rbind(c(20,10), c(50,30), c(20,50), c(30,30)) #' q <- query(p <- point(sd), y) #' -#' plot(data.frame(data(p))[c("x", "y")], asp=1) -#' points(data.frame(data(q))[c("x", "y")], col="red") +#' plot(.xy(p), asp=1) +#' points(.xy(q), col="red") #' lines(rbind(y, y[1, ]), col="blue") #' #' # shapes that intersect the query region are kept @@ -80,34 +82,10 @@ NULL return(mx) } -#' @rdname query -#' @export -setMethod("query", "SpatialData", \(x, j=NULL, ...) { - # check validity of dots - args <- list(...) - .check_box(args) - # guess coordinate space - stopifnot(length(j) == 1) - j <- if (is.null(j)) { - .guess_space(x) - } else { - if (is.character(j)) { - match.arg(j, CTname(x)) - } else if (is.numeric(j)) { - stopifnot(j > 0, j == round(j)) - CTname(x)[j] - } - } - # execute query - for (l in rownames(x)) - for (e in colnames(x)[[l]]) - x[[l]][[e]] <- query(x[[l]][[e]], j, ...) - return(x) -}) - #' @rdname query #' @export setMethod("query", "ImageArray", \(x, y) { + if (is.matrix(y)) stop("Polygon query not supported for images") .check_box(y) d <- dim(x) y$ymax <- min(y$ymax, d[2]) @@ -120,6 +98,7 @@ setMethod("query", "ImageArray", \(x, y) { #' @rdname query #' @export setMethod("query", "LabelArray", \(x, y) { + if (is.matrix(y)) stop("Polygon query not supported for labels") .check_box(y) d <- dim(x) y$ymax <- min(y$ymax, d[1]) diff --git a/man/query.Rd b/man/query.Rd index 2076a792..f7455ecf 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -2,15 +2,12 @@ % Please edit documentation in R/query.R \name{query} \alias{query} -\alias{query,SpatialData-method} \alias{query,ImageArray-method} \alias{query,LabelArray-method} \alias{query,ShapeFrame-method} \alias{query,PointFrame-method} \title{spatial queries} \usage{ -\S4method{query}{SpatialData}(x, j = NULL, ...) - \S4method{query}{ImageArray}(x, y) \S4method{query}{LabelArray}(x, y) @@ -33,30 +30,32 @@ same as input Spatial queries serve to subset \code{SpatialData} elements according to a rectangular bounding box or arbitrary polygonal shapes. Queries rely on lesser-/greater-equal and \code{sf::st_intersects} for -spatial operations, so that points on the boundary are included as well. -Note: shape queries ignore non-spatial attributes (e.g., radius for circles) -such that a circle is included if its centroid intersects the query region; -similarly, a polygon is included if any vertex intersects the query region. +spatial operations (i.e., instances that intersect the query region +in any way are kept). For circle shapes, radii are currently ignored +(i.e., a circle is kept if its centroid intersects the query region). } \examples{ zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) +# helper for visualizing point coordinates +.xy <- \(.) data.frame(data(.)[c("x", "y")]) + # bounding box y <- list(xmin=11, xmax=44, ymin=22, ymax=55) q <- query(p <- point(sd), y) -plot(data.frame(data(p))[c("x", "y")], asp=1) -points(data.frame(data(q))[c("x", "y")], col="red") +plot(.xy(p), asp=1) +points(.xy(q), col="red") rect(y$xmin, y$ymin, y$xmax, y$ymax, border="blue") # polygon y <- rbind(c(20,10), c(50,30), c(20,50), c(30,30)) q <- query(p <- point(sd), y) -plot(data.frame(data(p))[c("x", "y")], asp=1) -points(data.frame(data(q))[c("x", "y")], col="red") +plot(.xy(p), asp=1) +points(.xy(q), col="red") lines(rbind(y, y[1, ]), col="blue") # shapes that intersect the query region are kept diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 5b2ae3f5..73637861 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -36,18 +36,6 @@ test_that("query,.check_pol", { for (. in q) expect_error(.check_pol(.)) }) -# test_that("query,...", { -# # extract 1st element from every layer -# lys <- lapply(seq_len(4), \(.) x[.,1][[.]][[1]]) -# for (y in lys) { -# # missing bounding box coordinates -# expect_error(query(y, xmin=0, xmax=1, ymin=0)) -# # # invalid coordinate space -# # expect_error(query(y, ".", xmin=0, xmax=1, ymin=0, ymax=1)) -# # expect_error(query(y, 100, xmin=0, xmax=1, ymin=0, ymax=1)) -# } -# }) - test_that("query,ImageArray", { d <- dim(i <- image(x)) # neither crop nor shift From 319b0bb12e108fea56a6382b05cbe937e7db7d8b Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Tue, 7 Apr 2026 19:26:50 +0200 Subject: [PATCH 078/151] track changes --- inst/NEWS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inst/NEWS b/inst/NEWS index 4c35e07a..59a27e8b 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,9 @@ +changes in version 0.99.27 + +- spatial queries (aka subsetting) by bounding box + (all element types) or polygons (points and shapes), + including unit tests, documentation, visual examples + changes in version 0.99.26 - added unit tests for existing transformations From 5676dd0e31039610248d89f9c051c712708f89b7 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Tue, 7 Apr 2026 19:26:57 +0200 Subject: [PATCH 079/151] v0.99.27 bump --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a3c52e55..31e61414 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R Depends: R (>= 4.5) -Version: 0.99.26 +Version: 0.99.27 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufacturer data and writing to .zarr, on-disk representation of images/labels as From 334841791cb763a31dbbdf59d2467f909622fdcb Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 00:01:54 +0200 Subject: [PATCH 080/151] init mask revision --- NAMESPACE | 2 - R/AllGenerics.R | 4 ++ R/Zattrs.R | 3 + R/mask.R | 113 +++++++++++++++++++----------------- R/query.R | 25 ++++++++ man/mask.Rd | 24 +++++--- man/query.Rd | 3 + tests/testthat/test-query.R | 2 +- 8 files changed, 112 insertions(+), 64 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 1699d4d5..c39d44ee 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -157,9 +157,7 @@ importFrom(sf,st_distance) importFrom(sf,st_geometry) importFrom(sf,st_geometry_type) importFrom(sf,st_intersects) -importFrom(sf,st_point) importFrom(sf,st_polygon) -importFrom(sf,st_sfc) importFrom(utils,.DollarNames) importFrom(utils,head) importFrom(utils,tail) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index c92b0aa0..5c9f6541 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -57,6 +57,10 @@ setGeneric("rotate", \(x, t, ...) standardGeneric("rotate")) setGeneric("transform", \(x, ...) standardGeneric("transform")) setGeneric("translation", \(x, t, ...) standardGeneric("translation")) +# sda ---- + +setGeneric("feature_key", \(x, ...) standardGeneric("feature_key")) + # uts ---- setGeneric("layer", \(x, i, ...) standardGeneric("layer")) diff --git a/R/Zattrs.R b/R/Zattrs.R index 2d69664c..dc42e53d 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -75,3 +75,6 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) if (!is.null(cs)) coolcat("channels(%d): %s\n", cs) } setMethod("show", "Zattrs", .showZattrs) + +setMethod("feature_key", "Zattrs", \(x) x$spatialdata_attrs$feature_key) +setMethod("feature_key", "SpatialDataElement", \(x) feature_key(meta(x))) \ No newline at end of file diff --git a/R/mask.R b/R/mask.R index 9f62a532..9285febe 100644 --- a/R/mask.R +++ b/R/mask.R @@ -7,9 +7,14 @@ #' @param i,j character string; names of elements to mask, #' specifically, \code{i} will be masked by \code{j}, #' adding a \code{table} for \code{j} in \code{x}. +#' @param how character string; statistic to use for masking. +#' Defaults to "mean" when masking images, ignored when masking points. #' @param ... optional arguments passed to and from other methods. #' -#' @return \code{\link{SingleCellExperiment}} +#' @return +#' Input \code{SpatialData} object \code{x} with an additional table named +#' \code{_masking_}; or a \code{SingleCellExperiment} object when +#' masking elements directly (i.e., without \code{x} as input). #' #' @examples #' library(SingleCellExperiment) @@ -17,10 +22,15 @@ #' x <- system.file(x, package="SpatialData") #' x <- readSpatialData(x, tables=FALSE) #' -#' # count points in circles -#' x <- mask(x, "blobs_points", "blobs_circles") -#' x <- mask(x, "blobs_image", "blobs_labels") -#' tables(x) +#' # count points in shapes +#' y <- mask(x, "blobs_points", "blobs_circles") +#' (sce <- mask(i=point(x), j=shape(x, 1))) +#' identical(assay(table(y)), assay(sce)) +#' +#' # average image channels by labels +#' y <- mask(x, "blobs_image", "blobs_labels") +#' (sce <- mask(i=image(x), j=label(x))) +#' identical(assay(table(y)), assay(sce)) #' #' @export NULL @@ -30,54 +40,48 @@ NULL #' @rdname mask #' @importFrom SingleCellExperiment int_colData int_colData<- int_metadata<- #' @export -setMethod("mask", "SpatialData", \(x, i, j, ...) { +setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, how=NULL) { stopifnot(length(i) == 1, is.character(i), i %in% unlist(colnames(x))) stopifnot(length(j) == 1, is.character(j), j %in% unlist(colnames(x))) # get element types - ls <- vapply( - list(i, j), \(e) rownames(x)[vapply(colnames(x), - \(es) e %in% es, logical(1))], character(1)) - a <- element(x, ls[[1]], i) - b <- element(x, ls[[2]], j) - t <- .mask(a, b, ...) + f <- \(i) names(which(rapply(colnames(x), \(.) i %in% ., "character"))) + t <- mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how) md <- list(region=j, region_key="region", instance_key="instance") int_metadata(t)$spatialdata_attrs <- md cd <- data.frame(region=j, instance=colnames(t)) int_colData(t) <- cbind(int_colData(t), cd) - nm <- paste0(i, "_masked_by_", j) + nm <- paste0(j, "_masking_", i) `table<-`(x, nm, value=t) }) -setGeneric(".mask", \(a, b, ...) standardGeneric(".mask")) - #' @importFrom methods as #' @importFrom Matrix rowSums sparseVector t #' @importFrom SingleCellExperiment SingleCellExperiment -#' @importFrom sf st_as_sf st_geometry_type st_sfc st_point st_distance -setMethod(".mask", c("PointFrame", "ShapeFrame"), \(a, b) { - n <- nrow(b <- st_as_sf(data(b))) - fk <- meta(a)$spatialdata_attrs$feature_key - switch(paste(st_geometry_type(b)[1]), - POINT={ - # realize one feature at a time - is <- split(seq_len(length(a)), a[[fk]]) - ns <- lapply(is, \(.) { - # make points 'sf'-compliant - xy <- as.data.frame(a[., c("x", "y")]) - ps <- st_sfc(lapply(asplit(xy, 1), st_point)) - # for each circle, count points within radius - z <- rowSums(st_distance(b, ps) < b$radius) - # sparsify counts - sv <- sparseVector(z[i <- z > 0], which(i), n) - sm <- as(sv, "sparseMatrix") - }) - # collect intro matrix w/ dim. features x circles - ns <- t(as(do.call(cbind, ns), "dgCMatrix")) - rownames(ns) <- names(is) - colnames(ns) <- seq(ncol(ns)) - }) +#' @importFrom sf st_as_sf st_geometry_type st_distance +setMethod("mask", c("missing", "PointFrame", "ShapeFrame"), \(x, i, j, how=NULL) { + if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") + n <- nrow(j <- st_as_sf(data(j))) + fun <- switch(as.character(st_geometry_type(j[1, ])), + POINT=\(i, j) rowSums(st_distance(j, i) <= j$radius), + \(i, j) vapply(st_intersects(j, i), length, integer(1))) + # realize one feature at i time + is <- split(seq_len(length(i)), i[[feature_key(i)]]) + ns <- lapply(is, \(.) { + # make points 'sf'-compliant + i <- as.data.frame(i[., c("x", "y")]) + i <- st_as_sf(i, coords=c("x", "y")) + # for each shape, count intersecting points + z <- fun(i, j) + # sparsify counts + sv <- sparseVector(z[i <- z > 0], which(i), n) + sm <- as(sv, "sparseMatrix") + }) + # collect into matrix w/ dim. features x shapes + ns <- t(do.call(cbind, ns)) + rownames(ns) <- names(is) + colnames(ns) <- seq(ncol(ns)) SingleCellExperiment(list(counts=ns)) }) @@ -85,20 +89,21 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(a, b) { #' @importFrom DelayedArray realize #' @importFrom S4Arrays as.array.Array #' @importFrom SingleCellExperiment SingleCellExperiment -setMethod(".mask", c("ImageArray", "LabelArray"), \(a, b, fun=mean) { - # TODO: somehow rewrite w/o realizing everything - # at once (maybe w/ 'DelayedArray::blockApply'?) - .a2v <- \(.) as.vector(as.array.Array(.)) - stopifnot(dim(a)[-1] == dim(b)) - w <- .a2v(data(b)); w[w == 0] <- NA - n <- length(i <- unique(w[!is.na(w)])) - ns <- vapply(seq_len(dim(a)[1]), \(.) { - v <- .a2v(data(a, 1)[., , ]) - tapply(v, w, sum, na.rm=TRUE) - }, numeric(n)) - ns <- t(as(ns, "dgCMatrix")) - dimnames(ns) <- list(seq(dim(a)[1]), i) - SingleCellExperiment(list(counts=ns)) +setMethod("mask", c("missing", "ImageArray", "LabelArray"), \(x, i, j, how=NULL) { + if (is.null(how)) how <- "mean" + stopifnot(dim(i)[-1] == dim(j)) + .j <- as(data(j), "sparseVector") + .j <- as.vector(.j[ok <- .j > 0]) + mx <- apply(data(i), 1, \(.i) { + .i <- as(.i, "sparseVector") + .i <- as.vector(.i[ok]) + tapply(.i, .j, how) + }) + colnames(mx) <- channels(i) + se <- SingleCellExperiment(list(t(mx))) + assayNames(se) <- how + return(se) }) -setMethod(".mask", c("ANY", "ANY"), \(a, b) - stop("'mask'ing between these element types not supported.")) + +setMethod("mask", c("missing", "ANY", "ANY"), \(x, i, j, how=NULL) + stop("'mask'ing between these element types not yet supported")) diff --git a/R/query.R b/R/query.R index fdf5fe54..a50cb834 100644 --- a/R/query.R +++ b/R/query.R @@ -82,6 +82,31 @@ NULL return(mx) } +#' @rdname query +#' @importFrom dplyr filter +#' @export +setMethod("query", "SpatialData", \(x, ..., i) { + # TODO: need more example data to properly implement this; + # for now, just a proof of concept using 'spatialdata_attrs' + if (missing(i)) i <- 1 + if (!length(tables(x))) + stop("There aren't any tables") + if (is.numeric(i)) { + i <- tableNames(x)[i] + } else if (is.character(i)) { + i <- match.arg(i, tableNames(x)) + } + t <- x$tables[[i]] + ns <- vapply(nm <- colnames(x), length, integer(1)) + nm <- data.frame(layer=rep.int(names(nm), ns), region=unlist(nm)) + nm <- filter(nm, ...) + i <- match(nm$layer, .LAYERS) + j <- split(nm$region, nm$layer) + x <- x[i, j] + x$tables$table <- t + return(x) +}) + #' @rdname query #' @export setMethod("query", "ImageArray", \(x, y) { diff --git a/man/mask.Rd b/man/mask.Rd index 2caa01cf..a64865cd 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -2,10 +2,10 @@ % Please edit documentation in R/mask.R \name{mask} \alias{mask} -\alias{mask,SpatialData-method} +\alias{mask,SpatialData,ANY,ANY-method} \title{Masking} \usage{ -\S4method{mask}{SpatialData}(x, i, j, ...) +\S4method{mask}{SpatialData,ANY,ANY}(x, i, j, how = NULL) } \arguments{ \item{x}{\code{\link{SpatialData}} object.} @@ -14,10 +14,15 @@ specifically, \code{i} will be masked by \code{j}, adding a \code{table} for \code{j} in \code{x}.} +\item{how}{character string; statistic to use for masking. +Defaults to "mean" when masking images, ignored when masking points.} + \item{...}{optional arguments passed to and from other methods.} } \value{ -\code{\link{SingleCellExperiment}} +Input \code{SpatialData} object \code{x} with an additional table named +\code{_masking_}; or a \code{SingleCellExperiment} object when +masking elements directly (i.e., without \code{x} as input). } \description{ ... @@ -28,9 +33,14 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) -# count points in circles -x <- mask(x, "blobs_points", "blobs_circles") -x <- mask(x, "blobs_image", "blobs_labels") -tables(x) +# count points in shapes +y <- mask(x, "blobs_points", "blobs_circles") +(sce <- mask(i=point(x), j=shape(x, 1))) +identical(assay(table(y)), assay(sce)) + +# average image channels by labels +y <- mask(x, "blobs_image", "blobs_labels") +(sce <- mask(i=image(x), j=label(x))) +identical(assay(table(y)), assay(sce)) } diff --git a/man/query.Rd b/man/query.Rd index f7455ecf..cb50c9b3 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -2,12 +2,15 @@ % Please edit documentation in R/query.R \name{query} \alias{query} +\alias{query,SpatialData-method} \alias{query,ImageArray-method} \alias{query,LabelArray-method} \alias{query,ShapeFrame-method} \alias{query,PointFrame-method} \title{spatial queries} \usage{ +\S4method{query}{SpatialData}(x, ..., i) + \S4method{query}{ImageArray}(x, y) \S4method{query}{LabelArray}(x, y) diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 73637861..2c161788 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -1,7 +1,7 @@ require(sf, quietly=TRUE) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, tables=FALSE) +x <- readSpatialData(x, anndataR=TRUE) test_that("query,.check_box", { # valid From 2e70594c98fd64526506368067ed10dad8e11229 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 00:04:08 +0200 Subject: [PATCH 081/151] more docs --- R/mask.R | 6 +++++- man/mask.Rd | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/R/mask.R b/R/mask.R index 9285febe..f0b8a206 100644 --- a/R/mask.R +++ b/R/mask.R @@ -1,7 +1,11 @@ #' @name mask #' @title Masking #' -#' @description ... +#' @description +#' Masking operations serve to aggregate data across layers, e.g., +#' counting points in shapes, averaging image channels by labels, etc. +#' For added flexibility, these may be carried out directly between elements, +#' or using an input \code{SpatialData} object and specifying element names. #' #' @param x \code{\link{SpatialData}} object. #' @param i,j character string; names of elements to mask, diff --git a/man/mask.Rd b/man/mask.Rd index a64865cd..da716523 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -25,7 +25,10 @@ Input \code{SpatialData} object \code{x} with an additional table named masking elements directly (i.e., without \code{x} as input). } \description{ -... +Masking operations serve to aggregate data across layers, e.g., +counting points in shapes, averaging image channels by labels, etc. +For added flexibility, these may be carried out directly between elements, +or using an input \code{SpatialData} object and specifying element names. } \examples{ library(SingleCellExperiment) From ecca7df8d51b1dc59ef7bea53bd36f13c4cf4e97 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 09:42:15 +0200 Subject: [PATCH 082/151] fix doc typo --- R/query.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/query.R b/R/query.R index a50cb834..e3ea8716 100644 --- a/R/query.R +++ b/R/query.R @@ -48,7 +48,7 @@ #' fd <- st_coordinates(st_as_sf(data(t))) #' plot( #' asp=1, xlim=c(15, 60), ylim=c(15, 60), -#' rbind(pol, pol[1, ]), type="l", col="blue") +#' rbind(y, y[1, ]), type="l", col="blue") #' foo <- by(df, df[, "L2"], \(x) points(x, type="b", col="black")) #' foo <- by(fd, fd[, "L2"], \(x) points(x, type="b", col="red")) NULL From 4d9539deb67b16dbc1d338bef2d42daacba4e948 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 12:44:59 +0200 Subject: [PATCH 083/151] some progress on masking with tables --- NAMESPACE | 2 + R/mask.R | 100 ++++++++++++++++++++++++++++++--------------- man/SpatialData.Rd | 8 ++-- man/mask.Rd | 31 ++++++++++---- man/query.Rd | 2 +- 5 files changed, 97 insertions(+), 46 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index c39d44ee..1cbd069d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -114,6 +114,7 @@ importFrom(SingleCellExperiment,int_colData) importFrom(SingleCellExperiment,int_metadata) importFrom(SummarizedExperiment,"colData<-") importFrom(SummarizedExperiment,assay) +importFrom(SummarizedExperiment,assayNames) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) @@ -148,6 +149,7 @@ importFrom(methods,new) importFrom(methods,setClassUnion) importFrom(methods,setReplaceMethod) importFrom(reticulate,import) +importFrom(scuttle,aggregateAcrossCells) importFrom(sf,"st_geometry<-") importFrom(sf,st_as_sf) importFrom(sf,st_bbox) diff --git a/R/mask.R b/R/mask.R index f0b8a206..7682e5fe 100644 --- a/R/mask.R +++ b/R/mask.R @@ -12,7 +12,7 @@ #' specifically, \code{i} will be masked by \code{j}, #' adding a \code{table} for \code{j} in \code{x}. #' @param how character string; statistic to use for masking. -#' Defaults to "mean" when masking images, ignored when masking points. +#' @param name function use to generate the new \code{table}'s name. #' @param ... optional arguments passed to and from other methods. #' #' @return @@ -28,14 +28,21 @@ #' #' # count points in shapes #' y <- mask(x, "blobs_points", "blobs_circles") -#' (sce <- mask(i=point(x), j=shape(x, 1))) -#' identical(assay(table(y)), assay(sce)) +#' t <- y$tables$blobs_circles_masking_blobs_points +#' (sce <- .mask(i=point(x), j=shape(x, 1))) +#' identical(assay(t), assay(sce)) #' #' # average image channels by labels #' y <- mask(x, "blobs_image", "blobs_labels") -#' (sce <- mask(i=image(x), j=label(x))) -#' identical(assay(table(y)), assay(sce)) +#' t <- y$tables$blobs_labels_masking_blobs_image +#' (sce <- .mask(i=image(x), j=label(x))) +#' identical(assay(t), assay(sce)) #' +#' library(SpatialData.data) +#' x <- get_demo_SDdata("merfish") +#' x <- readSpatialData(x) +#' y <- mask(x, "cells", "anatomical") +#' tail(tables(y), 1) #' @export NULL @@ -44,27 +51,52 @@ NULL #' @rdname mask #' @importFrom SingleCellExperiment int_colData int_colData<- int_metadata<- #' @export -setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, how=NULL) { +setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, how=NULL, name=\(i, j) sprintf("%s_by_%s", i, j), ...) { + if (!is.null(how)) how <- match.arg(how, c("sum", "mean")) stopifnot(length(i) == 1, is.character(i), i %in% unlist(colnames(x))) stopifnot(length(j) == 1, is.character(j), j %in% unlist(colnames(x))) - # get element types + ok <- is.character(name) && length(name) == 1 && !name %in% tableNames(x) + nm <- if (is.function(name)) name(i, j) else if (ok) name else stop( + "Invalid 'name'; should be a function or a ", + "character string not yet in 'tableNames(x)'") + t <- tryCatch(error=\(.) NULL, getTable(x, i)) f <- \(i) names(which(rapply(colnames(x), \(.) i %in% ., "character"))) - t <- mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how) - md <- list(region=j, - region_key="region", - instance_key="instance") - int_metadata(t)$spatialdata_attrs <- md - cd <- data.frame(region=j, instance=colnames(t)) - int_colData(t) <- cbind(int_colData(t), cd) - nm <- paste0(j, "_masking_", i) - `table<-`(x, nm, value=t) + se <- .mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how, table=t) + md <- list(region=j, region_key="region", instance_key="instance") + int_metadata(se)$spatialdata_attrs <- md + cd <- data.frame(region=j, instance=colnames(se)) + int_colData(se) <- cbind(int_colData(se), cd) + `table<-`(x, nm, value=se) +}) + +setGeneric(".mask", \(i, j, ...) standardGeneric(".mask")) + +#' @importFrom methods as +#' @importFrom DelayedArray realize +#' @importFrom S4Arrays as.array.Array +#' @importFrom SummarizedExperiment assayNames +#' @importFrom SingleCellExperiment SingleCellExperiment +setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL) { + if (is.null(how)) { how <- "mean"; message("Missing 'how'; defaulting to 'mean'") } + stopifnot(dim(i)[-1] == dim(j)) + .j <- as(data(j), "sparseVector") + .j <- as.vector(.j[ok <- .j > 0]) + mx <- apply(data(i), 1, \(.i) { + .i <- as(.i, "sparseVector") + .i <- as.vector(.i[ok]) + tapply(.i, .j, how) + }) + colnames(mx) <- channels(i) + se <- SingleCellExperiment(list(t(mx))) + assayNames(se) <- how + return(se) }) #' @importFrom methods as #' @importFrom Matrix rowSums sparseVector t #' @importFrom SingleCellExperiment SingleCellExperiment #' @importFrom sf st_as_sf st_geometry_type st_distance -setMethod("mask", c("missing", "PointFrame", "ShapeFrame"), \(x, i, j, how=NULL) { +setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL) { if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") n <- nrow(j <- st_as_sf(data(j))) fun <- switch(as.character(st_geometry_type(j[1, ])), @@ -89,25 +121,27 @@ setMethod("mask", c("missing", "PointFrame", "ShapeFrame"), \(x, i, j, how=NULL) SingleCellExperiment(list(counts=ns)) }) -#' @importFrom methods as -#' @importFrom DelayedArray realize -#' @importFrom S4Arrays as.array.Array +#' @importFrom scuttle aggregateAcrossCells +#' @importFrom SummarizedExperiment assayNames #' @importFrom SingleCellExperiment SingleCellExperiment -setMethod("mask", c("missing", "ImageArray", "LabelArray"), \(x, i, j, how=NULL) { - if (is.null(how)) how <- "mean" - stopifnot(dim(i)[-1] == dim(j)) - .j <- as(data(j), "sparseVector") - .j <- as.vector(.j[ok <- .j > 0]) - mx <- apply(data(i), 1, \(.i) { - .i <- as(.i, "sparseVector") - .i <- as.vector(.i[ok]) - tapply(.i, .j, how) - }) - colnames(mx) <- channels(i) - se <- SingleCellExperiment(list(t(mx))) +setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL) { + #how <- value <- NULL; i <- shape(x, "cells"); j <- shape(x, "anatomical") + if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } + if (is.null(table)) stop("Missing 'table'; can't mask shapes without") + if (is.null(value)) value <- rownames(table) + idx <- st_intersects( + st_as_sf(data(j)), + st_as_sf(data(i))) + foo <- integer(nrow(i)) + foo[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) + se <- aggregateAcrossCells(table, + ids=foo, statistics=how, use.assay.type=1, + store_number=paste0("n_", meta(table)$region)) + colnames(se) <- se[[1]]; se[[1]] <- NULL assayNames(se) <- how return(se) }) -setMethod("mask", c("missing", "ANY", "ANY"), \(x, i, j, how=NULL) +setMethod(".mask", c("ANY", "ANY"), \(x, i, j, ...) stop("'mask'ing between these element types not yet supported")) + diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/mask.Rd b/man/mask.Rd index da716523..d5488532 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -2,10 +2,17 @@ % Please edit documentation in R/mask.R \name{mask} \alias{mask} -\alias{mask,SpatialData,ANY,ANY-method} +\alias{mask,SpatialData-method} \title{Masking} \usage{ -\S4method{mask}{SpatialData,ANY,ANY}(x, i, j, how = NULL) +\S4method{mask}{SpatialData}( + x, + i, + j, + how = NULL, + name = function(i, j) sprintf("\%s_by_\%s", i, j), + ... +) } \arguments{ \item{x}{\code{\link{SpatialData}} object.} @@ -14,8 +21,9 @@ specifically, \code{i} will be masked by \code{j}, adding a \code{table} for \code{j} in \code{x}.} -\item{how}{character string; statistic to use for masking. -Defaults to "mean" when masking images, ignored when masking points.} +\item{how}{character string; statistic to use for masking.} + +\item{name}{function use to generate the new \code{table}'s name.} \item{...}{optional arguments passed to and from other methods.} } @@ -38,12 +46,19 @@ x <- readSpatialData(x, tables=FALSE) # count points in shapes y <- mask(x, "blobs_points", "blobs_circles") -(sce <- mask(i=point(x), j=shape(x, 1))) -identical(assay(table(y)), assay(sce)) +t <- y$tables$blobs_circles_masking_blobs_points +(sce <- .mask(i=point(x), j=shape(x, 1))) +identical(assay(t), assay(sce)) # average image channels by labels y <- mask(x, "blobs_image", "blobs_labels") -(sce <- mask(i=image(x), j=label(x))) -identical(assay(table(y)), assay(sce)) +t <- y$tables$blobs_labels_masking_blobs_image +(sce <- .mask(i=image(x), j=label(x))) +identical(assay(t), assay(sce)) +library(SpatialData.data) +x <- get_demo_SDdata("merfish") +x <- readSpatialData(x) +y <- mask(x, "cells", "anatomical") +tail(tables(y), 1) } diff --git a/man/query.Rd b/man/query.Rd index cb50c9b3..b3a383bd 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -70,7 +70,7 @@ df <- st_coordinates(st_as_sf(data(s))) fd <- st_coordinates(st_as_sf(data(t))) plot( asp=1, xlim=c(15, 60), ylim=c(15, 60), - rbind(pol, pol[1, ]), type="l", col="blue") + rbind(y, y[1, ]), type="l", col="blue") foo <- by(df, df[, "L2"], \(x) points(x, type="b", col="black")) foo <- by(fd, fd[, "L2"], \(x) points(x, type="b", col="red")) } From 2216dae31608808be039f2903fc5d791d3e08dda Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 13:30:09 +0200 Subject: [PATCH 084/151] masking tests --- R/mask.R | 29 ++++++++++++-------- tests/testthat/test-mask.R | 56 ++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/R/mask.R b/R/mask.R index 7682e5fe..09d408c9 100644 --- a/R/mask.R +++ b/R/mask.R @@ -46,26 +46,29 @@ #' @export NULL -# TODO: table from point + shape, image + label etc. etc. etc. +.check_ij <- \(x, .) stopifnot(length(.) == 1, is.character(.), . %in% unlist(colnames(x))) #' @rdname mask #' @importFrom SingleCellExperiment int_colData int_colData<- int_metadata<- #' @export -setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, how=NULL, name=\(i, j) sprintf("%s_by_%s", i, j), ...) { +setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, + how=NULL, name=\(i, j) sprintf("%s_by_%s", i, j), ...) { + .check_ij(x, i); .check_ij(x, j) if (!is.null(how)) how <- match.arg(how, c("sum", "mean")) - stopifnot(length(i) == 1, is.character(i), i %in% unlist(colnames(x))) - stopifnot(length(j) == 1, is.character(j), j %in% unlist(colnames(x))) ok <- is.character(name) && length(name) == 1 && !name %in% tableNames(x) nm <- if (is.function(name)) name(i, j) else if (ok) name else stop( "Invalid 'name'; should be a function or a ", "character string not yet in 'tableNames(x)'") t <- tryCatch(error=\(.) NULL, getTable(x, i)) f <- \(i) names(which(rapply(colnames(x), \(.) i %in% ., "character"))) - se <- .mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how, table=t) + se <- .mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how, table=t, ...) md <- list(region=j, region_key="region", instance_key="instance") int_metadata(se)$spatialdata_attrs <- md - cd <- data.frame(region=j, instance=colnames(se)) - int_colData(se) <- cbind(int_colData(se), cd) + assay(se) <- as(assay(se), "dgCMatrix") + cd <- int_colData(se) + cd$region <- j + cd$instance <- colnames(se) + int_colData(se) <- cd `table<-`(x, nm, value=se) }) @@ -76,7 +79,7 @@ setGeneric(".mask", \(i, j, ...) standardGeneric(".mask")) #' @importFrom S4Arrays as.array.Array #' @importFrom SummarizedExperiment assayNames #' @importFrom SingleCellExperiment SingleCellExperiment -setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL) { +setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { if (is.null(how)) { how <- "mean"; message("Missing 'how'; defaulting to 'mean'") } stopifnot(dim(i)[-1] == dim(j)) .j <- as(data(j), "sparseVector") @@ -96,7 +99,7 @@ setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL) { #' @importFrom Matrix rowSums sparseVector t #' @importFrom SingleCellExperiment SingleCellExperiment #' @importFrom sf st_as_sf st_geometry_type st_distance -setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL) { +setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") n <- nrow(j <- st_as_sf(data(j))) fun <- switch(as.character(st_geometry_type(j[1, ])), @@ -124,18 +127,20 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL) { #' @importFrom scuttle aggregateAcrossCells #' @importFrom SummarizedExperiment assayNames #' @importFrom SingleCellExperiment SingleCellExperiment -setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL) { +setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { #how <- value <- NULL; i <- shape(x, "cells"); j <- shape(x, "anatomical") if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } if (is.null(table)) stop("Missing 'table'; can't mask shapes without") - if (is.null(value)) value <- rownames(table) + ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) + if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") idx <- st_intersects( st_as_sf(data(j)), st_as_sf(data(i))) foo <- integer(nrow(i)) foo[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) se <- aggregateAcrossCells(table, - ids=foo, statistics=how, use.assay.type=1, + ids=foo, subset.row=value, + statistics=how, use.assay.type=1, store_number=paste0("n_", meta(table)$region)) colnames(se) <- se[[1]]; se[[1]] <- NULL assayNames(se) <- how diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index 9039bfe6..93c6cf3c 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -3,24 +3,56 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, anndataR=TRUE) -test_that("mask(),ImageArray,LabelArray", { +test_that("mask,ImageArray,LabelArray", { i <- "blobs_image" j <- "blobs_labels" - x <- SpatialData::mask(x, i, j, fun=sum) + # reproduce example data + y <- mask(x, i, j, how="sum") expect_equivalent( - assay(SpatialData::table(x, 1)), - assay(SpatialData::table(x, 2))) + assay(tables(y)[[2]]), + assay(tables(x)[[1]])) + # default to 'mean' with a message + expect_message(y <- mask(x, i, j)) + expect_silent(z <- mask(x, i, j, how="mean")) + expect_identical(y, z) }) -test_that("mask(),PointFrame,ShapeFrame", { +test_that("mask,PointFrame,ShapeFrame", { i <- "blobs_points" j <- "blobs_circles" - x <- SpatialData::mask(x, i, j) - t <- getTable(x, j) - md <- meta(point(x, i)) - md <- md$spatialdata_attrs - fk <- md$feature_key - nr <- length(unique(point(x, i)[[fk]])) + y <- mask(x, i, j) + t <- getTable(y, j) + fk <- feature_key(p <- point(x, i)) + np <- length(unique(p[[fk]])) nc <- nrow(shape(x, j)) - expect_equal(dim(t), c(nr, nc)) + expect_equal(dim(t), c(np, nc)) + # ignore 'how' with a warning + expect_warning(mask(x, i, j, how="sum")) +}) + +require(SpatialData.data, quietly=TRUE) +x <- get_demo_SDdata("merfish") +x <- readSpatialData(x) + +test_that("mask,ShapeFrame,ShapeFrame", { + i <- "cells" + j <- "anatomical" + # error without 'table' + y <- x; tables(y) <- list() + expect_error(mask(y, i, j)) + # default to 'sum' with a message + expect_message(y <- mask(x, i, j)) + expect_silent(z <- mask(x, i, j, how="sum")) + expect_identical(y, z) + old <- getTable(y, i) + new <- getTable(y, j) + expect_equal(dim(new), c(nrow(old), nrow(shape(x, j))+1)) + expect_equal(sum(assay(new)), sum(assay(old))) + expect_identical(rownames(new), rownames(old)) + expect_identical(meta(new)$region, j) + # 'value' should be a character + # vector of rownames in 'table(x, i)' + expect_error(mask(x, i, j, value="x")) + y <- mask(x, i, j, value=v <- sample(rownames(old), 5)) + expect_equal(sum(assay(new[v, ])), sum(assay(old[v, ]))) }) From 8c8e4ef5813099eaf3ba419e958e9f9bf21a8119 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 13:34:41 +0200 Subject: [PATCH 085/151] +Imports:scuttle --- DESCRIPTION | 1 + R/mask.R | 2 +- tests/testthat/test-mask.R | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 31e61414..cbaf5789 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -47,6 +47,7 @@ Imports: RBGL, reticulate, anndataR, + scuttle, sf, S4Arrays, S4Vectors, diff --git a/R/mask.R b/R/mask.R index 09d408c9..a89c0f8d 100644 --- a/R/mask.R +++ b/R/mask.R @@ -129,10 +129,10 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { #how <- value <- NULL; i <- shape(x, "cells"); j <- shape(x, "anatomical") - if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } if (is.null(table)) stop("Missing 'table'; can't mask shapes without") ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") + if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } idx <- st_intersects( st_as_sf(data(j)), st_as_sf(data(i))) diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index 93c6cf3c..da27a205 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -52,7 +52,8 @@ test_that("mask,ShapeFrame,ShapeFrame", { expect_identical(meta(new)$region, j) # 'value' should be a character # vector of rownames in 'table(x, i)' - expect_error(mask(x, i, j, value="x")) - y <- mask(x, i, j, value=v <- sample(rownames(old), 5)) + v <- sample(rownames(old), 5) + new <- getTable(mask(x, i, j, value=v), j) expect_equal(sum(assay(new[v, ])), sum(assay(old[v, ]))) + expect_error(mask(x, i, j, value=`[<-`(v, i=1, "x"))) }) From d9b2a20098baa3b3f3e6f89718baf433b656b87e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 13:37:02 +0200 Subject: [PATCH 086/151] note on masking --- inst/NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inst/NEWS b/inst/NEWS index 59a27e8b..f1cf28bc 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -3,6 +3,9 @@ changes in version 0.99.27 - spatial queries (aka subsetting) by bounding box (all element types) or polygons (points and shapes), including unit tests, documentation, visual examples +- masking draft to aggregate information across layers + (image by label, point by shape, shape by shape; + the latter aggregates values in an associated table) changes in version 0.99.26 From 00fc7ee4d2ccb0eabb589764857640efad9817ca Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 13:43:29 +0200 Subject: [PATCH 087/151] bug fix doc --- R/mask.R | 4 ++-- man/mask.Rd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/mask.R b/R/mask.R index a89c0f8d..bf173a57 100644 --- a/R/mask.R +++ b/R/mask.R @@ -28,13 +28,13 @@ #' #' # count points in shapes #' y <- mask(x, "blobs_points", "blobs_circles") -#' t <- y$tables$blobs_circles_masking_blobs_points +#' t <- tail(y$tables, 1)[[1]] #' (sce <- .mask(i=point(x), j=shape(x, 1))) #' identical(assay(t), assay(sce)) #' #' # average image channels by labels #' y <- mask(x, "blobs_image", "blobs_labels") -#' t <- y$tables$blobs_labels_masking_blobs_image +#' t <- tail(y$tables, 1)[[1]] #' (sce <- .mask(i=image(x), j=label(x))) #' identical(assay(t), assay(sce)) #' diff --git a/man/mask.Rd b/man/mask.Rd index d5488532..5bfe22e8 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -46,13 +46,13 @@ x <- readSpatialData(x, tables=FALSE) # count points in shapes y <- mask(x, "blobs_points", "blobs_circles") -t <- y$tables$blobs_circles_masking_blobs_points +t <- tail(y$tables, 1)[[1]] (sce <- .mask(i=point(x), j=shape(x, 1))) identical(assay(t), assay(sce)) # average image channels by labels y <- mask(x, "blobs_image", "blobs_labels") -t <- y$tables$blobs_labels_masking_blobs_image +t <- tail(y$tables, 1)[[1]] (sce <- .mask(i=image(x), j=label(x))) identical(assay(t), assay(sce)) From c04939f3a9b514214ffa2afbe43178cf41f4dafc Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:04:19 +0200 Subject: [PATCH 088/151] fix roxy imports --- NAMESPACE | 3 ++- R/mask.R | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 1cbd069d..7070fd1b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -112,9 +112,10 @@ importFrom(SingleCellExperiment,"int_metadata<-") importFrom(SingleCellExperiment,SingleCellExperiment) importFrom(SingleCellExperiment,int_colData) importFrom(SingleCellExperiment,int_metadata) +importFrom(SummarizedExperiment,"assay<-") +importFrom(SummarizedExperiment,"assayNames<-") importFrom(SummarizedExperiment,"colData<-") importFrom(SummarizedExperiment,assay) -importFrom(SummarizedExperiment,assayNames) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) diff --git a/R/mask.R b/R/mask.R index bf173a57..589b0f79 100644 --- a/R/mask.R +++ b/R/mask.R @@ -49,6 +49,8 @@ NULL .check_ij <- \(x, .) stopifnot(length(.) == 1, is.character(.), . %in% unlist(colnames(x))) #' @rdname mask +#' @importFrom methods as +#' @importFrom SummarizedExperiment assay assay<- #' @importFrom SingleCellExperiment int_colData int_colData<- int_metadata<- #' @export setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, @@ -74,10 +76,11 @@ setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, setGeneric(".mask", \(i, j, ...) standardGeneric(".mask")) +#' @noRd #' @importFrom methods as #' @importFrom DelayedArray realize #' @importFrom S4Arrays as.array.Array -#' @importFrom SummarizedExperiment assayNames +#' @importFrom SummarizedExperiment assayNames<- #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { if (is.null(how)) { how <- "mean"; message("Missing 'how'; defaulting to 'mean'") } @@ -95,6 +98,7 @@ setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { return(se) }) +#' @noRd #' @importFrom methods as #' @importFrom Matrix rowSums sparseVector t #' @importFrom SingleCellExperiment SingleCellExperiment @@ -124,8 +128,9 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { SingleCellExperiment(list(counts=ns)) }) +#' @noRd #' @importFrom scuttle aggregateAcrossCells -#' @importFrom SummarizedExperiment assayNames +#' @importFrom SummarizedExperiment assayNames<- #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { #how <- value <- NULL; i <- shape(x, "cells"); j <- shape(x, "anatomical") @@ -147,6 +152,7 @@ setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL return(se) }) +#' @noRd setMethod(".mask", c("ANY", "ANY"), \(x, i, j, ...) stop("'mask'ing between these element types not yet supported")) From 4843dfdf12a226b377cfcaa4dfb6fd4b5f663510 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:18:55 +0200 Subject: [PATCH 089/151] fix R CMD check error/warnings --- R/coord_utils.R | 2 +- R/mask.R | 12 +++++------- R/query.R | 2 ++ R/sdArray.R | 2 ++ man/Array-methods.Rd | 2 ++ man/coord-utils.Rd | 1 + man/mask.Rd | 11 +++++------ man/query.Rd | 4 ++++ tests/testthat/test-mask.R | 9 +++++++++ 9 files changed, 31 insertions(+), 14 deletions(-) diff --git a/R/coord_utils.R b/R/coord_utils.R index 83451fc4..440ad77c 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -1,6 +1,6 @@ #' @name coord-utils #' @title Coordinate transformations -#' @aliases axes CTname CTtype CTdata CTpath CTgraph addCT rmvCT +#' @aliases axes CTlist CTname CTtype CTdata CTpath CTgraph addCT rmvCT #' #' @param x \code{SpatialData}, an element, or \code{Zattrs}. #' @param i for \code{CTpath}, source node label; else, string or diff --git a/R/mask.R b/R/mask.R index 589b0f79..4196a4b3 100644 --- a/R/mask.R +++ b/R/mask.R @@ -28,21 +28,20 @@ #' #' # count points in shapes #' y <- mask(x, "blobs_points", "blobs_circles") -#' t <- tail(y$tables, 1)[[1]] -#' (sce <- .mask(i=point(x), j=shape(x, 1))) -#' identical(assay(t), assay(sce)) +#' tail(tables(y), 1) #' #' # average image channels by labels #' y <- mask(x, "blobs_image", "blobs_labels") -#' t <- tail(y$tables, 1)[[1]] -#' (sce <- .mask(i=image(x), j=label(x))) -#' identical(assay(t), assay(sce)) +#' tail(tables(y), 1) #' #' library(SpatialData.data) #' x <- get_demo_SDdata("merfish") #' x <- readSpatialData(x) +#' +#' # sum assay data from table by shapes #' y <- mask(x, "cells", "anatomical") #' tail(tables(y), 1) +#' #' @export NULL @@ -133,7 +132,6 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { #' @importFrom SummarizedExperiment assayNames<- #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { - #how <- value <- NULL; i <- shape(x, "cells"); j <- shape(x, "anatomical") if (is.null(table)) stop("Missing 'table'; can't mask shapes without") ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") diff --git a/R/query.R b/R/query.R index e3ea8716..2630d05d 100644 --- a/R/query.R +++ b/R/query.R @@ -12,6 +12,8 @@ #' @param y query specification; #' bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' #' (order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns. +#' @param i for \code{SpatialData}, index or name of table to query. +#' @param ... optional arguments passed to and from other methods. #' #' @return same as input #' diff --git a/R/sdArray.R b/R/sdArray.R index 8e005e5f..c1893892 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -8,6 +8,8 @@ #' dim,LabelArray-method #' length,ImageArray-method #' length,LabelArray-method +#' data_type,ImageArray-method +#' data_type,LabelArray-method #' #' @param x \code{ImageArray} or \code{LabelArray} #' @param k scalar index specifying which scale to extract. diff --git a/man/Array-methods.Rd b/man/Array-methods.Rd index 1990122e..2be1a08a 100644 --- a/man/Array-methods.Rd +++ b/man/Array-methods.Rd @@ -8,6 +8,8 @@ \alias{dim,LabelArray-method} \alias{length,ImageArray-method} \alias{length,LabelArray-method} +\alias{data_type,ImageArray-method} +\alias{data_type,LabelArray-method} \alias{data,sdArray-method} \alias{dim,sdArray-method} \alias{length,sdArray-method} diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index b1d3efd7..aab39e96 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -3,6 +3,7 @@ \name{coord-utils} \alias{coord-utils} \alias{axes} +\alias{CTlist} \alias{CTname} \alias{CTtype} \alias{CTdata} diff --git a/man/mask.Rd b/man/mask.Rd index 5bfe22e8..73c55451 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -46,19 +46,18 @@ x <- readSpatialData(x, tables=FALSE) # count points in shapes y <- mask(x, "blobs_points", "blobs_circles") -t <- tail(y$tables, 1)[[1]] -(sce <- .mask(i=point(x), j=shape(x, 1))) -identical(assay(t), assay(sce)) +tail(tables(y), 1) # average image channels by labels y <- mask(x, "blobs_image", "blobs_labels") -t <- tail(y$tables, 1)[[1]] -(sce <- .mask(i=image(x), j=label(x))) -identical(assay(t), assay(sce)) +tail(tables(y), 1) library(SpatialData.data) x <- get_demo_SDdata("merfish") x <- readSpatialData(x) + +# sum assay data from table by shapes y <- mask(x, "cells", "anatomical") tail(tables(y), 1) + } diff --git a/man/query.Rd b/man/query.Rd index b3a383bd..83d23cf4 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -22,6 +22,10 @@ \arguments{ \item{x}{\code{SpatialData} element.} +\item{...}{optional arguments passed to and from other methods.} + +\item{i}{for \code{SpatialData}, index or name of table to query.} + \item{y}{query specification; bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' (order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns.} diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index da27a205..1eebe0d0 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -3,6 +3,15 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, anndataR=TRUE) +test_that("mask,unsupported", { + nm <- list( + c(imageNames(x)[1], imageNames(x)[2]), # image,image + c(labelNames(x)[1], labelNames(x)[2]), # label,label + c(labelNames(x)[1], imageNames(x)[1]), # label,image + c(shapeNames(x)[1], pointNames(x)[1])) # shape,point + for (ij in nm) expect_error(mask(x, ij[1], ij[2])) +}) + test_that("mask,ImageArray,LabelArray", { i <- "blobs_image" j <- "blobs_labels" From 60c8c4611ade0ac3c923176a97600f19cb709400 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:22:51 +0200 Subject: [PATCH 090/151] make copi happier --- R/mask.R | 7 ++----- man/mask.Rd | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/R/mask.R b/R/mask.R index 4196a4b3..5a93763d 100644 --- a/R/mask.R +++ b/R/mask.R @@ -15,10 +15,7 @@ #' @param name function use to generate the new \code{table}'s name. #' @param ... optional arguments passed to and from other methods. #' -#' @return -#' Input \code{SpatialData} object \code{x} with an additional table named -#' \code{_masking_}; or a \code{SingleCellExperiment} object when -#' masking elements directly (i.e., without \code{x} as input). +#' @return Input \code{SpatialData} object \code{x} with an additional table. #' #' @examples #' library(SingleCellExperiment) @@ -151,6 +148,6 @@ setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL }) #' @noRd -setMethod(".mask", c("ANY", "ANY"), \(x, i, j, ...) +setMethod(".mask", c("ANY", "ANY"), \(i, j, ...) stop("'mask'ing between these element types not yet supported")) diff --git a/man/mask.Rd b/man/mask.Rd index 73c55451..6e960757 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -28,9 +28,7 @@ adding a \code{table} for \code{j} in \code{x}.} \item{...}{optional arguments passed to and from other methods.} } \value{ -Input \code{SpatialData} object \code{x} with an additional table named -\code{_masking_}; or a \code{SingleCellExperiment} object when -masking elements directly (i.e., without \code{x} as input). +Input \code{SpatialData} object \code{x} with an additional table. } \description{ Masking operations serve to aggregate data across layers, e.g., From 0ad8f952e26cf7eafc386efb9a0e1d4b2fbe7ad8 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:27:09 +0200 Subject: [PATCH 091/151] added roxy imports --- NAMESPACE | 3 +-- R/mask.R | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 7070fd1b..86d000b5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -93,14 +93,13 @@ importFrom(DelayedArray,ConstantArray) importFrom(DelayedArray,DelayedArray) importFrom(DelayedArray,cbind) importFrom(DelayedArray,rbind) -importFrom(DelayedArray,realize) importFrom(Matrix,rowSums) +importFrom(Matrix,sparseMatrix) importFrom(Matrix,sparseVector) importFrom(Matrix,t) importFrom(RBGL,sp.between) importFrom(Rarr,read_zarr_attributes) importFrom(Rarr,zarr_overview) -importFrom(S4Arrays,as.array.Array) importFrom(S4Vectors,"metadata<-") importFrom(S4Vectors,coolcat) importFrom(S4Vectors,isSequence) diff --git a/R/mask.R b/R/mask.R index 5a93763d..2f3038d0 100644 --- a/R/mask.R +++ b/R/mask.R @@ -74,8 +74,7 @@ setGeneric(".mask", \(i, j, ...) standardGeneric(".mask")) #' @noRd #' @importFrom methods as -#' @importFrom DelayedArray realize -#' @importFrom S4Arrays as.array.Array +#' @importFrom Matrix sparseVector #' @importFrom SummarizedExperiment assayNames<- #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { @@ -96,7 +95,7 @@ setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { #' @noRd #' @importFrom methods as -#' @importFrom Matrix rowSums sparseVector t +#' @importFrom Matrix t rowSums sparseVector sparseMatrix #' @importFrom SingleCellExperiment SingleCellExperiment #' @importFrom sf st_as_sf st_geometry_type st_distance setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { From 8505a1f7a8c43676a082d648d4017a4c97e0f95e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:28:13 +0200 Subject: [PATCH 092/151] rephrase comment --- R/mask.R | 2 +- man/mask.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/mask.R b/R/mask.R index 2f3038d0..f0a9de7f 100644 --- a/R/mask.R +++ b/R/mask.R @@ -35,7 +35,7 @@ #' x <- get_demo_SDdata("merfish") #' x <- readSpatialData(x) #' -#' # sum assay data from table by shapes +#' # sum table counts by shapes #' y <- mask(x, "cells", "anatomical") #' tail(tables(y), 1) #' diff --git a/man/mask.Rd b/man/mask.Rd index 6e960757..9c248c12 100644 --- a/man/mask.Rd +++ b/man/mask.Rd @@ -54,7 +54,7 @@ library(SpatialData.data) x <- get_demo_SDdata("merfish") x <- readSpatialData(x) -# sum assay data from table by shapes +# sum table counts by shapes y <- mask(x, "cells", "anatomical") tail(tables(y), 1) From febdf4c738474e1c1e0f11527102ce0f642913ee Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:33:06 +0200 Subject: [PATCH 093/151] bug fix: check for row-wise duplicates to assure polygon vertices are unique --- R/AllGenerics.R | 4 ---- R/mask.R | 6 ++++-- R/query.R | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 5c9f6541..c92b0aa0 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -57,10 +57,6 @@ setGeneric("rotate", \(x, t, ...) standardGeneric("rotate")) setGeneric("transform", \(x, ...) standardGeneric("transform")) setGeneric("translation", \(x, t, ...) standardGeneric("translation")) -# sda ---- - -setGeneric("feature_key", \(x, ...) standardGeneric("feature_key")) - # uts ---- setGeneric("layer", \(x, i, ...) standardGeneric("layer")) diff --git a/R/mask.R b/R/mask.R index f0a9de7f..dc64dec6 100644 --- a/R/mask.R +++ b/R/mask.R @@ -57,9 +57,11 @@ setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, nm <- if (is.function(name)) name(i, j) else if (ok) name else stop( "Invalid 'name'; should be a function or a ", "character string not yet in 'tableNames(x)'") - t <- tryCatch(error=\(.) NULL, getTable(x, i)) f <- \(i) names(which(rapply(colnames(x), \(.) i %in% ., "character"))) - se <- .mask(i=element(x, f(i), i), j=element(x, f(j), j), how=how, table=t, ...) + .i <- element(x, f(i), i) + .j <- element(x, f(j), j) + t <- tryCatch(error=\(.) NULL, getTable(x, i)) + se <- .mask(.i, .j, how=how, table=t, ...) md <- list(region=j, region_key="region", instance_key="instance") int_metadata(se)$spatialdata_attrs <- md assay(se) <- as(assay(se), "dgCMatrix") diff --git a/R/query.R b/R/query.R index 2630d05d..4a84d1ed 100644 --- a/R/query.R +++ b/R/query.R @@ -79,8 +79,8 @@ NULL bot <- mx[nrow(mx), ] if (!all(top == bot)) mx <- rbind(mx, top) - dup <- any(duplicated(mx[-1, ])) - if (dup) stop("Invalid polygon query; found duplicated vertices") + dup <- duplicated(as.data.frame(mx[-1, , drop=FALSE])) + if (any(dup)) stop("Invalid polygon query; found duplicated vertices") return(mx) } From 73539d9ef2a5db3ce25fdc80239cbfc0f376c8b0 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Wed, 8 Apr 2026 14:34:23 +0200 Subject: [PATCH 094/151] fix method export --- NAMESPACE | 1 + R/AllGenerics.R | 4 ++++ R/Zattrs.R | 9 ++++++++- man/Zattrs.Rd | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 86d000b5..3cc8f38f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -52,6 +52,7 @@ exportMethods(data) exportMethods(data_type) exportMethods(dim) exportMethods(element) +exportMethods(feature_key) exportMethods(getTable) exportMethods(hasTable) exportMethods(image) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index c92b0aa0..5c9f6541 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -57,6 +57,10 @@ setGeneric("rotate", \(x, t, ...) standardGeneric("rotate")) setGeneric("transform", \(x, ...) standardGeneric("transform")) setGeneric("translation", \(x, t, ...) standardGeneric("translation")) +# sda ---- + +setGeneric("feature_key", \(x, ...) standardGeneric("feature_key")) + # uts ---- setGeneric("layer", \(x, i, ...) standardGeneric("layer")) diff --git a/R/Zattrs.R b/R/Zattrs.R index dc42e53d..d21bec29 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -1,6 +1,8 @@ #' @name Zattrs #' @title The `Zattrs` class -#' +#' +#' @aliases feature_key +#' #' @param x list extracted from a OME-NGFF compliant .zattrs file. #' @param name character string for extraction (see ?base::`$`). #' @@ -16,6 +18,8 @@ #' CTname(z) #' CTtype(z) #' CTdata(z, "scale") +#' +#' feature_key(point(x)) #' #' @export Zattrs <- \(x=list()) { @@ -76,5 +80,8 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) } setMethod("show", "Zattrs", .showZattrs) +#' @export setMethod("feature_key", "Zattrs", \(x) x$spatialdata_attrs$feature_key) + +#' @export setMethod("feature_key", "SpatialDataElement", \(x) feature_key(meta(x))) \ No newline at end of file diff --git a/man/Zattrs.Rd b/man/Zattrs.Rd index 366844b1..06ddea16 100644 --- a/man/Zattrs.Rd +++ b/man/Zattrs.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/Zattrs.R \name{Zattrs} \alias{Zattrs} +\alias{feature_key} \alias{$,Zattrs-method} \title{The `Zattrs` class} \usage{ @@ -31,4 +32,6 @@ CTname(z) CTtype(z) CTdata(z, "scale") +feature_key(point(x)) + } From 5ba105587c365aa2e5a5177c63bf327edc488c1c Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 09:37:39 +0200 Subject: [PATCH 095/151] assure bb is within bounds; add tests of query,labelArray; fractorize image/label query --- R/query.R | 46 +++++++++++++++++++++---------------- tests/testthat/test-query.R | 33 +++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/R/query.R b/R/query.R index 4a84d1ed..135d139c 100644 --- a/R/query.R +++ b/R/query.R @@ -109,31 +109,37 @@ setMethod("query", "SpatialData", \(x, ..., i) { return(x) }) -#' @rdname query -#' @export -setMethod("query", "ImageArray", \(x, y) { - if (is.matrix(y)) stop("Polygon query not supported for images") +.query_sdArray <- \(x, y) { + if (is.matrix(y)) stop( + "Polygon query not supported for ", + "element of type 'image/labelArray'") .check_box(y) - d <- dim(x) - y$ymax <- min(y$ymax, d[2]) - y$xmax <- min(y$xmax, d[3]) - j <- seq(y$ymin, y$ymax) - k <- seq(y$xmin, y$xmax) - return(x[, j, k]) -}) - -#' @rdname query -#' @export -setMethod("query", "LabelArray", \(x, y) { - if (is.matrix(y)) stop("Polygon query not supported for labels") - .check_box(y) - d <- dim(x) + # protect image channels (i.e., + # only query spatial dimensions) + n <- length(d <- dim(x)) + if (n == 3) d <- d[-1] + # assure query is within bounds + y$xmin <- max(y$xmin, 0) + y$ymin <- max(y$ymin, 0) y$ymax <- min(y$ymax, d[1]) y$xmax <- min(y$xmax, d[2]) + # subset spatial dimensions i <- seq(y$ymin, y$ymax) j <- seq(y$xmin, y$xmax) - return(x[i, j]) -}) + if (n == 3) { + return(x[, i, j]) + } else { + return(x[i, j]) + } +} + +#' @rdname query +#' @export +setMethod("query", "ImageArray", \(x, y) .query_sdArray(x, y)) + +#' @rdname query +#' @export +setMethod("query", "LabelArray", \(x, y) .query_sdArray(x, y)) #' @rdname query #' @importFrom sf st_as_sf st_intersects st_polygon st_bbox st_crop diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 2c161788..f6ac2633 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -38,7 +38,7 @@ test_that("query,.check_pol", { test_that("query,ImageArray", { d <- dim(i <- image(x)) - # neither crop nor shift + # query equals dimensions y <- list(xmin=0, xmax=d[3], ymin=0, ymax=d[2]) expect_identical(query(i, y), i) # order is irrelevant @@ -49,8 +49,35 @@ test_that("query,ImageArray", { expect_equal(dim(j <- query(i, y)), c(3, h, w)) expect_identical(CTlist(i), CTlist(j)) # crop and shift - y <- list(xmin=1, xmax=w <- d[3]/2, ymin=2, ymax=h <- d[2]/4) - expect_equal(dim(query(i, y)), c(3, 1+h-2, 1+w-1)) + y <- list( + xmin=dx <- 3, xmax=w <- d[3]/2, + ymin=dy <- 5, ymax=h <- d[2]/4) + expect_equal(dim(query(i, y)), c(3, 1+h-dy, 1+w-dx)) + # non-finite boundaries + y <- list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) + expect_silent(query(i, y)) +}) + +test_that("query,LabelArray", { + d <- dim(l <- label(x)) + # query equals dimensions + y <- list(xmin=0, xmax=d[2], ymin=0, ymax=d[1]) + expect_identical(query(l, y), l) + # order is irrelevant + y <- list(ymax=d[1], xmax=d[2], xmin=0, ymin=0) + expect_identical(query(l, y), l) + # crop but don't shift + y <- list(xmin=0, xmax=w <- d[2]/2, ymin=0, ymax=h <- d[1]/4) + expect_equal(dim(m <- query(l, y)), c(h, w)) + expect_identical(CTlist(l), CTlist(m)) + # crop and shift + y <- list( + xmin=dx <- 3, xmax=w <- d[2]/2, + ymin=dy <- 5, ymax=h <- d[1]/4) + expect_equal(dim(query(l, y)), c(1+h-dy, 1+w-dx)) + # non-finite boundaries + y <- list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) + expect_silent(query(i, y)) }) test_that("query-box,PointFrame", { From 1c147d706b8e526273208e68f737711601faaebf Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 09:47:43 +0200 Subject: [PATCH 096/151] code rearrangement; fix typo --- R/query.R | 50 ++++++++++++++++++------------------- tests/testthat/test-query.R | 8 +++--- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/R/query.R b/R/query.R index 135d139c..f5fa6bd3 100644 --- a/R/query.R +++ b/R/query.R @@ -55,6 +55,31 @@ #' foo <- by(fd, fd[, "L2"], \(x) points(x, type="b", col="red")) NULL +#' @rdname query +#' @importFrom dplyr filter +#' @export +setMethod("query", "SpatialData", \(x, ..., i) { + # TODO: need more example data to properly implement this; + # for now, just a proof of concept using 'spatialdata_attrs' + if (missing(i)) i <- 1 + if (!length(tables(x))) + stop("There aren't any tables") + if (is.numeric(i)) { + i <- tableNames(x)[i] + } else if (is.character(i)) { + i <- match.arg(i, tableNames(x)) + } + t <- x$tables[[i]] + ns <- vapply(nm <- colnames(x), length, integer(1)) + nm <- data.frame(layer=rep.int(names(nm), ns), region=unlist(nm)) + nm <- filter(nm, ...) + i <- match(nm$layer, .LAYERS) + j <- split(nm$region, nm$layer) + x <- x[i, j] + x$tables$table <- t + return(x) +}) + .check_box <- \(bb) { xy <- c("xmin", "xmax", "ymin", "ymax") ok <- c(is.list(bb), @@ -84,31 +109,6 @@ NULL return(mx) } -#' @rdname query -#' @importFrom dplyr filter -#' @export -setMethod("query", "SpatialData", \(x, ..., i) { - # TODO: need more example data to properly implement this; - # for now, just a proof of concept using 'spatialdata_attrs' - if (missing(i)) i <- 1 - if (!length(tables(x))) - stop("There aren't any tables") - if (is.numeric(i)) { - i <- tableNames(x)[i] - } else if (is.character(i)) { - i <- match.arg(i, tableNames(x)) - } - t <- x$tables[[i]] - ns <- vapply(nm <- colnames(x), length, integer(1)) - nm <- data.frame(layer=rep.int(names(nm), ns), region=unlist(nm)) - nm <- filter(nm, ...) - i <- match(nm$layer, .LAYERS) - j <- split(nm$region, nm$layer) - x <- x[i, j] - x$tables$table <- t - return(x) -}) - .query_sdArray <- \(x, y) { if (is.matrix(y)) stop( "Polygon query not supported for ", diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index f6ac2633..7964b12a 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -29,10 +29,10 @@ test_that("query,.check_pol", { for (. in q) expect_silent(.check_pol(.)) # invalid q <- list( - `[<-`(m, i=1, j=1, value=NA), # missing value - `[<-`(m, i=1, j=1, value=Inf), # not finite + matrix(seq_len(6), 2, 3), # wrong dim. matrix(numeric(6), 3, 2), # duplicates - matrix(seq_len(6), 2, 3)) # wrong dim. + `[<-`(m, i=1, j=1, value=Inf), # not finite + `[<-`(m, i=1, j=1, value=NA)) # missing value for (. in q) expect_error(.check_pol(.)) }) @@ -77,7 +77,7 @@ test_that("query,LabelArray", { expect_equal(dim(query(l, y)), c(1+h-dy, 1+w-dx)) # non-finite boundaries y <- list(xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) - expect_silent(query(i, y)) + expect_silent(query(l, y)) }) test_that("query-box,PointFrame", { From 2c48a058d68fc7d00b6675c201f3f5c596bf9955 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 11:10:06 +0200 Subject: [PATCH 097/151] scuttle -> scrapper --- DESCRIPTION | 2 +- NAMESPACE | 2 +- R/mask.R | 17 ++++++++--------- tests/testthat/test-mask.R | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index cbaf5789..a694c81c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -47,7 +47,7 @@ Imports: RBGL, reticulate, anndataR, - scuttle, + scrapper, sf, S4Arrays, S4Vectors, diff --git a/NAMESPACE b/NAMESPACE index 3cc8f38f..4441913d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -150,7 +150,7 @@ importFrom(methods,new) importFrom(methods,setClassUnion) importFrom(methods,setReplaceMethod) importFrom(reticulate,import) -importFrom(scuttle,aggregateAcrossCells) +importFrom(scrapper,aggregateAcrossCells.se) importFrom(sf,"st_geometry<-") importFrom(sf,st_as_sf) importFrom(sf,st_bbox) diff --git a/R/mask.R b/R/mask.R index dc64dec6..62881d10 100644 --- a/R/mask.R +++ b/R/mask.R @@ -126,29 +126,28 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { }) #' @noRd -#' @importFrom scuttle aggregateAcrossCells +#' @importFrom methods as +#' @importFrom scrapper aggregateAcrossCells.se #' @importFrom SummarizedExperiment assayNames<- #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { if (is.null(table)) stop("Missing 'table'; can't mask shapes without") ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") - if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } + if (!is.null(how)) { message("Can only 'sum' when masking shapes; ignoring 'sum'") } idx <- st_intersects( st_as_sf(data(j)), st_as_sf(data(i))) foo <- integer(nrow(i)) foo[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) - se <- aggregateAcrossCells(table, - ids=foo, subset.row=value, - statistics=how, use.assay.type=1, - store_number=paste0("n_", meta(table)$region)) + se <- aggregateAcrossCells.se(table, foo, assay.type=1, + counts.name=paste0("n_", meta(table)$region)) colnames(se) <- se[[1]]; se[[1]] <- NULL - assayNames(se) <- how - return(se) + assayNames(se)[1] <- "counts" + metadata(se) <- list() + as(se, "SingleCellExperiment") }) #' @noRd setMethod(".mask", c("ANY", "ANY"), \(i, j, ...) stop("'mask'ing between these element types not yet supported")) - diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index 1eebe0d0..f69b34ec 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -50,8 +50,8 @@ test_that("mask,ShapeFrame,ShapeFrame", { y <- x; tables(y) <- list() expect_error(mask(y, i, j)) # default to 'sum' with a message - expect_message(y <- mask(x, i, j)) - expect_silent(z <- mask(x, i, j, how="sum")) + expect_silent(y <- mask(x, i, j)) + expect_message(z <- mask(x, i, j, how="sum")) expect_identical(y, z) old <- getTable(y, i) new <- getTable(y, j) From fa2b29e8b5f24608ddb9876ebaaadf422c33ccca Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 11:52:41 +0200 Subject: [PATCH 098/151] omit deps by aggregating via mtx mul --- DESCRIPTION | 1 - NAMESPACE | 1 - R/mask.R | 45 ++++++++++++++++++++++++++++---------- tests/testthat/test-mask.R | 4 ++-- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a694c81c..31e61414 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -47,7 +47,6 @@ Imports: RBGL, reticulate, anndataR, - scrapper, sf, S4Arrays, S4Vectors, diff --git a/NAMESPACE b/NAMESPACE index 4441913d..9fafee78 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -150,7 +150,6 @@ importFrom(methods,new) importFrom(methods,setClassUnion) importFrom(methods,setReplaceMethod) importFrom(reticulate,import) -importFrom(scrapper,aggregateAcrossCells.se) importFrom(sf,"st_geometry<-") importFrom(sf,st_as_sf) importFrom(sf,st_bbox) diff --git a/R/mask.R b/R/mask.R index 62881d10..2e59d630 100644 --- a/R/mask.R +++ b/R/mask.R @@ -52,7 +52,7 @@ NULL setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, how=NULL, name=\(i, j) sprintf("%s_by_%s", i, j), ...) { .check_ij(x, i); .check_ij(x, j) - if (!is.null(how)) how <- match.arg(how, c("sum", "mean")) + #if (!is.null(how)) how <- match.arg(how, c("sum", "mean")) ok <- is.character(name) && length(name) == 1 && !name %in% tableNames(x) nm <- if (is.function(name)) name(i, j) else if (ok) name else stop( "Invalid 'name'; should be a function or a ", @@ -127,25 +127,46 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { #' @noRd #' @importFrom methods as -#' @importFrom scrapper aggregateAcrossCells.se -#' @importFrom SummarizedExperiment assayNames<- +#' @importFrom Matrix sparseMatrix +#' @importFrom SummarizedExperiment assay #' @importFrom SingleCellExperiment SingleCellExperiment setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { + # validity if (is.null(table)) stop("Missing 'table'; can't mask shapes without") ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") - if (!is.null(how)) { message("Can only 'sum' when masking shapes; ignoring 'sum'") } + if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } + if (is.character(how)) how <- match.arg(how, c("sum", "mean", "detected", "prop.detected")) + # grouping idx <- st_intersects( st_as_sf(data(j)), st_as_sf(data(i))) - foo <- integer(nrow(i)) - foo[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) - se <- aggregateAcrossCells.se(table, foo, assay.type=1, - counts.name=paste0("n_", meta(table)$region)) - colnames(se) <- se[[1]]; se[[1]] <- NULL - assayNames(se)[1] <- "counts" - metadata(se) <- list() - as(se, "SingleCellExperiment") + ids <- integer(nrow(i)) + ids[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) + ids <- factor(ids, seq(0, nrow(j))) + nid <- nlevels(ids) + # aggregation + mx <- assay(table) + if (grepl("detected$", how)) { + mx <- mx > 0 + } + my <- sparseMatrix( + x=rep(1, length(ids)), + i=seq_along(ids), j=ids, + dims=c(ncol(table), nid)) + mx <- mx %*% my + if (grepl("mean|prop", how)) { + ns <- tabulate(ids, nid) + mx <- t(t(mx)/ns) + } + # wrangling + mx <- as(mx, "dgCMatrix") + colnames(mx) <- levels(ids) + mx <- list(mx); names(mx) <- how + se <- SingleCellExperiment(mx) + nm <- paste0("n_", meta(table)$region) + se[[nm]] <- ns + return(se) }) #' @noRd diff --git a/tests/testthat/test-mask.R b/tests/testthat/test-mask.R index f69b34ec..1eebe0d0 100644 --- a/tests/testthat/test-mask.R +++ b/tests/testthat/test-mask.R @@ -50,8 +50,8 @@ test_that("mask,ShapeFrame,ShapeFrame", { y <- x; tables(y) <- list() expect_error(mask(y, i, j)) # default to 'sum' with a message - expect_silent(y <- mask(x, i, j)) - expect_message(z <- mask(x, i, j, how="sum")) + expect_message(y <- mask(x, i, j)) + expect_silent(z <- mask(x, i, j, how="sum")) expect_identical(y, z) old <- getTable(y, i) new <- getTable(y, j) From 6847822ded2585d481ca2c876df5c93064c49542 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 12:29:51 +0200 Subject: [PATCH 099/151] bug fix; code cleaning --- R/mask.R | 29 +++++++++++------------------ man/SpatialData.Rd | 8 ++++---- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/R/mask.R b/R/mask.R index 2e59d630..96c73473 100644 --- a/R/mask.R +++ b/R/mask.R @@ -138,30 +138,23 @@ setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } if (is.character(how)) how <- match.arg(how, c("sum", "mean", "detected", "prop.detected")) # grouping - idx <- st_intersects( - st_as_sf(data(j)), - st_as_sf(data(i))) - ids <- integer(nrow(i)) - ids[unlist(idx)] <- rep(seq_along(idx), lengths(idx)) - ids <- factor(ids, seq(0, nrow(j))) - nid <- nlevels(ids) + js <- st_intersects(st_as_sf(data(j)), st_as_sf(data(i))) + is <- factor(integer(nrow(i)), seq(0, nrow(j))) + is[unlist(js)] <- rep(seq_along(js), lengths(js)) + is <- factor(is, seq(0, nrow(j))) + ns <- tabulate(is, ni <- nlevels(is)) # aggregation mx <- assay(table) - if (grepl("detected$", how)) { - mx <- mx > 0 - } + if (grepl("detected$", how)) mx <- mx > 0 my <- sparseMatrix( - x=rep(1, length(ids)), - i=seq_along(ids), j=ids, - dims=c(ncol(table), nid)) + x=rep(1, length(is)), + i=seq_along(is), j=is, + dims=c(ncol(table), ni)) mx <- mx %*% my - if (grepl("mean|prop", how)) { - ns <- tabulate(ids, nid) - mx <- t(t(mx)/ns) - } + if (grepl("mean|prop", how)) mx <- t(t(mx)/ns) # wrangling mx <- as(mx, "dgCMatrix") - colnames(mx) <- levels(ids) + colnames(mx) <- levels(is) mx <- list(mx); names(mx) <- how se <- SingleCellExperiment(mx) nm <- paste0("n_", meta(table)$region) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From 60e0ddb891eadfe86f2a3a355172f5fcabf7d256 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 12:30:13 +0200 Subject: [PATCH 100/151] remove duplicated factoring --- R/mask.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/mask.R b/R/mask.R index 96c73473..a755989e 100644 --- a/R/mask.R +++ b/R/mask.R @@ -141,7 +141,6 @@ setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL js <- st_intersects(st_as_sf(data(j)), st_as_sf(data(i))) is <- factor(integer(nrow(i)), seq(0, nrow(j))) is[unlist(js)] <- rep(seq_along(js), lengths(js)) - is <- factor(is, seq(0, nrow(j))) ns <- tabulate(is, ni <- nlevels(is)) # aggregation mx <- assay(table) From 0fd9fe3aaaa7e454c643123f42dd812a1c3408c3 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 12:39:07 +0200 Subject: [PATCH 101/151] expose 'assay' arg --- R/mask.R | 4 ++-- man/SpatialData.Rd | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/mask.R b/R/mask.R index a755989e..7a6658a4 100644 --- a/R/mask.R +++ b/R/mask.R @@ -130,7 +130,7 @@ setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { #' @importFrom Matrix sparseMatrix #' @importFrom SummarizedExperiment assay #' @importFrom SingleCellExperiment SingleCellExperiment -setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL, how=NULL, ...) { +setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, how=NULL, table=NULL, value=NULL, assay=1, ...) { # validity if (is.null(table)) stop("Missing 'table'; can't mask shapes without") ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) @@ -143,7 +143,7 @@ setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, table=NULL, value=NULL is[unlist(js)] <- rep(seq_along(js), lengths(js)) ns <- tabulate(is, ni <- nlevels(is)) # aggregation - mx <- assay(table) + mx <- assay(table, assay) if (grepl("detected$", how)) mx <- mx > 0 my <- sparseMatrix( x=rep(1, length(is)), diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From 7086ec2dba3c2c25bd957c1cd8ba3d713d12e63e Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 14:03:13 +0200 Subject: [PATCH 102/151] reorganize code into split scripts/docs --- NAMESPACE | 2 +- R/CTgraph.R | 168 ++++++++++++++++++++++++++++++++++ R/coord_utils.R | 171 +---------------------------------- R/trans.R | 1 - man/CTgraph.Rd | 63 +++++++++++++ man/coord-utils.Rd | 31 +------ man/plotCoordGraph.Rd | 26 ------ tests/testthat/test-zattrs.R | 76 +++++++++++++++- vignettes/SpatialData.Rmd | 2 +- 9 files changed, 310 insertions(+), 230 deletions(-) create mode 100644 R/CTgraph.R create mode 100644 man/CTgraph.Rd delete mode 100644 man/plotCoordGraph.Rd diff --git a/NAMESPACE b/NAMESPACE index 9fafee78..efbfdd9f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,7 @@ S3method(.DollarNames,Zattrs) S3method(filter,PointFrame) S3method(select,PointFrame) export(.SpatialData) +export(CTplot) export(ImageArray) export(LabelArray) export(PointFrame) @@ -15,7 +16,6 @@ export(SpatialData) export(Zattrs) export(do_tx_to_ext) export(mask) -export(plotCoordGraph) export(readImage) export(readLabel) export(readPoint) diff --git a/R/CTgraph.R b/R/CTgraph.R new file mode 100644 index 00000000..ee47ebd3 --- /dev/null +++ b/R/CTgraph.R @@ -0,0 +1,168 @@ +#' @name CTgraph +#' @title Coord. trans. graph +#' @aliases CTgraph CTpath CTplot +#' +#' @param x \code{SpatialData}, an element, or \code{Zattrs}. +#' @param i character string; name of source node. +#' @param j character string; name of target coordinate space. +#' @param g base R graph; extracted with \code{CTgraph}. +#' @param cex scalar numeric; controls fontsize of node labels. +#' @param fac,max scalar numeric; node labels with \code{nchar>max} +#' are split and hyphenated at position \code{floor(nchar/fac)} +#' +#' @examples +#' x <- file.path("extdata", "blobs.zarr") +#' x <- system.file(x, package="SpatialData") +#' x <- readSpatialData(x, tables=FALSE) +#' +#' # object-wide +#' g <- CTgraph(x) +#' CTplot(g) +#' +#' # one element +#' y <- label(x) +#' g <- CTgraph(y) +#' CTplot(g) +#' +#' # retrieve transformation(s) +#' # from element to target space +#' CTpath(x, "blobs_labels", "sequence") +NULL + +#' @rdname CTgraph +#' @export +setMethod("CTgraph", "SpatialData", \(x) { + names(ls) <- ls <- setdiff(.LAYERS, "tables") + md <- lapply(ls, \(l) { + names(es) <- es <- names(x[[l]]) + lapply(es, \(e) meta(x[[l]][[e]])) + }) + .make_g(md) +}) + +#' @rdname CTgraph +#' @export +setMethod("CTgraph", "SpatialDataElement", \(x) + .make_g(list("mock"=list("self"=meta(x))))) + +#' @rdname CTgraph +#' @export +setMethod("CTgraph", "ANY", \(x) stop("'x' should be a", + " 'SpatialData' object, or a non-'table' element")) + +#' @importFrom graph graphAM nodeDataDefaults<- edgeDataDefaults<- +.init_g <- \() { + g <- graphAM(edgemode="directed") + edgeDataDefaults(g, "data") <- list() + edgeDataDefaults(g, "type") <- character() + nodeDataDefaults(g, "type") <- character() + return(g) +} + +#' @importFrom graph nodes addNode addEdge nodeData<- edgeData<- +.make_g <- \(md) { + g <- .init_g() + for (l in names(md)) for (e in names(md[[l]])) { + .md <- md[[l]][[e]] + ms <- .md$multiscales + if (!is.null(ms)) .md <- ms[[1]] + ct <- .md$coordinateTransformations + g <- addNode(e, g) + nodeData(g, e, "type") <- "element" + for (i in seq_along(ct)) { + n <- ct[[i]]$output$name + if (!n %in% nodes(g)) { + g <- addNode(n, g) + nodeData(g, n, "type") <- "space" + } + t <- ct[[i]]$type + if (t == "sequence") { + sq <- ct[[i]]$transformations + . <- e + for (j in seq_along(sq)) { + if (j == length(sq)) { + m <- n + } else { + m <- paste(e, n, j, sep="_") + g <- addNode(m, g) + nodeData(g, m, "type") <- "none" + } + t <- sq[[j]]$type + d <- sq[[j]][[t]] + g <- addEdge(., m, g) + edgeData(g, ., m, "type") <- t + edgeData(g, ., m, "data") <- list(d) + . <- m + } + } else { + g <- addEdge(e, n, g) + d <- ct[[i]][[ct[[i]]$type]] + edgeData(g, e, n, "type") <- t + edgeData(g, e, n, "data") <- list(d) + } + } + } + return(g) +} + +# path ---- + +#' @importFrom graph edgeData +#' @importFrom RBGL sp.between +.path_ij <- \(g, i, j) { + p <- sp.between(g, i, j) + p <- p[[1]]$path_detail + n <- length(p)-1 + lapply(seq_len(n), \(.) edgeData(g, p[.], p[.+1])[[1]]) +} + +#' @rdname CTgraph +#' @export +setMethod("CTpath", "SpatialData", \(x, i, j) { + g <- CTgraph(x) + .path_ij(g, i, j) +}) + +#' @rdname CTgraph +#' @export +setMethod("CTpath", "SpatialDataElement", \(x, j) { + g <- CTgraph(x) + .path_ij(g, "self", j) +}) + +#' @rdname CTgraph +#' @export +setMethod("CTpath", "ANY", \(x) stop("'x' should be a", + " 'SpatialData' object, or a non-'table' element")) + +# plot ---- + +#' @importFrom graph nodes nodes<- graph.par +#' @rdname CTgraph +#' @export +CTplot <- \(g, cex=0.5, fac=2, max=10) { + if (!requireNamespace("Rgraphviz", quietly=TRUE)) + stop("Install 'Rgraphviz' to use this function") + g2view <- g # leave 'g' alone + nodes(g2view) <- .nodefix(nodes(g2view), fac=fac, max=max) + graph.par(list(nodes=list(shape="plaintext", cex=cex))) + g2view <- Rgraphviz::layoutGraph(g2view) + Rgraphviz::renderGraph(g2view) +} + +.nodefix <- \(x, fac=2, max=10) { + fix <- nchar(x) > max + if (!any(fix)) return(x) + x[fix] <- .fixup(x[fix], fac) + x +} + +.fixup <- \(x, fac) { + xs <- strsplit(x, "") + nc <- floor(nchar(x)/fac) + vapply(seq_along(xs), \(i) { + j <- seq_len(nc[i]) + y <- c(xs[[i]][j], "-\n", xs[[i]][-j]) + paste(y, collapse="") + }, character(1)) +} diff --git a/R/coord_utils.R b/R/coord_utils.R index 440ad77c..0bff61da 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -1,6 +1,6 @@ #' @name coord-utils #' @title Coordinate transformations -#' @aliases axes CTlist CTname CTtype CTdata CTpath CTgraph addCT rmvCT +#' @aliases axes CTlist CTname CTtype CTdata addCT rmvCT #' #' @param x \code{SpatialData}, an element, or \code{Zattrs}. #' @param i for \code{CTpath}, source node label; else, string or @@ -17,18 +17,6 @@ #' x <- system.file(x, package="SpatialData") #' x <- readSpatialData(x, tables=FALSE) #' -#' # element-wise -#' g <- CTgraph(y <- image(x)) -#' graph::nodes(g) -#' CTpath(y, "global") -#' -#' # object-wide -#' g <- CTgraph(x) -#' plotCoordGraph(g) -#' -#' # retrieve transformation from element to target space -#' CTpath(x, "blobs_labels", "sequence") -#' #' # view available target coordinate systems #' CTname(z <- meta(label(x))) #' @@ -80,11 +68,11 @@ setMethod("CTdata", "Zattrs", \(x, i=1, ...) { if (is.character(i)) { match.arg(i, CTname(x)) i <- match(i, CTname(x)) - } else { + } else if (is.numeric(i)) { stopifnot( i == round(i), i %in% seq_along(CTlist(x))) - } + } else stop("Invlid 'i'; should be a scalar character or integer") t <- CTtype(x)[i] if (t != "sequence") return(CTlist(x)[[i]][[t]]) @@ -125,110 +113,6 @@ setMethod("CTname", "SpatialData", \(x, ...) { names(t)[unlist(t) == "space"] }) -# CTgraph() ---- - -#' @rdname coord-utils -#' @export -setMethod("CTgraph", "SpatialData", \(x) { - names(ls) <- ls <- setdiff(.LAYERS, "tables") - md <- lapply(ls, \(l) { - names(es) <- es <- names(x[[l]]) - lapply(es, \(e) meta(x[[l]][[e]])) - }) - .make_g(md) -}) - -#' @rdname coord-utils -#' @export -setMethod("CTgraph", "SpatialDataElement", \(x) - .make_g(list("mock"=list("self"=meta(x))))) - -#' @rdname coord-utils -#' @export -setMethod("CTgraph", "ANY", \(x) stop("'x' should be a", - " 'SpatialData' object, or a non-'table' element")) - -#' @importFrom graph graphAM nodeDataDefaults<- edgeDataDefaults<- -.init_g <- \() { - g <- graphAM(edgemode="directed") - edgeDataDefaults(g, "data") <- list() - edgeDataDefaults(g, "type") <- character() - nodeDataDefaults(g, "type") <- character() - return(g) -} - -#' @importFrom graph nodes addNode addEdge nodeData<- edgeData<- -.make_g <- \(md) { - g <- .init_g() - for (l in names(md)) for (e in names(md[[l]])) { - .md <- md[[l]][[e]] - ms <- .md$multiscales - if (!is.null(ms)) .md <- ms[[1]] - ct <- .md$coordinateTransformations - g <- addNode(e, g) - nodeData(g, e, "type") <- "element" - for (i in seq_along(ct)) { - n <- ct[[i]]$output$name - if (!n %in% nodes(g)) { - g <- addNode(n, g) - nodeData(g, n, "type") <- "space" - } - t <- ct[[i]]$type - if (t == "sequence") { - sq <- ct[[i]]$transformations - . <- e - for (j in seq_along(sq)) { - if (j == length(sq)) { - m <- n - } else { - m <- paste(e, n, j, sep="_") - g <- addNode(m, g) - nodeData(g, m, "type") <- "none" - } - t <- sq[[j]]$type - d <- sq[[j]][[t]] - g <- addEdge(., m, g) - edgeData(g, ., m, "type") <- t - edgeData(g, ., m, "data") <- list(d) - . <- m - } - } else { - g <- addEdge(e, n, g) - d <- ct[[i]][[ct[[i]]$type]] - edgeData(g, e, n, "type") <- t - edgeData(g, e, n, "data") <- list(d) - } - } - } - return(g) -} - -# CTpath() ---- - -#' @rdname coord-utils -#' @export -setMethod("CTpath", "SpatialData", \(x, i, j) { - g <- CTgraph(x) - .path_ij(g, i, j) -}) - -#' @rdname coord-utils -#' @export -setMethod("CTpath", "SpatialDataElement", \(x, j) { - g <- CTgraph(x) - .path_ij(g, "self", j) -}) - -#' @importFrom graph edgeData -#' @importFrom RBGL sp.between -.path_ij <- \(g, i, j) { - p <- sp.between(g, i, j) - p <- p[[1]]$path_detail - n <- length(p)-1 - lapply(seq_len(n), \(.) - edgeData(g, p[.], p[.+1])[[1]]) -} - # rmv ---- #' @rdname coord-utils @@ -339,52 +223,3 @@ setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { # # @importFrom EBImage resize # setMethod("translation", "ImageArray", \(x, t, ...) {}) # setMethod("transform", "ImageArray", \(x, t) get(t$type)(x, unlist(t[[t$type]]))) - -# plotCoordGraph() ---- - -#' @name plotCoordGraph -#' @title CT graph viz. -#' -#' @description -#' given a \code{graphNEL} instance, nodes with \code{nchar>max} are -#' split and hyphenated at character position \code{floor(nchar/fac)}. -#' -#' @param g base R graph; extracted with \code{\link{CTgraph}}. -#' @param cex scalar numeric; controls fontsize of node labels. -#' -#' @examples -#' x <- file.path("extdata", "blobs.zarr") -#' x <- system.file(x, package="SpatialData") -#' x <- readSpatialData(x, tables=FALSE) -#' -#' g <- CTgraph(x) -#' plotCoordGraph(g, cex=0.6) -#' -#' @importFrom graph nodes nodes<- graph.par -#' @export -plotCoordGraph <- \(g, cex=0.6) { - if (!requireNamespace("Rgraphviz", quietly=TRUE)) - stop("To use this function, install the 'Rgraphviz'.") - g2view <- g # leave 'g' alone - nodes(g2view) <- .nodefix(nodes(g2view)) - graph.par(list(nodes=list(shape="plaintext", cex=cex))) - g2view <- Rgraphviz::layoutGraph(g2view) - Rgraphviz::renderGraph(g2view) -} - -.nodefix <- \(x, fac=2, max=10) { - fix <- nchar(x) > max - if (!any(fix)) return(x) - x[fix] <- .fixup(x[fix], fac) - x -} - -.fixup <- \(x, fac) { - xs <- strsplit(x, "") - nc <- floor(nchar(x)/fac) - vapply(seq_along(xs), \(i) { - j <- seq_len(nc[i]) - y <- c(xs[[i]][j], "-\n", xs[[i]][-j]) - paste(y, collapse="") - }, character(1)) -} diff --git a/R/trans.R b/R/trans.R index 4deeadba..51d522f6 100644 --- a/R/trans.R +++ b/R/trans.R @@ -60,7 +60,6 @@ setMethod("scale", c("sdArray", "numeric"), \(x, j, t, ...) { #' @importFrom methods as #' @export setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { - #x <- label(sd, 2); t <- c(64,0) stopifnot( length(t) == length(dim(x)), t == round(t), all(is.finite(t))) diff --git a/man/CTgraph.Rd b/man/CTgraph.Rd new file mode 100644 index 00000000..89336782 --- /dev/null +++ b/man/CTgraph.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/CTgraph.R +\name{CTgraph} +\alias{CTgraph} +\alias{CTpath} +\alias{CTplot} +\alias{CTgraph,SpatialData-method} +\alias{CTgraph,SpatialDataElement-method} +\alias{CTgraph,ANY-method} +\alias{CTpath,SpatialData-method} +\alias{CTpath,SpatialDataElement-method} +\alias{CTpath,ANY-method} +\title{Coord. trans. graph} +\usage{ +\S4method{CTgraph}{SpatialData}(x) + +\S4method{CTgraph}{SpatialDataElement}(x) + +\S4method{CTgraph}{ANY}(x) + +\S4method{CTpath}{SpatialData}(x, i, j) + +\S4method{CTpath}{SpatialDataElement}(x, j) + +\S4method{CTpath}{ANY}(x) + +CTplot(g, cex = 0.5, fac = 2, max = 10) +} +\arguments{ +\item{x}{\code{SpatialData}, an element, or \code{Zattrs}.} + +\item{i}{character string; name of source node.} + +\item{j}{character string; name of target coordinate space.} + +\item{g}{base R graph; extracted with \code{CTgraph}.} + +\item{cex}{scalar numeric; controls fontsize of node labels.} + +\item{fac, max}{scalar numeric; node labels with \code{nchar>max} +are split and hyphenated at position \code{floor(nchar/fac)}} +} +\description{ +Coord. trans. graph +} +\examples{ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, tables=FALSE) + +# object-wide +g <- CTgraph(x) +CTplot(g) + +# one element +y <- label(x) +g <- CTgraph(y) +CTplot(g) + +# retrieve transformation(s) +# from element to target space +CTpath(x, "blobs_labels", "sequence") +} diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index aab39e96..9902646c 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -22,11 +22,6 @@ \alias{CTtype,SpatialDataElement-method} \alias{CTname,SpatialDataElement-method} \alias{CTname,SpatialData-method} -\alias{CTgraph,SpatialData-method} -\alias{CTgraph,SpatialDataElement-method} -\alias{CTgraph,ANY-method} -\alias{CTpath,SpatialData-method} -\alias{CTpath,SpatialDataElement-method} \alias{rmvCT,SpatialDataElement-method} \alias{rmvCT,Zattrs-method} \alias{addCT,SpatialDataElement-method} @@ -55,16 +50,6 @@ \S4method{CTname}{SpatialData}(x, ...) -\S4method{CTgraph}{SpatialData}(x) - -\S4method{CTgraph}{SpatialDataElement}(x) - -\S4method{CTgraph}{ANY}(x) - -\S4method{CTpath}{SpatialData}(x, i, j) - -\S4method{CTpath}{SpatialDataElement}(x, j) - \S4method{rmvCT}{SpatialDataElement}(x, i) \S4method{rmvCT}{Zattrs}(x, i) @@ -81,14 +66,14 @@ \item{i}{for \code{CTpath}, source node label; else, string or scalar integer giving the name or index of a coordinate space.} -\item{j}{character string; name of target coordinate space.} - \item{name}{character(1); name of coordinate space} \item{type}{character(1); type of transformation} \item{data}{transformation data; size and shape depend on transformation and element type (e.g., numeric(1) for rotation, numeric(2) for scaling in 2D)} + +\item{j}{character string; name of target coordinate space.} } \description{ Coordinate transformations @@ -98,18 +83,6 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) -# element-wise -g <- CTgraph(y <- image(x)) -graph::nodes(g) -CTpath(y, "global") - -# object-wide -g <- CTgraph(x) -plotCoordGraph(g) - -# retrieve transformation from element to target space -CTpath(x, "blobs_labels", "sequence") - # view available target coordinate systems CTname(z <- meta(label(x))) diff --git a/man/plotCoordGraph.Rd b/man/plotCoordGraph.Rd deleted file mode 100644 index a4acca58..00000000 --- a/man/plotCoordGraph.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/coord_utils.R -\name{plotCoordGraph} -\alias{plotCoordGraph} -\title{CT graph viz.} -\usage{ -plotCoordGraph(g, cex = 0.6) -} -\arguments{ -\item{g}{base R graph; extracted with \code{\link{CTgraph}}.} - -\item{cex}{scalar numeric; controls fontsize of node labels.} -} -\description{ -given a \code{graphNEL} instance, nodes with \code{nchar>max} are -split and hyphenated at character position \code{floor(nchar/fac)}. -} -\examples{ -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, tables=FALSE) - -g <- CTgraph(x) -plotCoordGraph(g, cex=0.6) - -} diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index ab0cd84c..eabf5a19 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -21,6 +21,71 @@ test_that("axes", { expect_is(y, "list") expect_length(y, 2) expect_equal(unlist(y), c("x", "y")) + # missing + y <- image(x) + y@meta$multiscales[[1]]$axes <- NULL + expect_error(axes(y)) +}) + +.CTtype <- c("identity", "scale", "rotate", "translation", "affine", "sequence") + +test_that("CTlist", { + y <- CTlist(label(x)) + expect_is(y, "list") + expect_length(y, 5) + z <- Reduce(intersect, lapply(y, names)) + expect_setequal(z, c("input", "output", "type")) + z <- vapply(y, \(.) .$type, character(1)) + expect_true(all(z %in% .CTtype)) +}) +test_that("CTdata", { + # invalid + expect_error(CTdata(label(x), "")) + expect_error(CTdata(label(x), 99)) + expect_error(CTdata(label(x), Inf)) + expect_error(CTdata(label(x), TRUE)) + # identity + y <- CTdata(label(x), "global") + expect_null(y) + # scale + y <- CTdata(label(x), "scale") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + expect_true(all(unlist(y) > 0)) + # translation + y <- CTdata(label(x), "translation") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + # affine + y <- CTdata(label(x), "affine") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + expect_true(all(unlist(y) > 0)) + z <- vapply(y, length, integer(1)) + expect_true(all(z == 3)) + # sequence + y <- CTdata(label(x), "sequence") + expect_is(y, "list") + expect_length(y, 2) + expect_true(all(names(y) %in% .CTtype)) + z <- vapply(y, length, integer(1)) + expect_true(all(z == 2)) +}) +test_that("CTtype", { + y <- CTtype(label(x)) + expect_is(y, "character") + expect_length(y, 5) + expect_true(all(y %in% .CTtype)) +}) +test_that("CTname", { + y <- CTname(label(x)) + expect_is(y, "character") + expect_length(y, 5) + expect_true(all(nchar(y) > 0)) + expect_true(!any(duplicated(y))) }) test_that("rmvCT", { @@ -29,6 +94,9 @@ test_that("rmvCT", { expect_error(rmvCT(y, 100)) expect_error(rmvCT(y, ".")) expect_error(rmvCT(y, c(".", CTname(y)[1]))) + # identity is kept with a warning + expect_warning(z <- rmvCT(y, "global")) + expect_identical(CTname(z), CTname(y)) # by name i <- sample(setdiff(CTname(y), "global"), 2) expect_identical(CTname(rmvCT(y, i)), setdiff(CTname(y), i)) @@ -119,7 +187,7 @@ test_that("CTpath", { expect_length(z$type, 1) }) -test_that("plotCoordGraph", { +test_that("CTplot", { f <- function(.) { tf <- tempfile(fileext=".pdf") on.exit(unlink(tf)) @@ -127,10 +195,10 @@ test_that("plotCoordGraph", { file.size(tf) } g <- CTgraph(x) - p <- f(plotCoordGraph(g)) + p <- f(CTplot(g)) expect_is(p, "numeric") expect_true(p > f(plot(1))) - p <- f(plotCoordGraph(g, 0.1)) - q <- f(plotCoordGraph(g, 0.9)) + p <- f(CTplot(g, 0.1)) + q <- f(CTplot(g, 0.9)) expect_true(p < q) }) diff --git a/vignettes/SpatialData.Rmd b/vignettes/SpatialData.Rmd index 125e2ed6..8c6e879d 100644 --- a/vignettes/SpatialData.Rmd +++ b/vignettes/SpatialData.Rmd @@ -132,7 +132,7 @@ We can represent these are a graph as follows: ```{r cs-graph} (g <- CTgraph(x)) -plotCoordGraph(g) +CTplot(g) ``` The above representation greatly facilitates queries of the transformation(s) From 405621c975ae5c7d152b60f97db36738ab458ad1 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 17:02:52 +0200 Subject: [PATCH 103/151] fix typo --- DESCRIPTION | 1 + NAMESPACE | 6 +-- R/coord_utils.R | 22 +--------- R/sdArray.R | 5 ++- R/trans.R | 80 ++++++++++++++---------------------- man/SpatialData.Rd | 8 ++-- man/coord-utils.Rd | 2 - man/trans.Rd | 12 ++---- tests/testthat/test-zattrs.R | 2 +- 9 files changed, 47 insertions(+), 91 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 31e61414..41da3d26 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -50,6 +50,7 @@ Imports: sf, S4Arrays, S4Vectors, + SparseArray, SingleCellExperiment, SummarizedExperiment Suggests: diff --git a/NAMESPACE b/NAMESPACE index efbfdd9f..61fad362 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -90,10 +90,10 @@ importClassesFrom(S4Vectors,DFrame) importFrom(BiocGenerics,as.data.frame) importFrom(BiocGenerics,colnames) importFrom(BiocGenerics,rownames) -importFrom(DelayedArray,ConstantArray) importFrom(DelayedArray,DelayedArray) -importFrom(DelayedArray,cbind) -importFrom(DelayedArray,rbind) +importFrom(EBImage,resize) +importFrom(EBImage,rotate) +importFrom(EBImage,translate) importFrom(Matrix,rowSums) importFrom(Matrix,sparseMatrix) importFrom(Matrix,sparseVector) diff --git a/R/coord_utils.R b/R/coord_utils.R index 0bff61da..91a1517b 100644 --- a/R/coord_utils.R +++ b/R/coord_utils.R @@ -72,7 +72,7 @@ setMethod("CTdata", "Zattrs", \(x, i=1, ...) { stopifnot( i == round(i), i %in% seq_along(CTlist(x))) - } else stop("Invlid 'i'; should be a scalar character or integer") + } else stop("Invalid 'i'; should be a scalar character or integer") t <- CTtype(x)[i] if (t != "sequence") return(CTlist(x)[[i]][[t]]) @@ -203,23 +203,3 @@ setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { } return(x) }) - -# Dec 8 VJC -- why is this in doc comment mode? changing to plain comment -# # @importFrom EBImage resize -# setMethod("scale", "ImageArray", \(x, t, ...) { -# a <- as.array(data(x)) -# # TODO: this should be done w/o realizing -# # into memory, but EBImage needs an array? -# d <- length(dim(a)) -# if (missing(t)) -# t <- rep(1, d) -# b <- resize(aperm(a), -# w=dim(a)[d]*t[d], -# h=dim(a)[d-1]*t[d-1]) -# x@data <- aperm(b) -# x -# }) -# -# # @importFrom EBImage resize -# setMethod("translation", "ImageArray", \(x, t, ...) {}) -# setMethod("transform", "ImageArray", \(x, t) get(t$type)(x, unlist(t[[t$type]]))) diff --git a/R/sdArray.R b/R/sdArray.R index c1893892..26a89bee 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -51,7 +51,10 @@ setMethod("length", "sdArray", \(x) length(data(x, NULL))) #' @rdname Array-methods #' @export -setMethod("data_type", "sdArray", \(x) data_type(data(x))) +setMethod("data_type", "sdArray", \(x) { + if (is(y <- data(x), "DelayedArray")) + data_type(y) else metadata(x)$data_type +}) #' @rdname Array-methods #' @importFrom DelayedArray DelayedArray diff --git a/R/trans.R b/R/trans.R index 51d522f6..34e5eb3c 100644 --- a/R/trans.R +++ b/R/trans.R @@ -41,63 +41,43 @@ #' y["shapes", c("rot", "wide", "left")] NULL -# image ---- +# array ---- -#' @rdname trans -#' @export -setMethod("scale", c("sdArray", "numeric"), \(x, j, t, ...) { - stopifnot(length(t) == length(dim(x)), t > 0) +#' @importFrom methods as +#' @importFrom S4Vectors metadata<- +.trans_a <- \(x, f) { + a <- f(aperm(as.array(data(x)))) + x@data <- list(as(aperm(a), "SparseArray")) + metadata(x) <- list(data_type=data_type(x)) + return(x) +} + +#' @importFrom EBImage resize +setMethod("scale", c("sdArray", "numeric"), \(x, t, ...) { + stopifnot(length(t) == length(dim(x)), is.finite(t), t > 0) if (all(t == 1)) return(x) - if (is.numeric(j)) j <- CTname(x) - j <- match.arg(j, CTname(x)) - addCT(x, name=j, type="scale", data=t) + d <- length(dim(a)) + f <- \(.) resize(., + w=dim(.)[d]*t[d], + h=dim(.)[d-1]*t[d-1]) + .trans_a(x, f) }) -# label ---- +#' @importFrom EBImage rotate +setMethod("rotate", c("sdArray", "numeric"), \(x, t, ...) { + # negate angle since 'EBImage' rotates clockwise + stopifnot(length(t) == 1, is.finite(t)) + if (t %% 360 == 0) return(x) + f <- \(.) rotate(., -t) + .trans_a(x, f) +}) -#' @rdname trans -#' @importFrom DelayedArray cbind rbind ConstantArray -#' @importFrom methods as -#' @export +#' @importFrom EBImage translate setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { - stopifnot( - length(t) == length(dim(x)), - t == round(t), all(is.finite(t))) + stopifnot(length(t) == length(dim(x)), is.finite(t)) if (all(t == 0)) return(x) - ys <- data(x, NULL) - 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) - } - x@data <- lapply(seq_along(ys), \(k) { - t <- ts[[k]] - y <- as(ys[[k]], "DelayedArray") - # TODO: no 'abind' support so that we - # permute, 'c/rbind', and back-permute; - # surely there has to be a better way? - if (length(dim(y)) == 2) { - n <- NULL - } else { - t <- t[-1] - n <- dim(x)[1] - y <- aperm(y, c(2,3,1)) - } - if (t[2] != 0) { - d <- c(nrow(y), abs(t[2]), n) - z <- 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 <- ConstantArray(0, dim=d) - y <- if (t[1] > 0) rbind(z, y) else rbind(y, z) - } - if (!is.null(n)) aperm(y, c(3,1,2)) else y - }) - return(x) + f <- \(.) translate(., t[-1]) + .trans_a(x, f) }) # point ---- diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/coord-utils.Rd b/man/coord-utils.Rd index 9902646c..3bc530f8 100644 --- a/man/coord-utils.Rd +++ b/man/coord-utils.Rd @@ -7,8 +7,6 @@ \alias{CTname} \alias{CTtype} \alias{CTdata} -\alias{CTpath} -\alias{CTgraph} \alias{addCT} \alias{rmvCT} \alias{axes,Zattrs-method} diff --git a/man/trans.Rd b/man/trans.Rd index b132b1d3..ca4617ff 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -5,8 +5,6 @@ \alias{scale} \alias{rotate} \alias{translation} -\alias{scale,sdArray,numeric-method} -\alias{translation,sdArray,numeric-method} \alias{scale,PointFrame,numeric-method} \alias{rotate,PointFrame,numeric-method} \alias{translation,PointFrame,numeric-method} @@ -15,10 +13,6 @@ \alias{translation,ShapeFrame,numeric-method} \title{Transformations} \usage{ -\S4method{scale}{sdArray,numeric}(x, j, t, ...) - -\S4method{translation}{sdArray,numeric}(x, t, ...) - \S4method{scale}{PointFrame,numeric}(x, t, ...) \S4method{rotate}{PointFrame,numeric}(x, t, ...) @@ -34,12 +28,12 @@ \arguments{ \item{x}{\code{SpatialData} element} -\item{j}{scalar character or numeric; -name or index of coordinate space.} - \item{t}{transformation data.} \item{...}{option arguments passed to and from other methods.} + +\item{j}{scalar character or numeric; +name or index of coordinate space.} } \description{ Transformations diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index eabf5a19..8321fd26 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -162,7 +162,7 @@ test_that("CTgraph", { # every element & transformation ns <- lapply(setdiff(SpatialData:::.LAYERS, "tables"), \(l) lapply(names(x[[l]]), - \(e) c(e, CTname(x[[l]][[e]])))) + \(e) c(e, CTname(x[[l]][[e]])))) ns <- sort(unique(unlist(ns))) expect_true(all(ns %in% sort(graph::nodes(g)))) # element-wise From af7c6748d2c83898ca50adc160d8e92aa9bd13d7 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 18:40:44 +0200 Subject: [PATCH 104/151] +centroids() --- NAMESPACE | 3 ++ R/AllGenerics.R | 2 ++ R/ShapeFrame.R | 11 ++++++- R/mask.R | 2 +- R/utils.R | 73 +++++++++++++++++++++++++++++++++++++++++++++++ man/ShapeFrame.Rd | 3 ++ man/utils.Rd | 42 +++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 R/utils.R create mode 100644 man/utils.Rd diff --git a/NAMESPACE b/NAMESPACE index 61fad362..589b2e21 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -46,6 +46,7 @@ exportMethods(CTtype) exportMethods(addCT) exportMethods(as.data.frame) exportMethods(axes) +exportMethods(centroids) exportMethods(channels) exportMethods(colnames) exportMethods(data) @@ -53,6 +54,7 @@ exportMethods(data_type) exportMethods(dim) exportMethods(element) exportMethods(feature_key) +exportMethods(geom_type) exportMethods(getTable) exportMethods(hasTable) exportMethods(image) @@ -97,6 +99,7 @@ importFrom(EBImage,translate) importFrom(Matrix,rowSums) importFrom(Matrix,sparseMatrix) importFrom(Matrix,sparseVector) +importFrom(Matrix,summary) importFrom(Matrix,t) importFrom(RBGL,sp.between) importFrom(Rarr,read_zarr_attributes) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 5c9f6541..b54d2c10 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -74,6 +74,8 @@ setGeneric("mask", \(x, i, j, ...) standardGeneric("mask")) setGeneric("channels", \(x, ...) standardGeneric("channels")) setGeneric("data_type", \(x, ...) standardGeneric("data_type")) +setGeneric("geom_type", \(x, ...) standardGeneric("geom_type")) +setGeneric("centroids", \(x, ...) standardGeneric("centroids")) # tbl ---- diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index e33b2aab..38e4e198 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -51,8 +51,9 @@ setMethod("length", "ShapeFrame", \(x) nrow(data(x))) #' @export setMethod("names", "ShapeFrame", \(x) names(data(x))) -#' @importFrom utils .DollarNames #' @export +#' @rdname ShapeFrame +#' @importFrom utils .DollarNames .DollarNames.ShapeFrame <- \(x, pattern="") { grep(pattern, names(x), value=TRUE) } @@ -61,6 +62,14 @@ setMethod("names", "ShapeFrame", \(x) names(data(x))) #' @exportMethod $ setMethod("$", "ShapeFrame", \(x, name) data(x)[[name]]) +#' @export +#' @importFrom sf st_as_sf st_geometry_type +setMethod("geom_type", "ShapeFrame", \(x) { + y <- st_as_sf(data(x)) + z <- st_geometry_type(y[1, ]) + return(as.character(z)) +}) + # sub ---- #' @rdname ShapeFrame diff --git a/R/mask.R b/R/mask.R index 7a6658a4..a4e2c75d 100644 --- a/R/mask.R +++ b/R/mask.R @@ -103,7 +103,7 @@ setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") n <- nrow(j <- st_as_sf(data(j))) - fun <- switch(as.character(st_geometry_type(j[1, ])), + fun <- switch(geom_type(j), POINT=\(i, j) rowSums(st_distance(j, i) <= j$radius), \(i, j) vapply(st_intersects(j, i), length, integer(1))) # realize one feature at i time diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 00000000..6800b4ce --- /dev/null +++ b/R/utils.R @@ -0,0 +1,73 @@ +#' @name utils +#' @rdname utils +#' @title Utilities +#' @aliases centroids +#' +#' @param x a \code{SpatialData} element (any but image) +#' @param as determines how results will be returned +#' +#' @examples +#' x <- file.path("extdata", "blobs.zarr") +#' x <- system.file(x, package="SpatialData") +#' x <- readSpatialData(x, tables=FALSE) +#' +#' centroids(label(x)) +#' centroids(shape(x)) +#' centroids(shape(x, 3), "list") +#' +#' head(centroids(point(x))) +#' xy <- centroids(point(x), "list") +#' plot(xy$gene_a, col=a <- "red") +#' points(xy$gene_b, col=b <- "blue") +#' legend("topright", legend=names(xy), col=c(a, b), pch=21) +NULL + +#' @export +#' @rdname utils +setMethod("centroids", "ANY", \(x, ...) stop("'centroids' ", + "only supported for label, shape, and point elements")) + +#' @export +#' @rdname utils +#' @importFrom Matrix summary +setMethod("centroids", "LabelArray", \(x, + as=c("data.frame", "matrix")) { + as <- match.arg(as) + y <- data(x) + y <- as(y, "dgCMatrix") + i <- summary(y) + xy <- tapply(i[, -3], i[[3]], colMeans) + xy <- do.call(rbind, xy) + xy <- cbind(xy, as.integer(rownames(xy))) + colnames(xy) <- c("x", "y", "i") + if (as == "matrix") return(xy) + as.data.frame(xy) +}) + +#' @export +#' @rdname utils +#' @importFrom sf st_as_sf st_geometry_type st_coordinates +setMethod("centroids", "ShapeFrame", \(x, + as=c("data.frame", "matrix", "list")) { + as <- match.arg(as) + y <- st_as_sf(data(x)) + xy <- st_coordinates(y) + colnames(xy)[c(1, 2)] <- c("x", "y") + if (as == "matrix") return(xy) + xy <- as.data.frame(xy) + if (as == "data.frame") return(xy) + split(xy, xy[seq(3, ncol(xy))]) +}) + +#' @export +#' @rdname utils +setMethod("centroids", "PointFrame", \(x, + as=c("data.frame", "matrix", "list")) { + as <- match.arg(as) + i <- feature_key(x) + xy <- data(x)[, c("x", "y", i)] + xy <- as.data.frame(xy) + if (as == "data.frame") return(xy) + if (as == "matrix") return(as.matrix(xy)) + lapply(split(xy, xy[[i]]), `[`, -3) +}) \ No newline at end of file diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index 5f252f0b..55ae132e 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -5,6 +5,7 @@ \alias{dim,ShapeFrame-method} \alias{length,ShapeFrame-method} \alias{names,ShapeFrame-method} +\alias{.DollarNames.ShapeFrame} \alias{$,ShapeFrame-method} \alias{[,ShapeFrame,missing,ANY,ANY-method} \alias{[,ShapeFrame,ANY,missing,ANY-method} @@ -20,6 +21,8 @@ ShapeFrame(data = data.frame(), meta = Zattrs(), metadata = list(), ...) \S4method{names}{ShapeFrame}(x) +\method{.DollarNames}{ShapeFrame}(x, pattern = "") + \S4method{$}{ShapeFrame}(x, name) \S4method{[}{ShapeFrame,missing,ANY,ANY}(x, i, j, ..., drop = TRUE) diff --git a/man/utils.Rd b/man/utils.Rd new file mode 100644 index 00000000..fa204330 --- /dev/null +++ b/man/utils.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{utils} +\alias{utils} +\alias{centroids} +\alias{centroids,ANY-method} +\alias{centroids,LabelArray-method} +\alias{centroids,ShapeFrame-method} +\alias{centroids,PointFrame-method} +\title{Utilities} +\usage{ +\S4method{centroids}{ANY}(x, ...) + +\S4method{centroids}{LabelArray}(x, as = c("data.frame", "matrix")) + +\S4method{centroids}{ShapeFrame}(x, as = c("data.frame", "matrix", "list")) + +\S4method{centroids}{PointFrame}(x, as = c("data.frame", "matrix", "list")) +} +\arguments{ +\item{x}{a \code{SpatialData} element (any but image)} + +\item{as}{determines how results will be returned} +} +\description{ +Utilities +} +\examples{ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, tables=FALSE) + +centroids(label(x)) +centroids(shape(x)) +centroids(shape(x, 3), "list") + +head(centroids(point(x))) +xy <- centroids(point(x), "list") +plot(xy$gene_a, col=a <- "red") +points(xy$gene_b, col=b <- "blue") +legend("topright", legend=names(xy), col=c(a, b), pch=21) +} From 000874d8d031d330609a7ef50f8e5157d4f12b4c Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 19:23:50 +0200 Subject: [PATCH 105/151] +centroids(); more tests --- R/LabelArray.R | 2 +- R/mask.R | 2 +- R/utils.R | 12 +++-- man/LabelArray.Rd | 2 +- man/SpatialData.Rd | 8 +-- man/utils.Rd | 2 +- tests/testthat/test-methods.R | 24 ++++----- tests/testthat/test-trans.R | 96 +++++++++++++++++------------------ tests/testthat/test-utils.R | 65 ++++++++++++++++++++++++ 9 files changed, 140 insertions(+), 73 deletions(-) create mode 100644 tests/testthat/test-utils.R diff --git a/R/LabelArray.R b/R/LabelArray.R index 519fc7c2..403f8b3f 100644 --- a/R/LabelArray.R +++ b/R/LabelArray.R @@ -30,7 +30,7 @@ #' @importFrom S4Vectors metadata<- #' @importFrom methods new #' @export -LabelArray <- function(data=array(), meta=Zattrs(), metadata=list(), ...) { +LabelArray <- function(data=list(), meta=Zattrs(), metadata=list(), ...) { x <- .LabelArray(data=data, meta=meta, ...) metadata(x) <- metadata return(x) diff --git a/R/mask.R b/R/mask.R index a4e2c75d..6b81b514 100644 --- a/R/mask.R +++ b/R/mask.R @@ -102,11 +102,11 @@ setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { #' @importFrom sf st_as_sf st_geometry_type st_distance setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") - n <- nrow(j <- st_as_sf(data(j))) fun <- switch(geom_type(j), POINT=\(i, j) rowSums(st_distance(j, i) <= j$radius), \(i, j) vapply(st_intersects(j, i), length, integer(1))) # realize one feature at i time + n <- nrow(j <- st_as_sf(data(j))) is <- split(seq_len(length(i)), i[[feature_key(i)]]) ns <- lapply(is, \(.) { # make points 'sf'-compliant diff --git a/R/utils.R b/R/utils.R index 6800b4ce..cbbb4674 100644 --- a/R/utils.R +++ b/R/utils.R @@ -39,9 +39,10 @@ setMethod("centroids", "LabelArray", \(x, xy <- tapply(i[, -3], i[[3]], colMeans) xy <- do.call(rbind, xy) xy <- cbind(xy, as.integer(rownames(xy))) - colnames(xy) <- c("x", "y", "i") + dimnames(xy) <- list(NULL, c("x", "y", "i")) if (as == "matrix") return(xy) - as.data.frame(xy) + xy <- as.data.frame(xy) + xy$i <- factor(xy$i); xy }) #' @export @@ -55,6 +56,10 @@ setMethod("centroids", "ShapeFrame", \(x, colnames(xy)[c(1, 2)] <- c("x", "y") if (as == "matrix") return(xy) xy <- as.data.frame(xy) + rownames(xy) <- NULL + if (ncol(xy) > 2) + for (. in seq(3, ncol(xy))) + xy[[.]] <- factor(xy[[.]], unique(xy[[.]])) if (as == "data.frame") return(xy) split(xy, xy[seq(3, ncol(xy))]) }) @@ -62,12 +67,11 @@ setMethod("centroids", "ShapeFrame", \(x, #' @export #' @rdname utils setMethod("centroids", "PointFrame", \(x, - as=c("data.frame", "matrix", "list")) { + as=c("data.frame", "list")) { as <- match.arg(as) i <- feature_key(x) xy <- data(x)[, c("x", "y", i)] xy <- as.data.frame(xy) if (as == "data.frame") return(xy) - if (as == "matrix") return(as.matrix(xy)) lapply(split(xy, xy[[i]]), `[`, -3) }) \ No newline at end of file diff --git a/man/LabelArray.Rd b/man/LabelArray.Rd index 2d9c7f17..0c3dea1b 100644 --- a/man/LabelArray.Rd +++ b/man/LabelArray.Rd @@ -5,7 +5,7 @@ \alias{[,LabelArray,ANY,ANY,ANY-method} \title{The \code{LabelArray} class} \usage{ -LabelArray(data = array(), meta = Zattrs(), metadata = list(), ...) +LabelArray(data = list(), meta = Zattrs(), metadata = list(), ...) \S4method{[}{LabelArray,ANY,ANY,ANY}(x, i, j, ..., drop = FALSE) } diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/utils.Rd b/man/utils.Rd index fa204330..57d41527 100644 --- a/man/utils.Rd +++ b/man/utils.Rd @@ -15,7 +15,7 @@ \S4method{centroids}{ShapeFrame}(x, as = c("data.frame", "matrix", "list")) -\S4method{centroids}{PointFrame}(x, as = c("data.frame", "matrix", "list")) +\S4method{centroids}{PointFrame}(x, as = c("data.frame", "list")) } \arguments{ \item{x}{a \code{SpatialData} element (any but image)} diff --git a/tests/testthat/test-methods.R b/tests/testthat/test-methods.R index ebe9a90d..66e5c677 100644 --- a/tests/testthat/test-methods.R +++ b/tests/testthat/test-methods.R @@ -1,15 +1,14 @@ -if (FALSE) { library(SingleCellExperiment) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x) -# skirt base::table ambiguity -sdtable <- SpatialData::table -`sdtable<-` <- `SpatialData::table<-` -sdtables <- SpatialData::tables +# # skirt base::table ambiguity +# sdtable <- SpatialData::table +# `sdtable<-` <- `SpatialData::table<-` +# sdtables <- SpatialData::tables -fun <- c("image", "label", "shape", "point", "sdtable") +fun <- c("image", "label", "shape", "point", "table") nms <- c("blobs_image", "blobs_labels", "blobs_circles", "blobs_points", "table") typ <- c("ImageArray", "LabelArray", "ShapeFrame", "PointFrame", "SingleCellExperiment") @@ -170,12 +169,12 @@ test_that("[,LabelArray", { expect_equal(dim(y[FALSE,FALSE]), c(0,0)) expect_equal(dim(y[FALSE,TRUE]), c(0,ncol(y))) expect_equal(dim(y[TRUE,FALSE]), c(nrow(y),0)) - i <- logical(nrow(y)); j <- logical(ncol(y)) - n <- replicate(2, sample(seq(2, 10), 1)) - i[sample(nrow(y), n[1])] <- TRUE - j[sample(ncol(y), n[2])] <- TRUE - expect_equal(nrow(y[i,]), n[1]) - expect_equal(ncol(y[,j]), n[2]) + # i <- logical(nrow(y)); j <- logical(ncol(y)) + # n <- replicate(2, sample(seq(2, 10), 1)) + # i[sample(nrow(y), n[1])] <- TRUE + # j[sample(ncol(y), n[2])] <- TRUE + # expect_equal(nrow(y[i,]), n[1]) + # expect_equal(ncol(y[,j]), n[2]) # numeric expect_identical(y[,], y) # none expect_equal(nrow(y[1,]), 1) # no j @@ -258,4 +257,3 @@ test_that("[,SpatialData", { element(y, 1, 1), element(x, 1, .n(x)[1])) }) -} diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index a43d240d..4d6e9301 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -2,55 +2,55 @@ zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) -test_that("translation,imageArray", { - x <- image(sd, 1) - y <- translation(x, c(0,0,0)) - expect_identical(x, y) - expect_error(translation(x, numeric(2))) - expect_error(translation(x, numeric(4))) - expect_error(translation(x, character(3))) - # 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) -}) +# test_that("translation,imageArray", { +# x <- image(sd, 1) +# y <- translation(x, c(0,0,0)) +# expect_identical(x, y) +# expect_error(translation(x, numeric(2))) +# expect_error(translation(x, numeric(4))) +# expect_error(translation(x, character(3))) +# # 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) +# }) -test_that("translation,labelArray", { - x <- label(sd, 1) - y <- translation(x, c(0,0)) - expect_identical(x, y) - expect_error(translation(x, numeric(1))) - expect_error(translation(x, numeric(3))) - expect_error(translation(x, character(2))) - # row - t <- c(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, 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) - # res - x <- label(sd, 2) - t <- c(n <- nrow(x), 0) - y <- translation(x, t) - dx <- vapply(data(x, NULL), dim, integer(2)) - dy <- vapply(data(y, NULL), dim, integer(2)) - expect_equal(dx[1,], dy[1,]/2) - expect_identical(dx[2,], dy[2,]) -}) +# test_that("translation,labelArray", { +# x <- label(sd, 1) +# y <- translation(x, c(0,0)) +# expect_identical(x, y) +# expect_error(translation(x, numeric(1))) +# expect_error(translation(x, numeric(3))) +# expect_error(translation(x, character(2))) +# # row +# t <- c(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, 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) +# # res +# x <- label(sd, 2) +# t <- c(n <- nrow(x), 0) +# y <- translation(x, t) +# dx <- vapply(data(x, NULL), dim, integer(2)) +# dy <- vapply(data(y, NULL), dim, integer(2)) +# expect_equal(dx[1,], dy[1,]/2) +# expect_identical(dx[2,], dy[2,]) +# }) test_that("translation,PointFrame", { x <- point(sd, 1) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 00000000..c689da51 --- /dev/null +++ b/tests/testthat/test-utils.R @@ -0,0 +1,65 @@ +xy <- c("x", "y") +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, tables=FALSE) + +test_that("centroids,LabelArray", { + y <- label(x) + z <- centroids(y, "data.frame") + expect_is(z, "data.frame") + expect_identical(names(z), c(xy, "i")) + expect_is(z$i, "factor") + expect_is(unlist(z[xy]), "numeric") + .z <- centroids(y, "matrix") + expect_is(.z, "matrix") + z$i <- as.integer(as.character(z$i)) + expect_identical(.z, as.matrix(z)) +}) + +test_that("centroids,PointFrame", { + i <- feature_key(y <- point(x)) + z <- centroids(y, "data.frame") + expect_is(z, "data.frame") + expect_identical(names(z), c(xy, i)) + expect_is(z[[i]], "factor") + expect_is(unlist(z[xy]), "integer") + .z <- centroids(y, "list") + expect_is(.z, "list") + expect_all_true(names(.z) %in% z[[i]]) + expect_length(.z, length(unique(z[[i]]))) + for (. in names(.z)) expect_identical( + .z[[.]][xy], z[z[[i]] == ., xy]) +}) + +test_that("centroids,ShapeFrame", { + # circle + y <- shape(x) + z <- centroids(y, "data.frame") + expect_is(z, "data.frame") + expect_identical(names(z), xy) + expect_is(unlist(z[xy]), "numeric") + .z <- centroids(y, "matrix") + expect_identical(.z, as.matrix(z)) + # polygon + y <- shape(x, 3) + z <- centroids(y, "data.frame") + expect_is(z, "data.frame") + expect_all_true(xy %in% names(z)) + expect_is(z[[ncol(z)]], "factor") + expect_is(unlist(z[xy]), "numeric") + .z <- centroids(y, "matrix") + expect_is(.z, "matrix") + expect_identical(.z[, xy], as.matrix(z[xy])) + # multipolygon + y <- shape(x, 2) + z <- centroids(y, "data.frame") + expect_is(z, "data.frame") + expect_all_true(xy %in% names(z)) + expect_is(z[[ncol(z)]], "factor") + expect_is(unlist(z[xy]), "numeric") + .z <- centroids(y, "matrix") + expect_is(.z, "matrix") + for (. in seq(3, ncol(z))) + z[[.]] <- as.integer(as.character(z[[.]])) + expect_identical(.z, as.matrix(z)) +}) From 10f30d65ad58a44a364ea486f3fa1c5d94a8992f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 19:29:35 +0200 Subject: [PATCH 106/151] +Imports:EBImage --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 41da3d26..a70ccca0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,6 +38,7 @@ Imports: BiocGenerics, DelayedArray, dplyr, + EBImage, geoarrow, graph, Matrix, From 0493f9ab69ea0558cf090951ddbc7cbfbe21ccf7 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 20:02:07 +0200 Subject: [PATCH 107/151] +extent() --- NAMESPACE | 1 + R/AllGenerics.R | 1 + R/utils.R | 46 +++++++++++++++++++++++++++++++++++-- man/utils.Rd | 15 ++++++++++++ tests/testthat/test-query.R | 3 +++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 589b2e21..a2308f7e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -53,6 +53,7 @@ exportMethods(data) exportMethods(data_type) exportMethods(dim) exportMethods(element) +exportMethods(extent) exportMethods(feature_key) exportMethods(geom_type) exportMethods(getTable) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index b54d2c10..831c8389 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -76,6 +76,7 @@ setGeneric("channels", \(x, ...) standardGeneric("channels")) setGeneric("data_type", \(x, ...) standardGeneric("data_type")) setGeneric("geom_type", \(x, ...) standardGeneric("geom_type")) setGeneric("centroids", \(x, ...) standardGeneric("centroids")) +setGeneric("extent", \(x, ...) standardGeneric("extent")) # tbl ---- diff --git a/R/utils.R b/R/utils.R index cbbb4674..767c85de 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,7 +1,7 @@ #' @name utils #' @rdname utils #' @title Utilities -#' @aliases centroids +#' @aliases centroids extent #' #' @param x a \code{SpatialData} element (any but image) #' @param as determines how results will be returned @@ -20,8 +20,18 @@ #' plot(xy$gene_a, col=a <- "red") #' points(xy$gene_b, col=b <- "blue") #' legend("topright", legend=names(xy), col=c(a, b), pch=21) +#' +#' # object-wide +#' extent(x) +#' +#' # element-wise +#' extent(label(x)) +#' extent(point(x)) +#' extent(shape(x)) NULL +# centroids ---- + #' @export #' @rdname utils setMethod("centroids", "ANY", \(x, ...) stop("'centroids' ", @@ -74,4 +84,36 @@ setMethod("centroids", "PointFrame", \(x, xy <- as.data.frame(xy) if (as == "data.frame") return(xy) lapply(split(xy, xy[[i]]), `[`, -3) -}) \ No newline at end of file +}) + +# extent ---- + +# TODO: this needs more work to consider transformations + +#' @export +#' @rdname utils +setMethod("extent", "SpatialData", \(x) { + ls <- setdiff(.LAYERS, "tables") + ex <- lapply(ls, \(.) lapply(x[[.]], extent)) + ex <- Reduce(c, ex) + names(xy) <- xy <- c("x", "y") + lapply(xy, \(z) { + d <- vapply(ex, \(.) .[[z]], numeric(2)) + c(min(d[1, ]), max(d[2, ])) + }) +}) + +#' @export +#' @rdname utils +setMethod("extent", "SpatialDataElement", \(x) { + if (is(x, "sdArray")) { + nm <- vapply(ax <- axes(x), \(.) .$name, character(1)) + xy <- vapply(ax, \(.) .$type == "space", logical(1)) + d <- dim(x); names(d) <- nm + lapply(d[xy], \(.) c(0, .)) + } else { + ax <- unlist(axes(x)) + xy <- centroids(x)[ax] + apply(xy, 2, range, simplify=FALSE) + } +}) diff --git a/man/utils.Rd b/man/utils.Rd index 57d41527..cc478d2b 100644 --- a/man/utils.Rd +++ b/man/utils.Rd @@ -3,10 +3,13 @@ \name{utils} \alias{utils} \alias{centroids} +\alias{extent} \alias{centroids,ANY-method} \alias{centroids,LabelArray-method} \alias{centroids,ShapeFrame-method} \alias{centroids,PointFrame-method} +\alias{extent,SpatialData-method} +\alias{extent,SpatialDataElement-method} \title{Utilities} \usage{ \S4method{centroids}{ANY}(x, ...) @@ -16,6 +19,10 @@ \S4method{centroids}{ShapeFrame}(x, as = c("data.frame", "matrix", "list")) \S4method{centroids}{PointFrame}(x, as = c("data.frame", "list")) + +\S4method{extent}{SpatialData}(x) + +\S4method{extent}{SpatialDataElement}(x) } \arguments{ \item{x}{a \code{SpatialData} element (any but image)} @@ -39,4 +46,12 @@ xy <- centroids(point(x), "list") plot(xy$gene_a, col=a <- "red") points(xy$gene_b, col=b <- "blue") legend("topright", legend=names(xy), col=c(a, b), pch=21) + +# object-wide +extent(x) + +# element-wise +extent(label(x)) +extent(point(x)) +extent(shape(x)) } diff --git a/tests/testthat/test-query.R b/tests/testthat/test-query.R index 7964b12a..0490f584 100644 --- a/tests/testthat/test-query.R +++ b/tests/testthat/test-query.R @@ -38,6 +38,9 @@ test_that("query,.check_pol", { test_that("query,ImageArray", { d <- dim(i <- image(x)) + # unsupported query + y <- matrix(seq_len(8), 4, 2) + expect_error(query(i, y)) # query equals dimensions y <- list(xmin=0, xmax=d[3], ymin=0, ymax=d[2]) expect_identical(query(i, y), i) From 942210a0f5c5d5cf72ffffdd0ef7f064b02451c7 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 20:11:30 +0200 Subject: [PATCH 108/151] bug fix in trans example --- R/trans.R | 2 +- R/utils.R | 1 + man/trans.Rd | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/R/trans.R b/R/trans.R index 34e5eb3c..c4eb6eef 100644 --- a/R/trans.R +++ b/R/trans.R @@ -16,7 +16,7 @@ #' #' # image #' y <- x -#' image(y) <- scale(image(y), 1, c(1, 1, 1/3)) +#' image(y) <- scale(image(y), c(1, 1, 1/3)) #' CTpath(image(x), "global") #' CTpath(image(y), "global") #' diff --git a/R/utils.R b/R/utils.R index 767c85de..e2706093 100644 --- a/R/utils.R +++ b/R/utils.R @@ -42,6 +42,7 @@ setMethod("centroids", "ANY", \(x, ...) stop("'centroids' ", #' @importFrom Matrix summary setMethod("centroids", "LabelArray", \(x, as=c("data.frame", "matrix")) { + # TODO: should these be offset by 0.5? as <- match.arg(as) y <- data(x) y <- as(y, "dgCMatrix") diff --git a/man/trans.Rd b/man/trans.Rd index ca4617ff..35c4107a 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -45,7 +45,7 @@ x <- readSpatialData(x, tables=FALSE) # image y <- x -image(y) <- scale(image(y), 1, c(1, 1, 1/3)) +image(y) <- scale(image(y), c(1, 1, 1/3)) CTpath(image(x), "global") CTpath(image(y), "global") From 243bf19881ed5e721e5d718622ce75fa42206dfb Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 22:43:20 +0200 Subject: [PATCH 109/151] +flip,flop,mirror; revised image/label trans --- NAMESPACE | 3 + R/AllGenerics.R | 4 ++ R/sdArray.R | 3 +- R/trans.R | 63 +++++++++++++++---- man/trans.Rd | 26 +++++++- tests/testthat/test-trans.R | 118 +++++++++++++++++++++--------------- 6 files changed, 152 insertions(+), 65 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index a2308f7e..721c03b6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -55,6 +55,8 @@ exportMethods(dim) exportMethods(element) exportMethods(extent) exportMethods(feature_key) +exportMethods(flip) +exportMethods(flop) exportMethods(geom_type) exportMethods(getTable) exportMethods(hasTable) @@ -68,6 +70,7 @@ exportMethods(layer) exportMethods(length) exportMethods(mask) exportMethods(meta) +exportMethods(mirror) exportMethods(names) exportMethods(point) exportMethods(pointNames) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 831c8389..d1c46285 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -57,6 +57,10 @@ setGeneric("rotate", \(x, t, ...) standardGeneric("rotate")) setGeneric("transform", \(x, ...) standardGeneric("transform")) setGeneric("translation", \(x, t, ...) standardGeneric("translation")) +setGeneric("flip", \(x, ...) standardGeneric("flip")) +setGeneric("flop", \(x, ...) standardGeneric("flop")) +setGeneric("mirror", \(x, ...) standardGeneric("mirror")) + # sda ---- setGeneric("feature_key", \(x, ...) standardGeneric("feature_key")) diff --git a/R/sdArray.R b/R/sdArray.R index 26a89bee..ea0e4662 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -49,8 +49,9 @@ setMethod("dim", "sdArray", \(x) dim(data(x))) #' @export setMethod("length", "sdArray", \(x) length(data(x, NULL))) -#' @rdname Array-methods #' @export +#' @rdname Array-methods +#' @importFrom S4Vectors metadata setMethod("data_type", "sdArray", \(x) { if (is(y <- data(x), "DelayedArray")) data_type(y) else metadata(x)$data_type diff --git a/R/trans.R b/R/trans.R index c4eb6eef..3e09995c 100644 --- a/R/trans.R +++ b/R/trans.R @@ -1,12 +1,14 @@ #' @name trans #' @rdname trans #' @title Transformations -#' @aliases scale rotate translation +#' @aliases scale rotate translation flip flop mirror #' #' @param x \code{SpatialData} element #' @param j scalar character or numeric; #' name or index of coordinate space. -#' @param t transformation data. +#' @param t transformation data; exceptions: for \code{mirror}, controls +#' whether to perform \bold{v}ertical or \bold{h}orizontal reflection; +#' no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}). #' @param ... option arguments passed to and from other methods. #' #' @examples @@ -17,8 +19,8 @@ #' # image #' y <- x #' image(y) <- scale(image(y), c(1, 1, 1/3)) -#' CTpath(image(x), "global") -#' CTpath(image(y), "global") +#' dim(image(x)) +#' dim(image(y)) #' #' # point #' y <- x @@ -43,32 +45,58 @@ NULL # array ---- +.mirror <- \(x, t) { + y <- aperm(data(x), c(1, 2, 3)) + x@data <- list(y) + rotate(x, t) +} + +#' @export +#' @rdname trans +setMethod("mirror", "sdArray", \(x, t=c("v", "h"), ...) + switch(match.arg(t), v=flip, h=flop)(x)) + +#' @export +#' @rdname trans +setMethod("flip", "sdArray", \(x, ...) .mirror(x, -90)) + +#' @export +#' @rdname trans +setMethod("flop", "sdArray", \(x, ...) .mirror(x, 90)) + #' @importFrom methods as #' @importFrom S4Vectors metadata<- .trans_a <- \(x, f) { a <- f(aperm(as.array(data(x)))) + metadata(x)$data_type <- data_type(x) x@data <- list(as(aperm(a), "SparseArray")) - metadata(x) <- list(data_type=data_type(x)) return(x) } +#' @export +#' @rdname trans #' @importFrom EBImage resize setMethod("scale", c("sdArray", "numeric"), \(x, t, ...) { stopifnot(length(t) == length(dim(x)), is.finite(t), t > 0) if (all(t == 1)) return(x) - d <- length(dim(a)) + n <- length(d <- dim(x)) f <- \(.) resize(., - w=dim(.)[d]*t[d], - h=dim(.)[d-1]*t[d-1]) + w=d[n]*t[n], + h=d[n-1]*t[n-1]) .trans_a(x, f) }) +#' @export +#' @rdname trans #' @importFrom EBImage rotate setMethod("rotate", c("sdArray", "numeric"), \(x, t, ...) { # negate angle since 'EBImage' rotates clockwise stopifnot(length(t) == 1, is.finite(t)) if (t %% 360 == 0) return(x) - f <- \(.) rotate(., -t) + f <- \(.) EBImage::rotate(., -t) + metadata(x)$wh <- lapply( + rev(dim(x)[-1]), \(.) + c(c(0, .) %*% .R(t*pi/180))) .trans_a(x, f) }) @@ -76,15 +104,24 @@ setMethod("rotate", c("sdArray", "numeric"), \(x, t, ...) { setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { stopifnot(length(t) == length(dim(x)), is.finite(t)) if (all(t == 0)) return(x) - f <- \(.) translate(., t[-1]) - .trans_a(x, f) + if (length(d <- dim(x)) == 3) { + # protect non-spatial dim. + t <- t[-1]; d <- d[-1] + } + t <- rev(t); d <- rev(d) + metadata(x)$wh <- list( + c(t[1], t[1]+d[1]), + c(t[2], t[2]+d[2])) + #f <- \(.) translate(., t, output.dim=d+t) + #.trans_a(x, f) + x }) # point ---- +#' @export #' @rdname trans #' @importFrom dplyr mutate -#' @export setMethod("scale", c("PointFrame", "numeric"), \(x, t, ...) { stopifnot(is.numeric(t), length(t) == 2, t > 0, is.finite(t)) if (all(t == 1)) return(x) @@ -95,9 +132,9 @@ setMethod("scale", c("PointFrame", "numeric"), \(x, t, ...) { return(x) }) +#' @export #' @rdname trans #' @importFrom dplyr mutate select -#' @export setMethod("rotate", c("PointFrame", "numeric"), \(x, t, ...) { stopifnot(is.numeric(t), length(t) == 1, is.finite(t)) if (t %% 360 == 0) return(x) diff --git a/man/trans.Rd b/man/trans.Rd index 35c4107a..8bb43142 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -5,6 +5,14 @@ \alias{scale} \alias{rotate} \alias{translation} +\alias{flip} +\alias{flop} +\alias{mirror} +\alias{mirror,sdArray-method} +\alias{flip,sdArray-method} +\alias{flop,sdArray-method} +\alias{scale,sdArray,numeric-method} +\alias{rotate,sdArray,numeric-method} \alias{scale,PointFrame,numeric-method} \alias{rotate,PointFrame,numeric-method} \alias{translation,PointFrame,numeric-method} @@ -13,6 +21,16 @@ \alias{translation,ShapeFrame,numeric-method} \title{Transformations} \usage{ +\S4method{mirror}{sdArray}(x, t = c("v", "h"), ...) + +\S4method{flip}{sdArray}(x, ...) + +\S4method{flop}{sdArray}(x, ...) + +\S4method{scale}{sdArray,numeric}(x, t, ...) + +\S4method{rotate}{sdArray,numeric}(x, t, ...) + \S4method{scale}{PointFrame,numeric}(x, t, ...) \S4method{rotate}{PointFrame,numeric}(x, t, ...) @@ -28,7 +46,9 @@ \arguments{ \item{x}{\code{SpatialData} element} -\item{t}{transformation data.} +\item{t}{transformation data; exceptions: for \code{mirror}, controls +whether to perform \bold{v}ertical or \bold{h}orizontal reflection; +no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}).} \item{...}{option arguments passed to and from other methods.} @@ -46,8 +66,8 @@ x <- readSpatialData(x, tables=FALSE) # image y <- x image(y) <- scale(image(y), c(1, 1, 1/3)) -CTpath(image(x), "global") -CTpath(image(y), "global") +dim(image(x)) +dim(image(y)) # point y <- x diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index 4d6e9301..3bbfe64d 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -2,55 +2,77 @@ zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) -# test_that("translation,imageArray", { -# x <- image(sd, 1) -# y <- translation(x, c(0,0,0)) -# expect_identical(x, y) -# expect_error(translation(x, numeric(2))) -# expect_error(translation(x, numeric(4))) -# expect_error(translation(x, character(3))) -# # 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) -# }) +test_that("translation,imageArray", { + x <- image(sd, 1) + # identity + y <- translation(x, c(0,0,0)) + expect_identical(x, y) + expect_null(metadata(y)$wh) + # invalid + expect_error(translation(x, numeric(2))) + expect_error(translation(x, numeric(4))) + expect_error(translation(x, character(3))) + # row + t <- c(0,n <- sample(77, 1),0) + z <- translation(y <- x[,-1,-c(1,2)], t) + expect_equal(dim(z), dim(y)) + expect_is(data(z), "DelayedArray") + md <- metadata(z)$wh + expect_is(md, "list") + expect_is(unlist(md), "numeric") + expect_equal(md[[1]], c(0, dim(y)[3])) + expect_equal(md[[2]], c(n, dim(y)[2]+n)) + # col + t <- c(0,0,n <- sample(77, 1)) + z <- translation(y <- x[,-1,-c(1,2)], t) + expect_equal(dim(z), dim(y)) + expect_is(data(z), "DelayedArray") + md <- metadata(z)$wh + expect_is(md, "list") + expect_is(unlist(md), "numeric") + expect_equal(md[[1]], c(n, dim(y)[3]+n)) + expect_equal(md[[2]], c(0, dim(y)[2])) +}) -# test_that("translation,labelArray", { -# x <- label(sd, 1) -# y <- translation(x, c(0,0)) -# expect_identical(x, y) -# expect_error(translation(x, numeric(1))) -# expect_error(translation(x, numeric(3))) -# expect_error(translation(x, character(2))) -# # row -# t <- c(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, 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) -# # res -# x <- label(sd, 2) -# t <- c(n <- nrow(x), 0) -# y <- translation(x, t) -# dx <- vapply(data(x, NULL), dim, integer(2)) -# dy <- vapply(data(y, NULL), dim, integer(2)) -# expect_equal(dx[1,], dy[1,]/2) -# expect_identical(dx[2,], dy[2,]) -# }) +test_that("translation,labelArray", { + x <- label(sd, 1) + # identity + y <- translation(x, c(0,0)) + expect_identical(x, y) + expect_null(metadata(y)$wh) + # invalid + expect_error(translation(x, numeric(1))) + expect_error(translation(x, numeric(3))) + expect_error(translation(x, character(2))) + # row + t <- c(n <- sample(77, 1), 0) + z <- translation(y <- x[-1,-c(1,2)], t) + expect_equal(dim(z), dim(y)) + expect_is(data(z), "DelayedArray") + md <- metadata(z)$wh + expect_is(md, "list") + expect_is(unlist(md), "numeric") + expect_equal(md[[1]], c(0, dim(y)[2])) + expect_equal(md[[2]], c(n, dim(y)[1]+n)) + # col + t <- c(0, n <- sample(77, 1)) + z <- translation(y <- x[-1,-c(1,2)], t) + expect_equal(dim(z), dim(y)) + expect_is(data(z), "DelayedArray") + md <- metadata(z)$wh + expect_is(md, "list") + expect_is(unlist(md), "numeric") + expect_equal(md[[1]], c(n, dim(y)[2]+n)) + expect_equal(md[[2]], c(0, dim(y)[1])) + # TODO: multiscale + # x <- label(sd, 2) + # t <- c(n <- nrow(x), 0) + # y <- translation(x, t) + # dx <- vapply(data(x, NULL), dim, integer(2)) + # dy <- vapply(data(y, NULL), dim, integer(2)) + # expect_equal(dx[1,], dy[1,]/2) + # expect_identical(dx[2,], dy[2,]) +}) test_that("translation,PointFrame", { x <- point(sd, 1) From bd7057d3867a3e70c3b350c6f23b28242d128263 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 23:07:15 +0200 Subject: [PATCH 110/151] tests for flip/flip/mirror --- R/ShapeFrame.R | 4 ++-- R/trans.R | 9 +++++---- tests/testthat/test-trans.R | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index 38e4e198..838ce7fd 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -65,8 +65,8 @@ setMethod("$", "ShapeFrame", \(x, name) data(x)[[name]]) #' @export #' @importFrom sf st_as_sf st_geometry_type setMethod("geom_type", "ShapeFrame", \(x) { - y <- st_as_sf(data(x)) - z <- st_geometry_type(y[1, ]) + y <- st_as_sf(data(x[1, ])) + z <- st_geometry_type(y) return(as.character(z)) }) diff --git a/R/trans.R b/R/trans.R index 3e09995c..46874db8 100644 --- a/R/trans.R +++ b/R/trans.R @@ -46,7 +46,9 @@ NULL # array ---- .mirror <- \(x, t) { - y <- aperm(data(x), c(1, 2, 3)) + d <- length(dim(x)) == 3 + i <- if (d) c(1, 3, 2) else c(2, 1) + y <- aperm(data(x), i) x@data <- list(y) rotate(x, t) } @@ -94,9 +96,8 @@ setMethod("rotate", c("sdArray", "numeric"), \(x, t, ...) { stopifnot(length(t) == 1, is.finite(t)) if (t %% 360 == 0) return(x) f <- \(.) EBImage::rotate(., -t) - metadata(x)$wh <- lapply( - rev(dim(x)[-1]), \(.) - c(c(0, .) %*% .R(t*pi/180))) + if (length(d <- dim(x)) == 3) d <- d[-1] + metadata(x)$wh <- lapply(rev(d), \(.) c(c(0, .) %*% .R(-t*pi/180))) .trans_a(x, f) }) diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index 3bbfe64d..71bb6579 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -2,6 +2,23 @@ zs <- file.path("extdata", "blobs.zarr") zs <- system.file(zs, package="SpatialData") sd <- readSpatialData(zs, tables=FALSE) +test_that("mirror,sdArray", { + x <- label(sd, 1)[-1,-c(1,2)] + expect_error(mirror(x, "x")) + expect_identical(mirror(x, "v"), flip(x)) + expect_identical(mirror(x, "h"), flop(x)) + # vertical reflection + y <- flip(x) + expect_identical(dim(y), dim(x)) + expect_equal(data(y)[1, ], rev(data(x)[1, ])) + expect_equal(data(y)[, 1], data(x)[, ncol(x)]) + # horizontal reflection + y <- flop(x) + expect_identical(dim(y), dim(x)) + expect_equal(data(y)[, 1], rev(data(x)[, 1])) + expect_equal(data(y)[1, ], data(x)[nrow(x), ]) +}) + test_that("translation,imageArray", { x <- image(sd, 1) # identity From 8cef9bd0f8fff7e0e38eff02392817de2d5b7b26 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 23:28:48 +0200 Subject: [PATCH 111/151] make copi happier --- R/ShapeFrame.R | 1 + man/ShapeFrame.Rd | 1 + tests/testthat/test-utils.R | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index 838ce7fd..b8e11a51 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -1,5 +1,6 @@ #' @name ShapeFrame #' @title The `ShapeFrame` class +#' @aliases geom_type #' #' @param x \code{ShapeFrame} #' @param data \code{arrow}-derived table for on-disk, diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index 55ae132e..8eaa3eb3 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/ShapeFrame.R \name{ShapeFrame} \alias{ShapeFrame} +\alias{geom_type} \alias{dim,ShapeFrame-method} \alias{length,ShapeFrame-method} \alias{names,ShapeFrame-method} diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index c689da51..5b34de5b 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1,4 +1,5 @@ xy <- c("x", "y") +require(sf, quietly=TRUE) x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) @@ -15,7 +16,6 @@ test_that("centroids,LabelArray", { z$i <- as.integer(as.character(z$i)) expect_identical(.z, as.matrix(z)) }) - test_that("centroids,PointFrame", { i <- feature_key(y <- point(x)) z <- centroids(y, "data.frame") @@ -30,7 +30,6 @@ test_that("centroids,PointFrame", { for (. in names(.z)) expect_identical( .z[[.]][xy], z[z[[i]] == ., xy]) }) - test_that("centroids,ShapeFrame", { # circle y <- shape(x) @@ -63,3 +62,37 @@ test_that("centroids,ShapeFrame", { z[[.]] <- as.integer(as.character(z[[.]])) expect_identical(.z, as.matrix(z)) }) + +test_that("extent,ImageArray", { + z <- extent(y <- image(x)[,-1,-c(1,2)]) + expect_is(z, "list") + expect_is(unlist(z), "numeric") + expect_identical(names(z), rev(xy)) + expect_identical(z$y, c(0, dim(y)[2])) + expect_identical(z$x, c(0, dim(y)[3])) +}) +test_that("extent,LabelArray", { + z <- extent(y <- label(x)[,-1,-c(1,2)]) + expect_is(z, "list") + expect_is(unlist(z), "numeric") + expect_identical(names(z), rev(xy)) + expect_identical(z$y, c(0, dim(y)[1])) + expect_identical(z$x, c(0, dim(y)[2])) +}) +test_that("extent,PointFraome", { + z <- extent(y <- point(x)) + expect_is(z, "list") + expect_identical(names(z), xy) + expect_is(unlist(z), "integer") + expect_identical(z$x, range(y$x)) + expect_identical(z$y, range(y$y)) +}) +test_that("extent,ShapeFrame", { + z <- extent(y <- shape(x)) + expect_is(z, "list") + expect_identical(names(z), xy) + expect_is(unlist(z), "numeric") + mx <- st_coordinates(st_as_sf(data(y))) + expect_identical(z$x, range(mx[, 1])) + expect_identical(z$y, range(mx[, 2])) +}) From f3dba2f8f4a767a6bb8e53893f5ce84a9698a189 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Thu, 9 Apr 2026 23:57:12 +0200 Subject: [PATCH 112/151] add GHA badge to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4ac6b05a..b5faa35e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # `SpatialData` +[![build](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) + > for a demo of the class, see the [vignette](https://htmlpreview.github.io/?https://github.com/HelenaLC/SpatialData/blob/main/vignettes/SpatialData.html) > for visualization capabilites, see [`SpatialData.plot`](https://github.com/HelenaLC/SpatialData.plot) From 6afcc2d23c5e84bba39d089a3541f836beb7ccbc Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 00:03:55 +0200 Subject: [PATCH 113/151] test w/ and w/o push on badge --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5faa35e..23df15ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # `SpatialData` -[![build](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) +[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) + +[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main?event=push)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) > for a demo of the class, see the [vignette](https://htmlpreview.github.io/?https://github.com/HelenaLC/SpatialData/blob/main/vignettes/SpatialData.html) From f5700c7cda4b2768eb9bb7132c2e8efc8b56a547 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 09:23:39 +0200 Subject: [PATCH 114/151] allow trans on different scales --- R/trans.R | 85 +++++++++++-------------------------- man/trans.Rd | 14 +++--- tests/testthat/test-utils.R | 5 +++ 3 files changed, 38 insertions(+), 66 deletions(-) diff --git a/R/trans.R b/R/trans.R index 46874db8..bb303385 100644 --- a/R/trans.R +++ b/R/trans.R @@ -9,6 +9,9 @@ #' @param t transformation data; exceptions: for \code{mirror}, controls #' whether to perform \bold{v}ertical or \bold{h}orizontal reflection; #' no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}). +#' @param k scalar index specifying which scale to use; +#' \code{Inf} to use lowest available resolution; +#' only applies to \code{sdArray}s (images, labels). #' @param ... option arguments passed to and from other methods. #' #' @examples @@ -43,33 +46,35 @@ #' y["shapes", c("rot", "wide", "left")] NULL +# rotation matrix to rotate points counter-clockwise through an angle 't' +.R <- \(t) matrix(c(cos(t), sin(t), -sin(t), cos(t)), 2, 2) + # array ---- -.mirror <- \(x, t) { +.mirror <- \(x, t, k=1) { d <- length(dim(x)) == 3 i <- if (d) c(1, 3, 2) else c(2, 1) - y <- aperm(data(x), i) - x@data <- list(y) - rotate(x, t) + x@data <- list(aperm(data(x, k), i)) + rotate(x, t, k=1) } #' @export #' @rdname trans -setMethod("mirror", "sdArray", \(x, t=c("v", "h"), ...) - switch(match.arg(t), v=flip, h=flop)(x)) +setMethod("mirror", "sdArray", \(x, t=c("v", "h"), k=1, ...) + switch(match.arg(t), v=flip, h=flop)(x, k)) #' @export #' @rdname trans -setMethod("flip", "sdArray", \(x, ...) .mirror(x, -90)) +setMethod("flip", "sdArray", \(x, k=1, ...) .mirror(x, -90, k)) #' @export #' @rdname trans -setMethod("flop", "sdArray", \(x, ...) .mirror(x, 90)) +setMethod("flop", "sdArray", \(x, k=1, ...) .mirror(x, 90, k)) #' @importFrom methods as #' @importFrom S4Vectors metadata<- -.trans_a <- \(x, f) { - a <- f(aperm(as.array(data(x)))) +.trans_a <- \(x, f, k=1) { + a <- f(aperm(as.array(data(x, k)))) metadata(x)$data_type <- data_type(x) x@data <- list(as(aperm(a), "SparseArray")) return(x) @@ -78,34 +83,35 @@ setMethod("flop", "sdArray", \(x, ...) .mirror(x, 90)) #' @export #' @rdname trans #' @importFrom EBImage resize -setMethod("scale", c("sdArray", "numeric"), \(x, t, ...) { +setMethod("scale", c("sdArray", "numeric"), \(x, t, k=1, ...) { stopifnot(length(t) == length(dim(x)), is.finite(t), t > 0) if (all(t == 1)) return(x) - n <- length(d <- dim(x)) + n <- length(d <- dim(data(x, k))) f <- \(.) resize(., w=d[n]*t[n], h=d[n-1]*t[n-1]) - .trans_a(x, f) + .trans_a(x, f, k) }) #' @export #' @rdname trans #' @importFrom EBImage rotate -setMethod("rotate", c("sdArray", "numeric"), \(x, t, ...) { +setMethod("rotate", c("sdArray", "numeric"), \(x, t, k=1,...) { # negate angle since 'EBImage' rotates clockwise stopifnot(length(t) == 1, is.finite(t)) if (t %% 360 == 0) return(x) f <- \(.) EBImage::rotate(., -t) - if (length(d <- dim(x)) == 3) d <- d[-1] + if (length(d <- dim(data(x, k))) == 3) d <- d[-1] metadata(x)$wh <- lapply(rev(d), \(.) c(c(0, .) %*% .R(-t*pi/180))) - .trans_a(x, f) + .trans_a(x, f, k) }) #' @importFrom EBImage translate -setMethod("translation", c("sdArray", "numeric"), \(x, t, ...) { +setMethod("translation", c("sdArray", "numeric"), \(x, t, k=1, ...) { stopifnot(length(t) == length(dim(x)), is.finite(t)) if (all(t == 0)) return(x) - if (length(d <- dim(x)) == 3) { + d <- dim(data(x, k)) + if (length(d) == 3) { # protect non-spatial dim. t <- t[-1]; d <- d[-1] } @@ -199,46 +205,3 @@ setMethod("translation", c("ShapeFrame", "numeric"), \(x, t, ...) { stopifnot(is.numeric(t), length(t) == 2, is.finite(t)) .trans_s(x, \(xy) sweep(xy, 2, t, `+`)) }) - -# utils ---- - -# rotation matrix to rotate points counter-clockwise through an angle 't' -.R <- \(t) matrix(c(cos(t), sin(t), -sin(t), cos(t)), 2, 2) - -# count occurrences of each coordinate space; -# return most frequent (in order of appearance) -.guess_space <- \(x) { - nms <- lapply(rownames(x), \(l) - lapply(colnames(x)[[l]], \(e) - CTname(x[[l]][[e]]))) - cnt <- table(nms <- unlist(nms)) - cnt <- cnt[unique(nms)] - names(which.max(cnt)) -} - -.trans_xy <- \(xy, ts, rev=FALSE) { - if (rev) ts <- rev(ts) - for (. in seq_along(ts)) { - t <- ts[[.]]$type - d <- ts[[.]]$data - d <- unlist(d) - if (length(d) == 3) - d <- d[-1] - switch(t, - identity={}, - scale={ - op <- ifelse(rev, `/`, `*`) - xy$x <- op(xy$x, d[2]) - xy$y <- op(xy$y, d[1]) - }, - rotate={ - xy <- xy %*% .R(d*pi/180) - }, - translation={ - op <- ifelse(rev, `-`, `+`) - xy$x <- op(xy$x, d[2]) - xy$y <- op(xy$y, d[1]) - }) - } - return(xy) -} diff --git a/man/trans.Rd b/man/trans.Rd index 8bb43142..4378d4eb 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -21,15 +21,15 @@ \alias{translation,ShapeFrame,numeric-method} \title{Transformations} \usage{ -\S4method{mirror}{sdArray}(x, t = c("v", "h"), ...) +\S4method{mirror}{sdArray}(x, t = c("v", "h"), k = 1, ...) -\S4method{flip}{sdArray}(x, ...) +\S4method{flip}{sdArray}(x, k = 1, ...) -\S4method{flop}{sdArray}(x, ...) +\S4method{flop}{sdArray}(x, k = 1, ...) -\S4method{scale}{sdArray,numeric}(x, t, ...) +\S4method{scale}{sdArray,numeric}(x, t, k = 1, ...) -\S4method{rotate}{sdArray,numeric}(x, t, ...) +\S4method{rotate}{sdArray,numeric}(x, t, k = 1, ...) \S4method{scale}{PointFrame,numeric}(x, t, ...) @@ -50,6 +50,10 @@ whether to perform \bold{v}ertical or \bold{h}orizontal reflection; no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}).} +\item{k}{scalar index specifying which scale to use; +\code{Inf} to use lowest available resolution; +only applies to \code{sdArray}s (images, labels).} + \item{...}{option arguments passed to and from other methods.} \item{j}{scalar character or numeric; diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 5b34de5b..99c3b9c1 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -4,6 +4,11 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) +image(x, "x") <- flip(image(x, 2), k=1) +image(x, "y") <- flip(image(x, 2), k=3) +plotSpatialData() + plotImage(x, "x") + +plotSpatialData() + plotImage(x, "y") + test_that("centroids,LabelArray", { y <- label(x) z <- centroids(y, "data.frame") From fc41893448e137b95ee1bc67fb16a58322b8bba8 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 09:35:13 +0200 Subject: [PATCH 115/151] fix skipped/broken tests --- tests/testthat/test-tables.R | 40 +++++++++++++++--------------------- tests/testthat/test-utils.R | 5 ----- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/tests/testthat/test-tables.R b/tests/testthat/test-tables.R index ae0ab567..07ed4ff3 100644 --- a/tests/testthat/test-tables.R +++ b/tests/testthat/test-tables.R @@ -1,10 +1,10 @@ -oo = options()$arrow.pull_as_vector - -options(arrow.pull_as_vector=TRUE) require(SingleCellExperiment, quietly=TRUE) +oo <- options()$arrow.pull_as_vector +options(arrow.pull_as_vector=TRUE) + x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, table=1, anndataR=TRUE) +x <- readSpatialData(x, anndataR=TRUE) md <- int_metadata(SpatialData::table(x)) md <- md$spatialdata_attrs @@ -32,7 +32,6 @@ test_that("hasTable()", { expect_error(hasTable(setTable(x, i), i, name=TRUE)) # many }) -if (FALSE) { test_that("getTable()", { # invalid expect_error(getTable(x, 123)) @@ -46,21 +45,16 @@ test_that("getTable()", { expect_error(getTable(x, i, ".")) expect_error(getTable(x, i, c(TRUE, FALSE))) # alter 'region' of a couple random observations - s <- t - . <- sample(ncol(s), 2) - # TODO: check the replacement of this test below - # int_colData(s)[[rk]][.] <- "." - tmp <- as.character(colData(s)[[rk]]) - tmp[.] <- "." - colData(s)[[rk]] <- tmp - SpatialData::table(x) <- s + s <- t; y <- x + int_colData(s)[[rk]] <- paste(int_colData(s)[[rk]]) + int_colData(s)[[rk]][. <- sample(ncol(s), 2)] <- "." + SpatialData::table(y) <- s # these should be gone when 'drop=TRUE' - t1 <- getTable(x, i, drop=FALSE) - t2 <- getTable(x, i, drop=TRUE) + t1 <- getTable(y, i, drop=FALSE) + t2 <- getTable(y, i, drop=TRUE) expect_identical(t1, s) expect_identical(t2, s[, -.]) }) -} test_that("setTable(),labels", { # invalid 'i' @@ -124,13 +118,13 @@ test_that("valTable()", { expect_error(valTable(x, i, 123)) expect_error(valTable(x, i, sample(rownames(t), 2))) expect_error(valTable(x, i, sample(names(colData(t)), 2))) - # # 'colData' - # df <- DataFrame(a=sample(letters, n), b=runif(n), - # region = valTable(x, i, j <- "region")) - # s <- t; colData(s) <- df; y <- x; SpatialData::table(y) <- s - # expect_identical(valTable(y, i, j <- "a"), s[[j]]) - # expect_identical(valTable(y, i, j <- "b"), s[[j]]) - # expect_error(valTable(y, i, "c")) + # 'colData' + cd <- DataFrame(a=sample(letters, n), b=runif(n)) + s <- t; colData(s) <- cd + y <- x; SpatialData::table(y) <- s + expect_identical(valTable(y, i, j <- "a"), s[[j]]) + expect_identical(valTable(y, i, j <- "b"), s[[j]]) + expect_error(valTable(y, i, "c")) # 'assay' data j <- sample(rownames(t), 1) v <- valTable(x, i, j) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 99c3b9c1..5b34de5b 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -4,11 +4,6 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, tables=FALSE) -image(x, "x") <- flip(image(x, 2), k=1) -image(x, "y") <- flip(image(x, 2), k=3) -plotSpatialData() + plotImage(x, "x") + -plotSpatialData() + plotImage(x, "y") - test_that("centroids,LabelArray", { y <- label(x) z <- centroids(y, "data.frame") From d95d33a1c8095ca199db0da88910149b23436daa Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 09:59:39 +0200 Subject: [PATCH 116/151] init spatialdata_attrs utils --- NAMESPACE | 3 +++ R/AllGenerics.R | 3 +++ R/Zattrs.R | 18 ++++++++++++++++-- man/SpatialData.Rd | 8 ++++---- tests/testthat/test-tables.R | 18 ++++++------------ 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 721c03b6..b0081863 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -63,6 +63,7 @@ exportMethods(hasTable) exportMethods(image) exportMethods(imageNames) exportMethods(images) +exportMethods(instance_key) exportMethods(label) exportMethods(labelNames) exportMethods(labels) @@ -76,6 +77,8 @@ exportMethods(point) exportMethods(pointNames) exportMethods(points) exportMethods(query) +exportMethods(region) +exportMethods(region_key) exportMethods(rmvCT) exportMethods(rotate) exportMethods(rownames) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index d1c46285..cc2107a1 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -63,7 +63,10 @@ setGeneric("mirror", \(x, ...) standardGeneric("mirror")) # sda ---- +setGeneric("region", \(x, ...) standardGeneric("region")) +setGeneric("region_key", \(x, ...) standardGeneric("region_key")) setGeneric("feature_key", \(x, ...) standardGeneric("feature_key")) +setGeneric("instance_key", \(x, ...) standardGeneric("instance_key")) # uts ---- diff --git a/R/Zattrs.R b/R/Zattrs.R index d21bec29..9bae799b 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -80,8 +80,22 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) } setMethod("show", "Zattrs", .showZattrs) +# TODO: only points can have this? #' @export -setMethod("feature_key", "Zattrs", \(x) x$spatialdata_attrs$feature_key) +setMethod("feature_key", "list", \(x) x$spatialdata_attrs$feature_key) +#' @export +setMethod("feature_key", "PointFrame", \(x) feature_key(meta(x))) + +# TODO: only tables can have this? +#' @export +setMethod("region_key", "SingleCellExperiment", \(x) meta(x)$region_key) +#' @export +setMethod("region", "SingleCellExperiment", \(x) meta(x)[[region_key(x)]]) +# TODO: only tables and points can have this? +#' @export +setMethod("instance_key", "list", \(x) x$instance_key) +#' @export +setMethod("instance_key", "PointFrame", \(x) instance_key(meta(x)$spatialdata_attrs)) #' @export -setMethod("feature_key", "SpatialDataElement", \(x) feature_key(meta(x))) \ No newline at end of file +setMethod("instance_key", "SingleCellExperiment", \(x) instance_key(meta(x))) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/tests/testthat/test-tables.R b/tests/testthat/test-tables.R index 07ed4ff3..86c87649 100644 --- a/tests/testthat/test-tables.R +++ b/tests/testthat/test-tables.R @@ -6,15 +6,12 @@ x <- file.path("extdata", "blobs.zarr") x <- system.file(x, package="SpatialData") x <- readSpatialData(x, anndataR=TRUE) -md <- int_metadata(SpatialData::table(x)) -md <- md$spatialdata_attrs -i <- md[[rk <- md$region_key]] -#int_colData(SpatialData::table(x))[[rk]] <- -# paste(int_colData(SpatialData::table(x))[[rk]]) +se <- SpatialData::table(x) +i <- md[[rk <- region_key(se)]] test_that("hasTable()", { # TRUE - i <- md$region + i <- region(table(x)) expect_true(hasTable(x, i)) # FALSE j <- setdiff(unlist(colnames(x)), c(i, tableNames(x))) @@ -85,8 +82,7 @@ test_that("setTable(),points/shapes", { n <- switch(., shape=length(y), point={ - md <- meta(y)$spatialdata_attrs - ik <- md$instance_key + ik <- instance_key(y) n <- length(unique(pull(data(y), ik))) }) df <- data.frame(foo=runif(n)) @@ -95,8 +91,7 @@ test_that("setTable(),points/shapes", { expect_true(hasTable(y, i)) t <- getTable(y, i) expect_identical(t$foo, df$foo) - md <- int_metadata(t)$spatialdata_attrs - expect_identical(md$region, i) + #expect_identical(region(t), i) # dots = list of functions f <- list( numbers=\(n) runif(n), @@ -106,8 +101,7 @@ test_that("setTable(),points/shapes", { expect_true(hasTable(y, i)) t <- getTable(y, i) expect_true(all(names(f) %in% names(colData(t)))) - md <- int_metadata(t)$spatialdata_attrs - expect_identical(md$region, i) + #expect_identical(region(t), i) } }) From 3cd909759aca2287ad75c11c4ea159d6a792b109 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 10:56:26 +0200 Subject: [PATCH 117/151] track changes + v0.99.28 bump --- DESCRIPTION | 2 +- R/validity.R | 45 +++++++++++++++++++++++++---------- inst/NEWS | 6 +++++ man/SpatialData.Rd | 8 +++---- tests/testthat/test-reading.R | 2 +- 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a70ccca0..c07cb169 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R Depends: R (>= 4.5) -Version: 0.99.27 +Version: 0.99.28 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufacturer data and writing to .zarr, on-disk representation of images/labels as diff --git a/R/validity.R b/R/validity.R index 15b36d52..b174c44c 100644 --- a/R/validity.R +++ b/R/validity.R @@ -1,19 +1,38 @@ -# TODO: everything... - -.validateTable <- function(object) { +# https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions +#' @importFrom SingleCellExperiment int_metadata int_colData +.validateTables <- \(object) { msg <- c() for (i in seq_along(tables(object))) { - t <- table(object, i) - if (!is(t, "SingleCellExperiment")) { - msg <- c(msg, paste0("Table ", i, " is not a 'SingleCellExperiment'")) + se <- table(object, i) + ok <- is(se, "SingleCellExperiment") + if (!ok) msg <- c(msg, paste0( + i, "-th table is not a 'SingleCellExperiment'")) + md <- int_metadata(se)$spatialdata_attrs + nm <- c("region", "region_key", "instance_key") + .nm <- sprintf("'%s'", paste(nm, collapse="/")) + if (any(ok <- nm %in% names(md))) { + if (!all(ok)) msg <- c(msg, paste0( + i, "-th table missing ", .nm, "; must set all if any")) + ok <- \(.) is.character(.) && length(.) == 1 + ok <- all(vapply(md, ok, logical(1))) + if (!ok) msg <- c(msg, paste0( + i, "-th table's", .nm, " is not a character string")) + ok <- !is.null(int_colData(se)[[md$region_key]]) + if (!ok) msg <- c(msg, paste0( + i, "-th table missing 'region_key' column in 'int_colData'")) + ok <- !is.null(int_colData(se)[[md$instance_key]]) + if (!ok) msg <- c(msg, paste0( + i, "-th table missing 'instance_key' column in 'int_colData'")) + } - # TODO: validate int_metadata$spatialdata_attrs - # md <- int_metadata(sce)[["spatialdata_attrs"]] - # if (!all(c("region_key", "instance_key") %in% names(md))) { - # msg <- c(msg, paste0("region_key/instance_key not present in ", - # i, "-th sce int_metadata")) - # } } + na <- setdiff( + unlist(lapply(tables(object), region)), + unlist(colnames(object)[setdiff(.LAYERS, "tables")])) + if (length(na)) + msg <- c(msg, paste( + "table region(s) not found in any layer:", + paste(sprintf("'%s'", na), collapse=", "))) return(msg) } @@ -87,7 +106,7 @@ setValidity2("ShapeFrame", .validateShapeFrame) for (y in images(x)) msg <- c(msg, .validateImageArray(y)) for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) - msg <- c(msg, .validateTable(x)) + msg <- c(msg, .validateTables(x)) return(msg) } diff --git a/inst/NEWS b/inst/NEWS index f1cf28bc..155ca6fa 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,9 @@ +changes in version 0.99.28 + +- validity checks for 'table' elements +- fixed and reenable broken/skipped tests +- spatialdata_attrs utilities (instance_key(), region_key(), region()) + changes in version 0.99.27 - spatial queries (aka subsetting) by bounding box diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/tests/testthat/test-reading.R b/tests/testthat/test-reading.R index 951ed8b2..57b7a4fe 100644 --- a/tests/testthat/test-reading.R +++ b/tests/testthat/test-reading.R @@ -17,7 +17,7 @@ test_that("readElement()", { test_that("readSpatialData()", { expect_is(y <- readSpatialData(x), "SpatialData") - a <- list(images=TRUE, labels=TRUE, shapes=TRUE, points=TRUE)#, tables=TRUE) + a <- list(images=TRUE, labels=TRUE, shapes=TRUE, points=TRUE, tables=FALSE) for (. in names(a)) { # setting any layer to FALSE skips it b <- c(list(x=x), a); b[[.]] <- FALSE From 915c9afff7a887fe91158dc0d9c6d889b961f41f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:06:36 +0200 Subject: [PATCH 118/151] fix test typo --- tests/testthat/test-tables.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-tables.R b/tests/testthat/test-tables.R index 86c87649..57147ca2 100644 --- a/tests/testthat/test-tables.R +++ b/tests/testthat/test-tables.R @@ -7,7 +7,9 @@ x <- system.file(x, package="SpatialData") x <- readSpatialData(x, anndataR=TRUE) se <- SpatialData::table(x) -i <- md[[rk <- region_key(se)]] +md <- int_metadata(se) +md <- md$spatialdata_attrs +i <- md[[rk <- md$region_key]] test_that("hasTable()", { # TRUE From e4b8fca066ed7ccfc2300f41d198c1d24582f5b6 Mon Sep 17 00:00:00 2001 From: "Helena L. Crowell" Date: Fri, 10 Apr 2026 11:08:13 +0200 Subject: [PATCH 119/151] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23df15ce..cabcb746 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) -[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main?event=push)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) +[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main&event=push)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) > for a demo of the class, see the [vignette](https://htmlpreview.github.io/?https://github.com/HelenaLC/SpatialData/blob/main/vignettes/SpatialData.html) From 1fde6a5f6d40a53b07d17c9ba7a4275e166341b9 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:10:07 +0200 Subject: [PATCH 120/151] validate before accession --- R/validity.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/R/validity.R b/R/validity.R index b174c44c..e288b9f8 100644 --- a/R/validity.R +++ b/R/validity.R @@ -2,9 +2,9 @@ #' @importFrom SingleCellExperiment int_metadata int_colData .validateTables <- \(object) { msg <- c() + sce <- \(.) is(., "SingleCellExperiment") for (i in seq_along(tables(object))) { - se <- table(object, i) - ok <- is(se, "SingleCellExperiment") + ok <- sce(se <- table(object, i)) if (!ok) msg <- c(msg, paste0( i, "-th table is not a 'SingleCellExperiment'")) md <- int_metadata(se)$spatialdata_attrs @@ -27,8 +27,8 @@ } } na <- setdiff( - unlist(lapply(tables(object), region)), - unlist(colnames(object)[setdiff(.LAYERS, "tables")])) + unlist(colnames(object)[setdiff(.LAYERS, "tables")]), + unlist(lapply(tables(object), \(.) if (sce(.)) region(.)))) if (length(na)) msg <- c(msg, paste( "table region(s) not found in any layer:", From 825d006420747707bca4d47cf27d6b05e3fb9bbc Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:11:17 +0200 Subject: [PATCH 121/151] skip sce-tests if not sce --- R/validity.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/validity.R b/R/validity.R index e288b9f8..6687dacb 100644 --- a/R/validity.R +++ b/R/validity.R @@ -7,6 +7,7 @@ ok <- sce(se <- table(object, i)) if (!ok) msg <- c(msg, paste0( i, "-th table is not a 'SingleCellExperiment'")) + if (!ok) next md <- int_metadata(se)$spatialdata_attrs nm <- c("region", "region_key", "instance_key") .nm <- sprintf("'%s'", paste(nm, collapse="/")) From 3648a706edc5314e9a384340102e9229953e9bb9 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:18:40 +0200 Subject: [PATCH 122/151] --,-- --- R/validity.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/validity.R b/R/validity.R index 6687dacb..3cb25f4f 100644 --- a/R/validity.R +++ b/R/validity.R @@ -28,8 +28,8 @@ } } na <- setdiff( - unlist(colnames(object)[setdiff(.LAYERS, "tables")]), - unlist(lapply(tables(object), \(.) if (sce(.)) region(.)))) + unlist(lapply(tables(object), \(.) if (sce(.)) region(.))), + unlist(colnames(object)[setdiff(.LAYERS, "tables")])) # don't flip! if (length(na)) msg <- c(msg, paste( "table region(s) not found in any layer:", From 48f18674d42d8e38b5399afb13a06f7f4e17514f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:20:19 +0200 Subject: [PATCH 123/151] fix typo --- tests/testthat/test-utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 5b34de5b..74343ff3 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -79,7 +79,7 @@ test_that("extent,LabelArray", { expect_identical(z$y, c(0, dim(y)[1])) expect_identical(z$x, c(0, dim(y)[2])) }) -test_that("extent,PointFraome", { +test_that("extent,PointFrame", { z <- extent(y <- point(x)) expect_is(z, "list") expect_identical(names(z), xy) From 370c9cc465fda6b1844e8acffab041721d5a7205 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 11:32:14 +0200 Subject: [PATCH 124/151] disable windows action --- .github/workflows/check-bioc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-bioc.yml b/.github/workflows/check-bioc.yml index 85dcaf02..da719a29 100644 --- a/.github/workflows/check-bioc.yml +++ b/.github/workflows/check-bioc.yml @@ -54,7 +54,7 @@ jobs: #- { os: ubuntu-latest, r: '4.5', bioc: '3.22', cont: "bioconductor/bioconductor_docker:RELEASE_3_22", rspm: "https://p3m.dev/cran/__linux__/noble/latest" } - { os: ubuntu-latest, r: 'devel', bioc: 'devel', cont: "bioconductor/bioconductor_docker:devel", rspm: "https://p3m.dev/cran/__linux__/noble/latest" } - { os: macOS-latest, r: 'devel', bioc: 'devel'} - - { os: windows-latest, r: 'devel', bioc: 'devel'} + #- { os: windows-latest, r: 'devel', bioc: 'devel'} ## Check https://github.com/r-lib/actions/tree/master/examples ## for examples using the http-user-agent env: From 4364607e03a355009d560f804457b2ebbbbcd09a Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 15:25:31 +0200 Subject: [PATCH 125/151] stash draft --- NAMESPACE | 2 ++ R/scratch.R | 33 +++++++++++++++++++++++++++++++++ R/utils.R | 4 +++- man/SpatialData.Rd | 8 ++++---- 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 R/scratch.R diff --git a/NAMESPACE b/NAMESPACE index b0081863..449d3f02 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -122,6 +122,7 @@ importFrom(SingleCellExperiment,"int_metadata<-") importFrom(SingleCellExperiment,SingleCellExperiment) importFrom(SingleCellExperiment,int_colData) importFrom(SingleCellExperiment,int_metadata) +importFrom(SpatialExperiment,SpatialExperiment) importFrom(SummarizedExperiment,"assay<-") importFrom(SummarizedExperiment,"assayNames<-") importFrom(SummarizedExperiment,"colData<-") @@ -130,6 +131,7 @@ importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) importFrom(ZarrArray,type) +importFrom(anndataR,read_zarr) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) importFrom(basilisk,basiliskRun) diff --git a/R/scratch.R b/R/scratch.R new file mode 100644 index 00000000..6de4c1e4 --- /dev/null +++ b/R/scratch.R @@ -0,0 +1,33 @@ +#' @importFrom methods as +#' @importFrom anndataR read_zarr +#' @importFrom SpatialExperiment SpatialExperiment +readSDasSPE <- \(x, i=1) { + if (!requireNamespace("SpatialExperiment", quietly=TRUE)) + stop("install the 'SpatialExperiment' package to use this function.") + y <- list.files(file.path(x, "tables"), full.names=TRUE) + if (!length(y)) + stop("couldn't find any tables") + if (is.character(i)) + i <- match(i, basename(y)) + sce <- readTable(y[i]) + colData(sce) <- NULL + for (l in setdiff(.LAYERS, "tables")) { + y <- list.files(file.path(x, l), full.names=TRUE) + y <- y[match(region(sce), basename(y))] + if (!is.na(y)) { + t <- basename(dirname(y)) + f <- paste0("read", + toupper(substr(t, 1, 1)), + substr(t, 2, nchar(t)-1)) + y <- get(f)(y) + xy <- centroids(y, "matrix") + xy <- xy[, c("x", "y")] + } + } + toSpatialExperiment(sce, spatialCoords=xy) +} + +require(sf, quietly=TRUE) +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +(spe <- readSDasSPE(x)) diff --git a/R/utils.R b/R/utils.R index e2706093..b75f3333 100644 --- a/R/utils.R +++ b/R/utils.R @@ -42,11 +42,13 @@ setMethod("centroids", "ANY", \(x, ...) stop("'centroids' ", #' @importFrom Matrix summary setMethod("centroids", "LabelArray", \(x, as=c("data.frame", "matrix")) { - # TODO: should these be offset by 0.5? as <- match.arg(as) y <- data(x) y <- as(y, "dgCMatrix") i <- summary(y) + # flip dimensions so that columns=x, rows=y + # TODO: should these be offset by 0.5? + i[, c(1, 2)] <- i[, c(2, 1)]-0.5 xy <- tapply(i[, -3], i[[3]], colMeans) xy <- do.call(rbind, xy) xy <- cbind(xy, as.integer(rownames(xy))) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..2eb34b55 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} +\alias{[[<-,SpatialData,character,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From 2ca4ea8f3b6d84945b6296815cc4a541b6468f48 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 15:57:51 +0200 Subject: [PATCH 126/151] address R CMD notes/warnings --- DESCRIPTION | 4 +-- R/ShapeFrame.R | 4 +-- R/SpatialData.R | 2 ++ R/Zattrs.R | 36 ++++++++++++++++++-- R/query.R | 4 +-- R/sdArray.R | 17 +++++----- R/trans.R | 6 ++-- R/utils.R | 5 +-- man/SDattrs.Rd | 51 ++++++++++++++++++++++++++++ man/ShapeFrame.Rd | 2 +- man/SpatialData.Rd | 2 ++ man/Zattrs.Rd | 1 - man/query.Rd | 4 +-- man/{Array-methods.Rd => sdArray.Rd} | 7 ++-- man/trans.Rd | 8 ++--- man/utils.Rd | 6 ++-- 16 files changed, 121 insertions(+), 38 deletions(-) create mode 100644 man/SDattrs.Rd rename man/{Array-methods.Rd => sdArray.Rd} (90%) diff --git a/DESCRIPTION b/DESCRIPTION index c07cb169..fac06671 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -51,7 +51,6 @@ Imports: sf, S4Arrays, S4Vectors, - SparseArray, SingleCellExperiment, SummarizedExperiment Suggests: @@ -63,8 +62,7 @@ Suggests: Rgraphviz, SpatialData.data, SpatialData.plot, - testthat, - DT + testthat Remotes: keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index b8e11a51..201785e0 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -55,9 +55,7 @@ setMethod("names", "ShapeFrame", \(x) names(data(x))) #' @export #' @rdname ShapeFrame #' @importFrom utils .DollarNames -.DollarNames.ShapeFrame <- \(x, pattern="") { - grep(pattern, names(x), value=TRUE) -} +.DollarNames.ShapeFrame <- \(x) grep("", names(x), value=TRUE) #' @rdname ShapeFrame #' @exportMethod $ diff --git a/R/SpatialData.R b/R/SpatialData.R index bacbfe6f..57ceed4a 100644 --- a/R/SpatialData.R +++ b/R/SpatialData.R @@ -8,6 +8,8 @@ #' image images image<- images<- imageNames #' shape shapes shape<- shapes<- shapeNames #' table tables table<- tables<- tableNames +#' [[<-,SpatialData,character,ANY-method +#' [[<-,SpatialData,numeric,ANY-method #' #' @description ... #' diff --git a/R/Zattrs.R b/R/Zattrs.R index 9bae799b..362bdc46 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -1,8 +1,6 @@ #' @name Zattrs #' @title The `Zattrs` class #' -#' @aliases feature_key -#' #' @param x list extracted from a OME-NGFF compliant .zattrs file. #' @param name character string for extraction (see ?base::`$`). #' @@ -80,22 +78,56 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) } setMethod("show", "Zattrs", .showZattrs) +#' @name SDattrs +#' @title \code{SpatialData} attributes +#' +#' @aliases +#' region +#' region_key +#' feature_key +#' instance_key +#' +#' @param x depends on which attributes are available; +#' specifically, \code{PointFrame} (\code{feature/instance_key}), or +#' \code{SingleCellExperiment} (\code{region}, \code{region/instance_key}), +#' +#' @return character string +#' +#' @examples +#' x <- file.path("extdata", "blobs.zarr") +#' x <- system.file(x, package="SpatialData") +#' x <- readSpatialData(x, anndataR=TRUE) +#' +#' region(table(x)) +#' region_key(table(x)) +#' +#' instance_key(point(x)) +#' fk <- feature_key(point(x)) +#' base::table(point(x)[[fk]]) +NULL + # TODO: only points can have this? #' @export +#' @rdname SDattrs setMethod("feature_key", "list", \(x) x$spatialdata_attrs$feature_key) #' @export setMethod("feature_key", "PointFrame", \(x) feature_key(meta(x))) # TODO: only tables can have this? #' @export +#' @rdname SDattrs setMethod("region_key", "SingleCellExperiment", \(x) meta(x)$region_key) #' @export +#' @rdname SDattrs setMethod("region", "SingleCellExperiment", \(x) meta(x)[[region_key(x)]]) # TODO: only tables and points can have this? #' @export +#' @rdname SDattrs setMethod("instance_key", "list", \(x) x$instance_key) #' @export +#' @rdname SDattrs setMethod("instance_key", "PointFrame", \(x) instance_key(meta(x)$spatialdata_attrs)) #' @export +#' @rdname SDattrs setMethod("instance_key", "SingleCellExperiment", \(x) instance_key(meta(x))) diff --git a/R/query.R b/R/query.R index f5fa6bd3..90817684 100644 --- a/R/query.R +++ b/R/query.R @@ -10,8 +10,8 @@ #' #' @param x \code{SpatialData} element. #' @param y query specification; -#' bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' -#' (order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns. +#' bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax'; +#' polygon: numeric matrix with at least 3 rows and exactly 2 columns. #' @param i for \code{SpatialData}, index or name of table to query. #' @param ... optional arguments passed to and from other methods. #' diff --git a/R/sdArray.R b/R/sdArray.R index ea0e4662..2d8ce7cf 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -1,15 +1,14 @@ -#' @name Array-methods +#' @name sdArray #' @title Methods for `ImageArray` and `LabelArray` class #' #' @aliases +#' data_type #' data,ImageArray-method #' data,LabelArray-method #' dim,ImageArray-method #' dim,LabelArray-method #' length,ImageArray-method #' length,LabelArray-method -#' data_type,ImageArray-method -#' data_type,LabelArray-method #' #' @param x \code{ImageArray} or \code{LabelArray} #' @param k scalar index specifying which scale to extract. @@ -30,7 +29,7 @@ #' @importFrom methods new NULL -#' @rdname Array-methods +#' @rdname sdArray #' @export setMethod("data", "sdArray", \(x, k=1) { if (is.null(k)) return(x@data) @@ -41,25 +40,25 @@ setMethod("data", "sdArray", \(x, k=1) { stop("'k=", k, "' but only ", n, " resolution(s) available") }) -#' @rdname Array-methods +#' @rdname sdArray #' @export setMethod("dim", "sdArray", \(x) dim(data(x))) -#' @rdname Array-methods +#' @rdname sdArray #' @export setMethod("length", "sdArray", \(x) length(data(x, NULL))) #' @export -#' @rdname Array-methods +#' @rdname sdArray #' @importFrom S4Vectors metadata setMethod("data_type", "sdArray", \(x) { if (is(y <- data(x), "DelayedArray")) data_type(y) else metadata(x)$data_type }) -#' @rdname Array-methods +#' @export +#' @rdname sdArray #' @importFrom DelayedArray DelayedArray #' @importFrom Rarr zarr_overview #' @importFrom ZarrArray path -#' @export setMethod("data_type", "DelayedArray", \(x) zarr_overview(path(x), as_data_frame=TRUE)$data_type) diff --git a/R/trans.R b/R/trans.R index bb303385..138bd9a4 100644 --- a/R/trans.R +++ b/R/trans.R @@ -3,9 +3,7 @@ #' @title Transformations #' @aliases scale rotate translation flip flop mirror #' -#' @param x \code{SpatialData} element -#' @param j scalar character or numeric; -#' name or index of coordinate space. +#' @param x \code{SpatialData} element. #' @param t transformation data; exceptions: for \code{mirror}, controls #' whether to perform \bold{v}ertical or \bold{h}orizontal reflection; #' no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}). @@ -106,6 +104,8 @@ setMethod("rotate", c("sdArray", "numeric"), \(x, t, k=1,...) { .trans_a(x, f, k) }) +#' @export +#' @rdname trans #' @importFrom EBImage translate setMethod("translation", c("sdArray", "numeric"), \(x, t, k=1, ...) { stopifnot(length(t) == length(dim(x)), is.finite(t)) diff --git a/R/utils.R b/R/utils.R index e2706093..2d7d3134 100644 --- a/R/utils.R +++ b/R/utils.R @@ -3,8 +3,9 @@ #' @title Utilities #' @aliases centroids extent #' -#' @param x a \code{SpatialData} element (any but image) -#' @param as determines how results will be returned +#' @param x a \code{SpatialData} element (any but image). +#' @param as character string; how results should be returned. +#' @param ... optional arguments passed to and from other methods. #' #' @examples #' x <- file.path("extdata", "blobs.zarr") diff --git a/man/SDattrs.Rd b/man/SDattrs.Rd new file mode 100644 index 00000000..b9843893 --- /dev/null +++ b/man/SDattrs.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/Zattrs.R +\name{SDattrs} +\alias{SDattrs} +\alias{region} +\alias{region_key} +\alias{feature_key} +\alias{instance_key} +\alias{feature_key,list-method} +\alias{region_key,SingleCellExperiment-method} +\alias{region,SingleCellExperiment-method} +\alias{instance_key,list-method} +\alias{instance_key,PointFrame-method} +\alias{instance_key,SingleCellExperiment-method} +\title{\code{SpatialData} attributes} +\usage{ +\S4method{feature_key}{list}(x) + +\S4method{region_key}{SingleCellExperiment}(x) + +\S4method{region}{SingleCellExperiment}(x) + +\S4method{instance_key}{list}(x) + +\S4method{instance_key}{PointFrame}(x) + +\S4method{instance_key}{SingleCellExperiment}(x) +} +\arguments{ +\item{x}{depends on which attributes are available; +specifically, \code{PointFrame} (\code{feature/instance_key}), or +\code{SingleCellExperiment} (\code{region}, \code{region/instance_key}),} +} +\value{ +character string +} +\description{ +\code{SpatialData} attributes +} +\examples{ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, anndataR=TRUE) + +region(table(x)) +region_key(table(x)) + +instance_key(point(x)) +fk <- feature_key(point(x)) +base::table(point(x)[[fk]]) +} diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index 8eaa3eb3..00b5d853 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -22,7 +22,7 @@ ShapeFrame(data = data.frame(), meta = Zattrs(), metadata = list(), ...) \S4method{names}{ShapeFrame}(x) -\method{.DollarNames}{ShapeFrame}(x, pattern = "") +\method{.DollarNames}{ShapeFrame}(x) \S4method{$}{ShapeFrame}(x, name) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 5c16f970..913adda8 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -34,6 +34,8 @@ \alias{table<-} \alias{tables<-} \alias{tableNames} +\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY-method} \alias{$,SpatialData-method} \alias{[[,SpatialData,numeric,ANY-method} \alias{[[,SpatialData,character,ANY-method} diff --git a/man/Zattrs.Rd b/man/Zattrs.Rd index 06ddea16..49a06fae 100644 --- a/man/Zattrs.Rd +++ b/man/Zattrs.Rd @@ -2,7 +2,6 @@ % Please edit documentation in R/Zattrs.R \name{Zattrs} \alias{Zattrs} -\alias{feature_key} \alias{$,Zattrs-method} \title{The `Zattrs` class} \usage{ diff --git a/man/query.Rd b/man/query.Rd index 83d23cf4..4b566bcc 100644 --- a/man/query.Rd +++ b/man/query.Rd @@ -27,8 +27,8 @@ \item{i}{for \code{SpatialData}, index or name of table to query.} \item{y}{query specification; -bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax' -(order is irrelevant); polygon: numeric matrix with ≥ 3 rows and 2 columns.} +bounding box: length-4 numeric list with names 'xmin/xmax/ymin/ymax'; +polygon: numeric matrix with at least 3 rows and exactly 2 columns.} } \value{ same as input diff --git a/man/Array-methods.Rd b/man/sdArray.Rd similarity index 90% rename from man/Array-methods.Rd rename to man/sdArray.Rd index 2be1a08a..cb1ca718 100644 --- a/man/Array-methods.Rd +++ b/man/sdArray.Rd @@ -1,15 +1,14 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/sdArray.R -\name{Array-methods} -\alias{Array-methods} +\name{sdArray} +\alias{sdArray} +\alias{data_type} \alias{data,ImageArray-method} \alias{data,LabelArray-method} \alias{dim,ImageArray-method} \alias{dim,LabelArray-method} \alias{length,ImageArray-method} \alias{length,LabelArray-method} -\alias{data_type,ImageArray-method} -\alias{data_type,LabelArray-method} \alias{data,sdArray-method} \alias{dim,sdArray-method} \alias{length,sdArray-method} diff --git a/man/trans.Rd b/man/trans.Rd index 4378d4eb..659543bc 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -13,6 +13,7 @@ \alias{flop,sdArray-method} \alias{scale,sdArray,numeric-method} \alias{rotate,sdArray,numeric-method} +\alias{translation,sdArray,numeric-method} \alias{scale,PointFrame,numeric-method} \alias{rotate,PointFrame,numeric-method} \alias{translation,PointFrame,numeric-method} @@ -31,6 +32,8 @@ \S4method{rotate}{sdArray,numeric}(x, t, k = 1, ...) +\S4method{translation}{sdArray,numeric}(x, t, k = 1, ...) + \S4method{scale}{PointFrame,numeric}(x, t, ...) \S4method{rotate}{PointFrame,numeric}(x, t, ...) @@ -44,7 +47,7 @@ \S4method{translation}{ShapeFrame,numeric}(x, t, ...) } \arguments{ -\item{x}{\code{SpatialData} element} +\item{x}{\code{SpatialData} element.} \item{t}{transformation data; exceptions: for \code{mirror}, controls whether to perform \bold{v}ertical or \bold{h}orizontal reflection; @@ -55,9 +58,6 @@ no data is needed for \code{flip} (\bold{v}) and \code{flop} (\bold{h}).} only applies to \code{sdArray}s (images, labels).} \item{...}{option arguments passed to and from other methods.} - -\item{j}{scalar character or numeric; -name or index of coordinate space.} } \description{ Transformations diff --git a/man/utils.Rd b/man/utils.Rd index cc478d2b..b17c24cf 100644 --- a/man/utils.Rd +++ b/man/utils.Rd @@ -25,9 +25,11 @@ \S4method{extent}{SpatialDataElement}(x) } \arguments{ -\item{x}{a \code{SpatialData} element (any but image)} +\item{x}{a \code{SpatialData} element (any but image).} -\item{as}{determines how results will be returned} +\item{...}{optional arguments passed to and from other methods.} + +\item{as}{character string; how results should be returned.} } \description{ Utilities From ec9863a9fbe39d153cf960a6e789980e8f672b6f Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Fri, 10 Apr 2026 17:04:02 +0200 Subject: [PATCH 127/151] rmv badge dup --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cabcb746..89f817af 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # `SpatialData` -[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) - [![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main&event=push)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) > for a demo of the class, see the [vignette](https://htmlpreview.github.io/?https://github.com/HelenaLC/SpatialData/blob/main/vignettes/SpatialData.html) From 16c3aff324e4ea110a49e3fd9ac39ad12b3d80fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BCr=20Manukyan?= Date: Fri, 10 Apr 2026 20:21:56 +0200 Subject: [PATCH 128/151] Remove Remotes ZarrArray ZarrArray is now in BioC devel --- DESCRIPTION | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c07cb169..bec76664 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -68,8 +68,7 @@ Suggests: Remotes: keller-mark/anndataR@spatialdata, HelenaLC/SpatialData.data, - HelenaLC/SpatialData.plot, - Bioconductor/ZarrArray + HelenaLC/SpatialData.plot biocViews: DataImport, DataRepresentation, From 0726c1308c43269852ec7cb2b80b7463fb83cf29 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 09:40:59 +0200 Subject: [PATCH 129/151] bump R version req --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index bec76664..8fbcafc8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: SpatialData Title: Representation of Python's SpatialData in R -Depends: R (>= 4.5) +Depends: R (>= 4.6) Version: 0.99.28 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufacturer From a56719f6ec368ef972b3347b1c0253fedc320b71 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 09:41:07 +0200 Subject: [PATCH 130/151] bug fix validity --- R/validity.R | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/R/validity.R b/R/validity.R index 3cb25f4f..edd7ba70 100644 --- a/R/validity.R +++ b/R/validity.R @@ -14,10 +14,12 @@ if (any(ok <- nm %in% names(md))) { if (!all(ok)) msg <- c(msg, paste0( i, "-th table missing ", .nm, "; must set all if any")) - ok <- \(.) is.character(.) && length(.) == 1 - ok <- all(vapply(md, ok, logical(1))) + ok <- all(vapply(md, is.character, logical(1))) if (!ok) msg <- c(msg, paste0( - i, "-th table's", .nm, " is not a character string")) + i, "-th table's ", .nm, " is not of type character")) + ok <- all(vapply(intersect(md, nm[-1]), length, integer(1)) == 1) + if (!ok) msg <- c(msg, paste0( + i, "-th table's 'region/instance_key' is not length 1")) ok <- !is.null(int_colData(se)[[md$region_key]]) if (!ok) msg <- c(msg, paste0( i, "-th table missing 'region_key' column in 'int_colData'")) From f53e06aeb25584bcb97f17ecbf9b4f168d36df14 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 09:43:15 +0200 Subject: [PATCH 131/151] rmv dup badge --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cabcb746..89f817af 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # `SpatialData` -[![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) - [![x](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml/badge.svg?branch=main&event=push)](https://github.com/HelenaLC/SpatialData/actions/workflows/check-bioc.yml) > for a demo of the class, see the [vignette](https://htmlpreview.github.io/?https://github.com/HelenaLC/SpatialData/blob/main/vignettes/SpatialData.html) From 68b3febad6738eefc07c7d7eb14d7294afdfcc83 Mon Sep 17 00:00:00 2001 From: Artur-man Date: Sat, 11 Apr 2026 17:32:55 +0200 Subject: [PATCH 132/151] add v3 of blobs data --- .../images/blobs_image/0/c/0/0/0 | Bin 0 -> 79769 bytes .../images/blobs_image/0/zarr.json | 49 + .../images/blobs_image/zarr.json | 102 + .../images/blobs_multiscale_image/0/c/0/0/0 | Bin 0 -> 79769 bytes .../images/blobs_multiscale_image/0/zarr.json | 44 + .../images/blobs_multiscale_image/1/c/0/0/0 | Bin 0 -> 22390 bytes .../images/blobs_multiscale_image/1/zarr.json | 44 + .../images/blobs_multiscale_image/2/c/0/0/0 | Bin 0 -> 5822 bytes .../images/blobs_multiscale_image/2/zarr.json | 44 + .../images/blobs_multiscale_image/zarr.json | 128 ++ inst/extdata/blobs_v3.zarr/images/zarr.json | 5 + .../blobs_v3.zarr/labels/blobs_labels/0/c/0/0 | Bin 0 -> 431 bytes .../labels/blobs_labels/0/zarr.json | 46 + .../labels/blobs_labels/zarr.json | 307 +++ .../labels/blobs_multiscale_labels/0/c/0/0 | Bin 0 -> 431 bytes .../blobs_multiscale_labels/0/zarr.json | 42 + .../labels/blobs_multiscale_labels/1/c/0/0 | Bin 0 -> 239 bytes .../blobs_multiscale_labels/1/zarr.json | 42 + .../labels/blobs_multiscale_labels/2/c/0/0 | Bin 0 -> 119 bytes .../blobs_multiscale_labels/2/zarr.json | 42 + .../labels/blobs_multiscale_labels/zarr.json | 100 + inst/extdata/blobs_v3.zarr/labels/zarr.json | 12 + .../points.parquet/part.0.parquet | Bin 0 -> 5888 bytes .../points/blobs_points/zarr.json | 51 + inst/extdata/blobs_v3.zarr/points/zarr.json | 5 + .../shapes/blobs_circles/shapes.parquet | Bin 0 -> 3214 bytes .../shapes/blobs_circles/zarr.json | 49 + .../shapes/blobs_multipolygons/shapes.parquet | Bin 0 -> 3346 bytes .../shapes/blobs_multipolygons/zarr.json | 49 + .../shapes/blobs_polygons/shapes.parquet | Bin 0 -> 3067 bytes .../shapes/blobs_polygons/zarr.json | 49 + inst/extdata/blobs_v3.zarr/shapes/zarr.json | 5 + .../blobs_v3.zarr/tables/table/X/data/c/0 | Bin 0 -> 249 bytes .../tables/table/X/data/zarr.json | 40 + .../blobs_v3.zarr/tables/table/X/indices/c/0 | Bin 0 -> 28 bytes .../tables/table/X/indices/zarr.json | 40 + .../blobs_v3.zarr/tables/table/X/indptr/c/0 | Bin 0 -> 53 bytes .../tables/table/X/indptr/zarr.json | 40 + .../blobs_v3.zarr/tables/table/X/zarr.json | 12 + .../tables/table/layers/zarr.json | 8 + .../blobs_v3.zarr/tables/table/obs/_index/c/0 | Bin 0 -> 54 bytes .../tables/table/obs/_index/zarr.json | 41 + .../tables/table/obs/instance_id/c/0 | Bin 0 -> 42 bytes .../tables/table/obs/instance_id/zarr.json | 43 + .../tables/table/obs/region/categories/c/0 | Bin 0 -> 29 bytes .../table/obs/region/categories/zarr.json | 41 + .../tables/table/obs/region/codes/zarr.json | 40 + .../tables/table/obs/region/zarr.json | 9 + .../blobs_v3.zarr/tables/table/obs/zarr.json | 13 + .../blobs_v3.zarr/tables/table/obsm/zarr.json | 8 + .../blobs_v3.zarr/tables/table/obsp/zarr.json | 8 + .../blobs_v3.zarr/tables/table/raw/zarr.json | 36 + .../uns/spatialdata_attrs/instance_key/c | Bin 0 -> 28 bytes .../spatialdata_attrs/instance_key/zarr.json | 37 + .../table/uns/spatialdata_attrs/region/c | Bin 0 -> 29 bytes .../uns/spatialdata_attrs/region/zarr.json | 37 + .../table/uns/spatialdata_attrs/region_key/c | Bin 0 -> 23 bytes .../spatialdata_attrs/region_key/zarr.json | 37 + .../table/uns/spatialdata_attrs/zarr.json | 8 + .../blobs_v3.zarr/tables/table/uns/zarr.json | 8 + .../blobs_v3.zarr/tables/table/var/_index/c/0 | Bin 0 -> 44 bytes .../tables/table/var/_index/zarr.json | 41 + .../blobs_v3.zarr/tables/table/var/zarr.json | 10 + .../blobs_v3.zarr/tables/table/varm/zarr.json | 8 + .../blobs_v3.zarr/tables/table/varp/zarr.json | 8 + .../blobs_v3.zarr/tables/table/zarr.json | 13 + inst/extdata/blobs_v3.zarr/tables/zarr.json | 5 + inst/extdata/blobs_v3.zarr/zarr.json | 1947 +++++++++++++++++ 68 files changed, 3753 insertions(+) create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_image/0/c/0/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_image/0/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_image/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/c/0/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/c/0/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/c/0/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/images/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/c/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_labels/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/c/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/c/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/2/c/0/0 create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/2/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/labels/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/points/blobs_points/points.parquet/part.0.parquet create mode 100644 inst/extdata/blobs_v3.zarr/points/blobs_points/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/points/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_circles/shapes.parquet create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_circles/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/shapes.parquet create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_polygons/shapes.parquet create mode 100644 inst/extdata/blobs_v3.zarr/shapes/blobs_polygons/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/shapes/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/data/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/data/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/indices/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/indices/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/indptr/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/indptr/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/X/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/layers/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/_index/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/_index/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/instance_id/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/instance_id/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/region/categories/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/region/categories/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/region/codes/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/region/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obs/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obsm/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/obsp/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/raw/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/instance_key/c create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/instance_key/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/region/c create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/region/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/region_key/c create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/region_key/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/spatialdata_attrs/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/uns/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/var/_index/c/0 create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/var/_index/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/var/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/varm/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/varp/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/table/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/tables/zarr.json create mode 100644 inst/extdata/blobs_v3.zarr/zarr.json diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_image/0/c/0/0/0 b/inst/extdata/blobs_v3.zarr/images/blobs_image/0/c/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..30dae7066a898a5570ca7be00e8eb970fc24cb2a GIT binary patch literal 79769 zcmV(xK6EWfAZgAVyTu9NMVi8fA zfVk?dX9#BSaLx?2aaX&ln2E9sbzrG@+Zr*SiLAvBo*=_U}R!0leQQ@k`_Q9uJ8pYYn&onh9fp7}}JNJ9G{`w-a%KV|hZGEY& z3U9DDu9{uW9Z-hTHVFHb2{rK5$LT^9q|x70qg3Emg6xDv7&bhwmb{FJr~(9CN4(jx zPbNfG!#2?tmtVphG)_%`GR#@E+OCQS%ePOF+T7PbzH7q8pi3}V|;JET!u zkVsm=qkeI~(KhMxS{yhLaQJq=OQWj9DADb^Hw*|ViGm*2kpNEw02R)WM>cH&|_te<~OHNR)#8D^8`;<=BSfzIn9>bP)DC z;|t6rQm`D-2C0S?Dp-G7L&t=s7&bm@;xJD_%Fy2`E~l;!?vA@76F~-n8?W`nIfMb_ za$FB~r&u!&{svG8M5g7!XB#|_ond+QQ!lS2ZS23(qhrAZlf0MB?SY~xvv}MgnZFmF znaloyRW0W!Q)1XPwtOo7VhC6#2%$ZPbj$!~wE^{&ygL{?3ElnDB?b#8#OilaqgdFI z341FG3I-5DFW0^L3~GYHyKs!yTDCJ2CuU6rec*WGz*Q+s9$2rA%gZ|b>=*siO0Fse zG*TQ=6NE^RCg|f#Cr754O3JR#K-AH~>1nxuA$rqb51D1Aw$D_p86c-D^Fpqb(l1;N zpQCl5U>XF+=`y6~viQ(EUg{c%76+WKi_ub(38_HBza-!YAb5lCT#G<5U@Tc~7i5GF z3k%ez{EJ~A`ts_lUDPbFGQb@d+E)ev6sYsI$?&MqT`CJH_rdc^of!DN0193bXD76X zN89-xx1+xZGdzut*$%D_>2sKtV?J$-R~&+{MAj;L%^I|2eJzHA3x+CzRQ;Z5%Ev>2 z>G{cGJ$?~XYwMZQs`!GSxp#l&|MFv z5>iH@h1Co|qG}8L9QTB$bxIBu*X0G&L9oH|xLBhdXhy8utzl8@0*=^sMruR3;ZnFK zhQ&}CWGYXcu$??pDtI^K&?*kj_ERqhLBvcgo)d1y;NjTt5oL$shPt%wW^==Fl+@;G zSqpSR6e#e)tvVWAOTcG7$Jk2%f#s-g&9=)YB;GpJcSympuC2-$EQ;ikeJXTvgGroIxa+o+Q9r#jS|qbarSWn9)z&CZ9CLWu+s%Z8LZPG z1mKu+U;7pEnJD!0x3+bUrnSZ6g7A|adE>m*?I3lgZpA+bkAGe`+?=t%E|O89isMn( zxttMTcs5&Q*R})$4kyE{fUQu&@3586Bnj|-Z}+uywZPQ>qMo-~FuZ`Q3^pbaTy{!C z$5TZ%vOz_|AT>M{azbo#%RMZw=OXxeuGYi{$?&EnK_DOVf$#f5LfpMYCIH2E=DAb} z0-dr24?F{oL^3dr2b0;%@O9ffFPsE18K^iui-_iUfW_fUE72SoSZe)}2VJMpUf3x| z@Sttv(RoV?F*;3z!hCX+#3=_E-zRrS3|(0HIq#5m$xca+dZM~;8V>UAE^VG4qZwa? z@fviBZFJPYKcwZpAXoK+GO`Nm`RwJG1b6A&dtH7+B($)6IK&2!60SN2ZVswz?s5J5 z$A^V>ZG)#Cw~*LHjpntD7@&0ZbTVGHLJKvP-zH!~6M-0-50Z5lvAwf+#m*EJ`FLfc6*^ZnZX-v`t1T*7^ zKwQA)c(8mwA0@(6kwC5ol_1kM)qT0UQW$0$RKM0(VJ|hdYtRM_6)xtPKLXlyMqph& zrF4h~Hhk1Rx#A#z<)73kUniu;c6iV|0emBToVe>=NRXkL1Aphf8d!#q;JqrAUJ4^n zomV#P=5~(5ad~Dgd|fEIyY$z=W7yV9n>W z(Lcr7!bDZ*jwy6NNooelGe-^pjUM=VXYcaL9MxN)yrQgbHWjU7qmpwDXDcXT?gvgyEQVbGzi7Sg!rk@g2l z-Pbb$F#;IDk#adXszXsQpwqa$;lhEB^mUbnXPdJCTw6^a30!EpZ4t|a)5QXR?~uV| z6WH+CRFpt7Fn6v&_UHM!Lp4yCUzx-MKJ9Mf{MRI2u(fgdi>ge0gmE zLB@Pl1B%-~fZm_JW&1cU{hX7~v?DZ-&s4Do$lwp?q#3$0=)|c#mXgJe#jF(9t?}6w z_X`z+F3Zo>lH;0zi7p(5B|P;vYOBN+@3f{XpDc6+o{Ri6 zx1(ppc@ZpCr9CnJ7s}d2+i~29MI*GNI>7!h3Z5oygB>3x`EVBmXzse5*=tL+KX;uA zY)M_x@J^;m0R>uYzwi^N?*)kB)7;LM-wHq;&GvYE0T7Q(?(wgo zUVbE6b%*T`Qjqov=Bi&Z(joSBa9s6(P8WoI-%{#2uJ)H?{|y(@i4N6AF9Q zI_)aT96`r@t5LGyN7Q$N)zm&o?!LR_L(asD2lcB4CN$5FL%wWq?0kE@9ERv86g3{4 z>oc}?4g;_3Y~`w;2Iu1vp{S}l4_>x82PEJl&QGshtR5xln5k?fbPZFlnHUB=2FBy6 zs3jOgC@X#Ks{l|HWud=EZ=m9lh~9R^EYJ!Kg}bt%HvL`zaoAx->AE@0=MoOPbk*Vf zcSX{44;1CT?$#n+8Kvi9An`OsNpshlJgF?T%ZHkDqT+%n-xETc8-gEPcbnM74RYjj z+|4o{t1uDHI}=>|cOCoPSi{Vb(Xa0sxSepRh2XvfszOt=1j_V>B4QKbqQ>l_dd^FfNq?&^tCleemr$JW78V`;WB0zF?g=3Z*eptUa zC6bS)A6r2FIA`Ma8@KvF6CW}R0y%lxqYlj-k_}#!+$-b8dH#^obek0M(mjMiBd3Cn zo<>W@i4+Q+KkJsGf=2R!&tC8`KngnMu7K{h%gYrnt@+9sUWxtWNf4=PqX;j28@bTH zn&^@;6gWtvE_`aJ*oQOcljGv9aD}?GP)mj2j_g#fl zRZcrPZi@(LHX!b*9R^|caaN@7jcuIndYGHWmSzd>1WIzoK?H-gQCO1HGXmq|=C*6e;Y z#|>O%6{Vb6iPCwSlr27)SP0@uK1p} zD))u+$;x$sIZVxQ2z_m~3u2fMj!#WQ)Y?%yefh#5a<oSlbt9**%!ag-r5Fv@3_%erAB8)QJ+qwca^p2hi8Np}@QzuSZNHMKtM0OUZFtkU* zaZNb=Iz?D<*>_J4-#*~U=oZhAJc18tf~5<)=yy;VYOq<=B+g2RM1b*W#bX_;&>3_P z`PL@rU?>@oH}!15VMLPROEIOXnUZDPwU@D#yfcF);nYomcfQah1*k6Y(>$%{R1#NT#e-(EIRfxhxXEb_ zG>9KHgM=+H;(XIvvlgu{BL1r2j0^B&#&N}VsyuOA{$1^vfB>27X$y>CCc)(TDUAm= zIy>2aEVW`V0~+CR!`eY;F*5urE2cLy6zfz5fMhj2YmEkE@$m)t+QHmMMHe|3t5C@6YY14rI!O3q8^tnf*p_yt7pZV7KCK2a89&Rz#g!4 zWj-2MVjg&LC_&v533yJtEJQj3ACGdfj}X`!GS`G4921&{mh_rHQ{YMBbbjEh4)rjE1NOgFWxu!35#YL5Yi&T~pJ*b-8QEjDy?* zugBGK3c}#w@$X;|PynnBPaEJJYOVZtQ~`>YKX6E#5(*EJXO!)WKqnw0G%qXxw8c8isI zxVk%Piv$J2SMFzSPYN@;75L!w6ojG(JYUoNy)w=j@v|SGCX5psJkCXh=_i~wg0!!Oze>RME20;7i7MipP9Lgc9 z^hPwGdhk9H=Cnpl${$ue+2BNexn^=-3sW5>PnBxhwp8tWYuu>N-1E>fqp~ z$i+&4CRo?};bwKHmM8MiDVWgq^0#5A$F1Job%V|rPTy&_gkZuF-d9_wHkSzqaal8>8OB$5uLWL^=yY0k z**%;<3?dAC75Ik=>l|kf4Xp}s0LF)A$%?AH(7qQ|3lJfF#(TULr#Pcbl?SX5@rmN1 z?u{{Ryv3wATP1*=Totd0=U8~^67tE>O&yRkpr4qtXTdlb=y+EZrhu?Acvus*j0C@$ zD}4Zx3jw_M9|Z*z4^sr5Xw*P)3<0HaFEl&=BLcnJCLGTX3PD~cYGR#26Z?;|aRsJ5 zP~S9}D)?%^+GTYMY~gspIc^G zx+CMl%Tr;9K||@Sy_7}coD^E--D53k6ljofTtY9IZC?ZTy?vd*F&ug@ZVqi@6F>JQ zhTU76c0Mi+rXQ25#dq~kKy4Ps_BGR#vk>>JpJ{@FQ0h|WklLi7Re%nnuJKoX49 zRaX$01%oKw0298tMm&>|u!bWS4K|Uc9vYnpPZI%A^-O~6aff-^bZ)h`V=%MLz^#)E z5O~RJ75h>)Y=?SfAO99=_ zW}uRyzq&2%i*$1O#tl~*vFoe`@gNLCNi zQzp&9&_E;Qr&>0ptRr2QMJB6K%3eG!*BY=}hm_Oq>BSM>_1v|j3d>sQ?6Eake5K%v z@Ld@+f)HhIepq`J&U((ok5SC%Azq?8@kpT`HDK&_0l|<9{77)wAb&A|*YJEvLC|Br zncveZS}m}`c{~zp!XYkU9B)l0W(EXbueDoiT>*fwiu}|>@%-vEO^;kr<#C-1Ndj*< zuS-qTRnzL?ZxXBRH*L(n27$!74mJAghLzhIR<(b#7?&7PzTFo(ffru_2@m!?aWElB z=f1|gh+|LK$ISsl`mCySR}X2Bl*pYQYmq|us4?(u25>G*f|?_ou!fC_3OO!bS+(Lk z>eh7BwY&g&Fj7Ce9a82Zth5 zv$&D-Hlkx*3}g-Jt`dn;q(Ku-!GHSRKXzHNX?(!i${{2k{Ok&N+Yup=2;qe+xR*QQ z%7d`UD^r-zorq!a)MlaxU}cG~g|xN~F+|}F8CJEyAosh=-~2Jq;kpk3Ar}LjI4#)( zr35Gle7@aE;iU1-p41Uh}Tk1QE*&$+FPnCCZ60C|E}R@`G+C z%LT@HaKw5R-*`^+!>08UnZ~Wox>m>H0RI+9ffwde`iO5ja|A|}uDM5HT0;=gp96r06<%Eab^tTdGE@Csz?udi8oLR_E@N z@4C;1&Y7c2=Ju2Iohy2Ex(-(Slqe5NN7U%)vP$0S`b~gWhtA zwP)$p@tY;ZW~G!h4l2p?)vEn?Q5X;0RL^GIlMpY5SJaMM5@JGzQ3d#7wo{hUG68Dc zptTeTuRK0BO*7cs76Xm05^ot64If`uE59X?PAd+K{Tp@&hOm_Gb*X_+MRNfD`>}0C zi5^pz-4Mc-Of2_T(i0_SaA<0HlwXK)CY?b zZ!f%s5iS9MMk)rP-Q4Trx4Nq8CHC5*J_*TbXDR%`+PAJ~?lvGS{ z2HSCY1RMKa*L-#ZXafu=Mt_z2 z1ENgP2j-Y@+#{#ya)mQCT;z0cArg(tw&JqawTP}qBWiDK3oucl^4FeQoS~rOvwlVx zy5)sFYuw)?Ug^=@4rX{x$t~U8N;awisP(c?ODr)uJb$Z%nAi+*_BRbVsv4dLw{_{2 zkHm4nj z?@cfB3D5v}#9Ln!IJ}D2RLc_TV?puLz8JSbvNU|vPmRR@p|8?wmig+XBH(sot0{xA`9+~&qnr6Z@eRy{xojez5#bi;i} zU?9G^%fbubgXJ~pu**_Gm^^f4l!cJZ@U5|AFAOu3_;5`LE1%%~-Mc-l zJDU1i+ET(c!PVCVa zh%oG2T)ZEcat2Q&8@J0K<^3*H3%5JX9!_VBq4A1kFvd-JNLNAeSiGz+8lDx1uj8(u zS$pn^%LWIS%0tX?)9FqXgQ_8a6vY)S6LY@~gqa?M=LfRDM#r1BC&zu^yuc(b+lz_p(%xyim+Jfno zIIT-zV^U{?zgiE{B9){bb_5O0j+ha*^}v`NKyAix-3?HnU@`dFroLdoQ^%){Ia}5i zBcJo8@%DIi@R|mGyx`epxn?B@G0bP&SGHKP8=lQ@#2HtRnQHSm-X&JX0v;|yMq5rr zE9;MFjF5wG*&d?;G1LRO5w~02QF43AaZ0Whz(uJDzA7_?vI5l@wAQ6bVpfP+fd%$o{Y`DTgF?XRoQGo6I8 zmLZ7Vl6ttBTC3)pDZxQZ4ws%vWx~}HqQPgwuC`%iU>=w7(*_z!tK!vYr5y+8^!tRuC zDLyfiF>Yrf?6i&f@>wIN!Dkt8ZWg!WWr;6lFvK(=iv=8WcxR6(99s|hRw}x{GwP{K zWFgI#HplHDp(Xsvbl+x2OD`?43y(OZ6cRAvzrspRDsO)-V~b%TvJsa}kOKvvv+7$3 zBixeKh(7De=8T760Z+{k{g+un`Ji%mDwI2H2OZUs;U>VyQ*D0qQcywp)lL`0Hx6<9 zG>nnO2~UJ)PXC=`cvksf@QfWLC@e=?K^eo8n{yd_4RF+$4Y?OdRZyDF z^{a*gvgUxhj*5tjsVhS4pW266volWr8APwKhKb%$0r!BeaR7W(3;Jq99gd@#EUrkx zX1=G)OkD^Hig(7y0?;hT;HqLe2Nv*%p4N$Ir-i2CaW(61Zri3m%OIv_#0#&R{&3&@ zs95_WU&_)2Kk3WiEMQ91`#IPKl>`fpV_uhn5+xLv!($=N1_!szujBN^L!rUFo`xx$ zsyGL)thC#4!fkOk4rt-jHlphizrc|}tvWH;gke}^#v`-X!9AcscH>>TWF5;4_w6+P zTH$5YDE*j5VX7Ga~z|}kDd|-xRWv&`5BZ9a&v_>5F>BTRf5{l3E5GJZfT6$^{ zPqKm>!rydowt$A;?k%V6fGI_kZrL`aHm%$H$T(Y@7Cr*5cvfxYW~Fz=4l!6CDlGVB zf*3zvJeEI&OMRD?5q;IfmFEscZ;Au?M2R)&nUGY-p?bU@WPGs4i9*2HT#{h~xz+Ht zQ;{bed#@i%BXRe3ma>RoQYC=!tCU*6RfiJ(yj(6DTYgEmbCR@Ma7K8@VQeWK%w12l zF#~2mh7`vgC1BvK6!pMh&}g*~KyR!f-ISTAeB%^Ycnq!z9N25v8f%61aZzgAOqh_5 zN{pa6S8DE0q0LixuND))@7{8_I$Dl#wh@PxM8$SaL0qTSW7&H5geB zvew5ndbcxLR<+lBGcb30xkiHT3s`LE-}mRp5oFVtk@Z5nw20${~4o z5NAkBPl*X@*+ldCN4*<7SUgntpchP=+l=VPG0MuMrR7|X0qU-6b@mIXZq}5+d|!!^ zir672@-c5mh7KBDpG%PkA(0FDY#1^ex_$}W5W6ho(*lHd2EkUnDEvBW^XH4s9w5hE zZfyQJ)V?oE$^vUC*$b1DD7*m~bl@{76auLz&xNqV^i}Y|-6O2UFw=D3c_TZ>xX5x) z1}WG<$a2m~U1Z@#`S`eca4c9#yS`g&llnvL=(}OIB(*Z!Zo5En?h(euQ{|{#T8x>uY)VvAaE<-Q6%9R5e29-ISBPko z(|W&TM%Ed2OV=}Upa}=z{kR`%Gu&t(9xZ`&ZUtHtgQz8R{Ib|QvzeDhor$%hVyOgR z@dCi$4p&aCSnF%h0v{HcDqbvBPDHZ;2=9x8%C@5eaLUpXhb7_!!1!ySav2qn0)L;8 z@Zs@b@4dR8BWh25+_$!cneL(KbAy!ERONJ>HQ5*t$LxrU3U^gr&{@5z+)j@qX_DLe zjPNz!i2Pl%N5TueE8o4rLn+xgblk|)psA(Pwk3%fKSOzN*{) zRUs%b4K2_|I#iVH5@R>Tv0!Z_*yOVe5EPTZFh1^afZf##Zm*T@vRk#S3RNb`{@LXv z{855N%~|JdeYQYaYT}N?Ye5%OEc$1RJCIQI&|WADdEna9{Y_t;6f2N^znWljWhu7g zvLqpv|b}M6*~g9H3~u$r|`d5kF{Xb)-FXM}a6} z5F;kH<)D2M;Y8KngCnx#W?&qbDH4Q_9q-p7voK;d+8);fi$_mw4u6M;J_frH_^bhG zVWqb0n?_4fI@v;q3u}78nFH`r#|4rvywLF~1Q}GocpZEl4hL36u)L=Z|j7H^Q zVYo5fuxjyCZ6AASSU2xXG8EpF>$|3>^=(RX+)+E>mpOq`Lydw4bTL?i034AZV zU3!;75&yXBi4ku~kz+6Ik*Zdtv;C&sF%5Mh<~RFxEYawjerg8-#4J!JpLKaMA=m-% zxGN}h%>aE}mUdM^51H^yb^T_o{&eAhjPf0Ij_{xvLpUMb^$sToc9l@q^* zbeFj&G^b#e1I>)$FnIBNl~9XRG9lu#!AMaFQ$fcG#moeCT63hu9-{~Sr)e8jV(A5BnOOqQPH;f zE->wyker8;j;BY392rsp9QoF&wO$tr6uic*jX()acq^l+ln-X*QyW9CRA%`Ox?_M! zgbt*CLUYjkit%(*EU%P_$RcmlgTL>K&t+w)aR!i!%EA??-@J&T1MSQKZVCzfg?QwBDL@0gz{5vF+ z5eB0aHg+Z;h-VHqZ+5ZZyy@+%4S&r0frB;U`{{@b#kHT^&)&pQi)ecOyTm0%#@NVhNog4SARc%t_EpV@ zC!d!>ZNoH`&0Z5lR@Vh8>59oP%e8S)2>CgJ2B1Ax3 zp&ri)2;#`BUo8L*GzLZt6On<<%VAn z2#A1Qd4gvsM3wcAfDxLLL<8dj2+#%-fONY9P<93YAgD+f>H-h9 zf*syprCv`~!%=KTm{X$%@deD$8EB$NL-Vx}cUcGMl5S6{*du6dLd`d8uThK-7| zm~k%GTT*iW1sdHjY%R@)T+%gJ(V#XImfdX&+Gs^W@3^WeiwYXAulwW3Zm(By^@0;;Si3=DMf8` z2vL$=*$2VIQWa*0M2z9x=E-9$6+B&dL*gjv5f&@psdT}y^0n2c;I>bOXnZRmzl&ul zP8SnwkOBd5@p@ZAnK(3oX9Rj?`+@A@ntuhP&~~t$)Lp`)Wa{j#No>7^%#pXfp@jri zQpcrsOWb<7{Ok;;D56N$Pi1WR6fdAXvH(hq)kWC*JyT#aWJ6jg!eFoPM`j!`)OkcEl%&<*PtPHaPILy6sh1PKv#0_8($g zhaR705OU@!h2prFXfwbm!>0{lt!Kl;`qWWP2b*P1x0G6KqS>K$K@OeG3uLgL=V(J# z&O+nw;MhvTI1NNN8L5`+1)q2nwP-i&Nu~D z3tT)c2@}7<2{&gAd>8*=8z=%6ty{JJs?1$k+7FrIs@OKQ7P4^N;w`2SKSUh&O8WCo&AyRaZ?nwWM(dxPv$jqgbKzp27kUI>>60ON4K40Woe1}J=;XHhER4u4>uW`1K z%^Mkf4Yqo^K!~XBK_ z`mVuIIdDz|SV4KW@by+-AkbNPcbg$|68tsHbyh7Xz->RWxyv=E8DiMQv}3*v0|0K!kDp~Mn|n7A7W+S5RX312O6y-gzm?@4)2 z;P`PqKIe0Y8`sfmgf1GsW^38rwjfZ222EH>qX;0{Jpa`R5~RG~evjKCg6pn{f7if= z7iI`ve=pf&ni&9k>>9~%iPRX=T_+_r()ifmduu84>x_ z1fZU?&?cX4RplE%)8e?XC0HUZFo%JrJfgXDU1yP;w#g(&cka56wx7@U48f|O(7J@?rTIkcaU3mo@4$i^NH z4!8X}w!jDX_g7A!5Xt~ynS^&~AAo*u8q8raZ>MLDg9#tbISXzUDdjmu#PlyW6crCW z00w-}K{JFs0X%;kb5tOMBg83v4fy#m_`Q@8&dz~fv=SA9T#NK{KE|M&j?$7Jr!zj_ z!34m9LuPKdwJzfL>C*P_a*))^0&$br(H44KGYKf5%ubvZP_#zzGRIqQ<6P+A5&c(Y zAFdb_OMlNyZ=GrG`0j_26FWMYe~+ACAWcN`)~P)}4$CASy8WtWIM#E`9S~)=0#YAo z+zOy+&Gn3gk~LIhiq8blxOFDu@T*#OYK@0Hj$71NeahkOy1OJ=(4E@HbrH>I<{9Cu zPf1{~9<=jfUl5~N|2|_o$N0xl_9bk(l-6jVFMCHwTqk^Ri~}X+7&3`WBY^@>b^aR{5-!%A89>AUKk^YzzZtPGdnMbV*C20y>i<_pXN>BB@5NZaebSV^vf7_lv0}+2z60 z3TlmhklpcAxg0ga3q;%!%mVFj-GUR2NZ=TjV#o6aw7O?3A)F5ZfRY>4K8Ktv>UD9d zcGK+8rx6SgE^F(90*=qoafea}vG}lMnylgLO+@&QgwK5_e$5`oMBP&gv-ziY*)c@L zIEP&($Kf{^QyB6!94a}4;P+G;G_Z^+Jp045Kc90*}F&>;BJtOCQ;S7cwkG4&GQ zLG-52I1JDvVqKE~a|0#e$S;BbvvKB@;*yW2yUVPkw<$6)r3f6?L<^d~47lvHLmUQk_=PODazRR{ymGBZQHN;SMcWvqaER#e)~Cle z;i3<}WlI!FLBYr0Jm_|ChG2f}jWI`OF%4&BQ9y?S1EW`++q6a@&b}*%85#mV3m<#t z76YpizX+6#Bo=lA4B5S3+FA|l2zf;e(}x;05gJeApP)0O;3q@t8P=3P6G_a zLw>1d7#0fpX#+Pl@pQ^%f4yK(+Y%m^NWcjV0m{qnqFU(nq4d)%42N4$Hg1VUnL)`$ z%`@HNWI%x4`RY>)UI88Oj*B9P`4f!!y0uhuEbL$?+gbsnV1{u#G-ETOQNgd(CJ4R2 zb^F1#2Ui)YXFO9|CJz>7`l$hsi(FEASIHd|a1*(2Be;gEXP6bfc<7~XwdQd7X-$8KRLO?Ty-%<4?0G7M_E7vAI7FaOf zbG!}Enqqd%&1)Xl9x{(LVLJ-t`FG&6AUMUpktZ+E8x`)*B8KmQrRnIYPJ&1|N)g`< z%RnJ+lISVFGHBump?tLh3}ZeV-{Yc~J&hpEb>AoeV5 zAX*SwivqZ<5;e98YxiH*X52x^q&Te74j+_G)p2jFThNemeAk1UveR<)-+CC{rf=i7 z9$DXb*y-?11%#+AwjccOERz;;=JsAKpg*Gx5dX-8+)dO_>81XD7vOge`#Ai>+&QLRO{q#w}GA@tTC51ml01ecA(FEM{3BUl?ln`KK zu7dMV3xvTM?h!E(N>ygXwYZi94_Wz`GZ*L7#U%7d~M~tTgUp14O+5N2h`#~QZ zQ_0JHX;6R!v6eh=XNlFHnVSnM9eJY0*zjQueg<5Ufc^`lWcfvD=DH^ad7EZp|IV#p zyM|5Ry~km3p1Tn)jAMrBu>jtI^DLl6LclxjAh6+`57(AjEa zELCuPJCj(Cn)Cq*C*d_B-~l$!ja=je(WK5h3|JGt$0h78m1DGG?9{?x07e6FRVvwW zxLp8oZTLTo7V2I@r#|Imic{iA`e^Uf!4D)X?`z>e1fyHsAw6)2qZ2yu(GbQVJ0YG9 z3j>EINtWAjn`ik*wgx@>~(zPV~$*DWaBSxeK`lf?Kx!<1Y@lrl}BA*==U}k@VmUJyuX2wK%Va2A(CHNB_O6p?gEa z`NeyIBxXd&J@N0A11v+B?>6VQ+_vzZwUUFigR$aUW+09MeM<2>$jK<_NV9Xf8P(FV z)83*0-;99|*7r$rZji&B&(aX6k z@nL}`04gXodWzy8%m9@btyV8}!t&AihW2d}P@z>}Bb;zSbJmKF6PIk^P{-U<^35Ls zXft9$|D67*0il=fr6DjN>=2fFDu=%?z%d&yp#v}ip%wtGt#(AP#Vj2P8x1SHe{cxUI;2Bb6kEZ!r6|jjp>^@YOOLRj4v4zNT8+#&^Ke} zELcObc&cxR2+dmq9xHhl3afwan0A7sT%iZo_6%()QXY8&iUmWTmPdX-t%4aQ_F$os zv27e7t_y~jOlKaWri++?hlpyXHOQz>;%ZS0CB=A1ET4H<`8*i!h~9k zElxo=*BAMjh45owcrwiq&^6Z9J_wfK77``rnj^GKXg{Ppwa2T40LkN9d#Bah3vTYZ zb5;6feSCKcj0k}Xa!>s+^`u*2{VGHrtR-MG-ZtQEgBvjA2Vp`HuS^3x`q|CiG6VQv z2~5u8zz}~+`hWtAZNS-Ng$XPrfbqiIR4FhHP2arnd_qt`?Wsx)Jgg8$-(9?C4M`OC zR3!_4M!{#_?7-Rs9jo`kHWF2tAFz(rw!y%H9?f^TWR9LiHN8%PWwr1WhqKF4H`VPp zE;#OrQCUGN(8~(vUPwJu{^^#4lD6jKQ=&-eF(E_jj3Sm66D-Z#k^s<&Oi0v2HI+}4 z{eIr+fya&>3IP93`&{ZUmUrK2YXZAj#s6J|uxFqN^13E?vc^~>E_)2t=Eh@iSss{S zb}+2|M(jZ1mKNu_PhXtNCC$HmAx%*DX1sf6%j>FT$3Y`ldFVNz_{_}cp5++ZhYY-G zrV%OnVAG|~Z4v3&s4Zz)OJ-c^#06*?$*c!qqs6Y_UC#;p4nVyXVOI9;Aj@RoF#ek3 zm^phqPQ!qiQyRvHOkfKX1wrwYO`re+q8?7^TuPeBP zaxGJJTu(~Z5jvMggL=Tg(KzaZcO0@yHSA#Pm9FrSNu90WxlZ?mGOR`vsG>NZ0koJ- zEwn@WWO$sG5&o$K6*MMjq@P8A$^s#q`M6*>ID9Wye-?r2gO$<@KZR+@VY4arM^0G^ zIAlg2C+p;Z#f}X3T0jCctYP+c?ylL?9vi3pdcjwC;`ggGbT0xtxNuw|DI5=e46a)n zc%u=n^SDGhU1TFYU&TRH!IFCL%2}u%$%#%6{MEtH^!OiIfO@M4qW4Qo5UmO{$hc|P zb|EbS#lH&mh`3VEYas*x;A3j$v)5C{0w_<!cM)Q!lop@Cm5(b-N&749+be4;ib1&Wc5b zGZ%cfCz2j{T+)UFrgB0C&9XH@OuW($m}^HviLb8c;K4+~qAs?Y9}O9Z<6;%Yn0^xQ zRUxfW2xHEtq@z60$?A4HInl`?KIC`(-LM6+J^i>E#~@H71pe{t$Daj~ZYumZqBTa2 z%jWi(K+%A9Tok~RU^xaZt4pJUUBm3A)0zQP5IHzx7*&S111tVlgY8a438OP|vS=G~ z0sK%;S6eJB27I z2FePjEuoBKHhA@@S}z3U<}k6-%NX?yfsv8J>z34C$%>S|LlT$h246(8%1x0=Av z0x5O-%s9bg)<@tQLcO4Er<7b$CB{~kpW{*0hCsoBvfnfXQhe@-o%VwtE0kT0qs|Z- zL+JuN#`V zpFWd_=Vg%)C?eqIz{qcTO>RHVD{3N{2#VsdIW4-}kjdT|>$S%C==eeyWgC|-2|h;> zn&Ah7q35CKD6yH=dmDW}4RqY}e)IXdO+gv%dSa{sH~HtfJ-Db;n=khj5%rRwk-&8u zK(F9o*Z5YS+A0^CI?n{KhAGlDcDR)jPfpD1}0xwBxyR<0!ZZM5n2NMwg z8bU;$@}Z1t4&!nW$~ZWo;7SG@rI|0HvKsNkK=Dr<8rZHfY{Oog%-Ec?IBVRQ+XDq5 z{|x3qzTURGB2E`jgTd#oO@{qAc47UK4+~9mDAcdiayi`ent0q|rv$`L14o4N0oyx< z(S*qZ)Ga{bl^gg(8z}WSqVlqkQVzB2aiGwn@ayx2Q9+}0%3>E4aEOHKhjZLHl^T&Z z(I*a#g2~Zf=gBOLSSw=6C+;2n0OSwmxSdFeF&x7#ss>@{bi$4ow(Pj7>RCKl>5bZt z8Lk&o>@Wg=pWFT3CZ(QxnZ8+|=X5JFz+an+Q+nrgd>2`GD-27;fthScAnC|`H#qiM z!%)>Idq9Iv zi-{0Q{tP{iyKGdQ%a}+#SAEf2Cn3-1w;W#Gs_peyXYA#NUckobj8#FsUV+{HG3mD{NBZPF_7SD>j zR=wwK+VP@!#NuiWKOq1>JiL+&v02my_;x`sIV8z|@De%3dRD;1L9p=Ig<@fS7igkw zj?Aw|>+vFsRn6#zq+kbTYjD4L9P{VnhH}-@4A-lLl#lK7{bVr9;kch{iM~ZF?=_&< z0_KwI!!^pSKF=*TzH&gMtfKP6XfxYFWI?VAy<0%$OvhQbd{lfHPxxg5M&kxa+1JZ% zO*6J^yjaez@Jkzjlf~GWUOkLjflUReR#VL`tVsG zM07naHDhCdUGFeA-W4o{&rMz0Q)k==W+75vGZ5~kRGRpNMM;Mns{5YyV(EoMs`JQ$qG|Hv=T~TmDAF753-%OxefCSOyQHvxrSN&z(P22Cs}!cWr?bOFX}xdz8Z^ntb=SYRH$S%j$XU z6fN`?867xxPp6Yf>%fI*YR$^l=haPfQ4I8R-33uu0`63BDLPLf}J= zt^6_3Xz((YH@Xr-a0j zY0#m?RiReaT7ba2Es!0nnHbR5m1YDIL;L%#2RO`6ShefQ>85Qs{daH7Zu-QWy>}UB z28T)#=k5Em0zvKH3Zbk39v*mVm5G3=!wir7sj_Xup!_4NlXq*d zLAU^c0k>gl=o70Y_RbSUVs~rJSDOiOq>xBGHU$FCAPA?w9w*iHfW5wIDt1Zcr}0BY zvspDgG=H-N(qnN04ww9Z#>f_id&0xD8;K}f-iW|< zT#xhpCQ3z_6k{*vZqyjHCqTy-`uwq}O}gAY9{7O2NYdX%T6g zlc#Ar`N2ntNw8!b6YITVItw$9u|a&epQLBhiyIBj2m&Qm&4S25MJPZKVWIi4xT{?)FPCI;1^P$Xu6)+Bj1J7nl>iH`!oM707i7vxc zJ9G*Oxm*>jr1s(I^(YN=$$|N}piqzKqL#l?7A;b+tdROt8bApx&`M8xSei9KZ}sob zX3R#I9=|;b0+Q%Q_|`uPAd0i5PKvpR=am!Wp1MG&AX3{-x&wgT>W8ksLXS}T3|Mhp z1ecynuGjyKjua#P8$Pg}p;!(eE556vd|Kl2_gKB_4GWruUs}o~J#_=b(9TDep9%|i zyZJKUS)sXQgqwsm1#5N-1eZ=`6x=K(;N<$yS8AqhQ!+O_vyv8_Y;Q+-t~V(n++sk3UD zXehDTc~u*?tRBo!_Y}Fde1y?-g+#RVEA&4;_1 zVwS9>1~~3?TH}lZ5uTeW#F2LQ3T&BQa?$bo}*Dw_eBs0Yu%yiZdg?iri(42dw}e`?_U*1v>*GSnT(64X%Bh zbZo8|gwY2egqd&dSa6Nha=EBA4rvlavL7XY`)sK_^-VQh#3HS#GmhA0;}ST0UWG|p z3u%*I`(gCbK_kHLY#RhPQIbC6P5=#rHe>Is_QAP45qs4VRun+|+;_JoSV9Rx(Zt@8 zeLZTtp(KL@HHuchCZmLe9y5xM8i63fiV1ZCISA7wgf14OR-KBgGnM;`GYI_hij$~|}|aZnU}`HwL`4VQu^w{82uQd!CIX6a)Y;wXYS<>Mr33p3k9?^>Ya4QiQJTNSQH*qTWg(I2qA6oFX7-u?f zQzNmaMW^&rBy=LEfXseZ2DztTMu=OMU9HAJ!Z>S;2vI%6h`(Ea#ZUzs=!r|50B9c3 zab%<3)W~YCCu@o^(9wYM%N-h#fLqqiyoexy3a!*Lx0oDO)*$1_f;TgWdsck-MwoJ0 z3F>cL2UEO#V^{T3Q9&dN`I2K4U`0!6uIB?75E$q6eH+S}!$@$rC1T!*0V^bTn{-y0 zm`VA2!IjGcB;xbl7~n*i;k~X07nre+tGAU6=@G2J<+N#We4j5=K6|S-fW&RuTQ^A+ zLGVc*^q7awnr{r9$1RJ9jnTk-$gmX`u5loaTEiIzL^i-_Pol2x;>`YL;A3}$8S}I} zh-wiI06A)-bt_LKhet+m0Hez*{kbnu9jNua)aDx}bcWj(nYp--aPsx4t{1%lbsS#f zXQ;*b!pP-to029_iTGWxn=B9HvCED1;*gLWW>t)oxEKoa7oZkg^4fF{wX^`SSZ(u-Yaq?AnP`Y!6E?Q)IIzdD9~juJ23Qqy@f zWo4X^iILndx+&R9Z0Ci1FjxjaS=>?!^tV6^kf&0m!CtTdaauIW;1GCxf6JN$YjaWm zZ6HMu?yco-n{8*yHbehTe1cszvhiFSEOrtm_`Eg}cUa2r>#0;MwI|+8&{Z>@jQlPH z*ER@v@0DQ=khnHfew8_-!}g%saeq`u>p*k(-AP+Gmki5)pWJOlYt!8rfNt>8khvvm z5cF;E>h%myoawOXJ}YmBFPV!IPXPjjh%!|1RXA|r0=qHuQDP&phmxY_3NyxObu)@S z*D{Nr8z$|oV%z`{D?PppW*a6ix~w z`oP9AE=j!}AIt@0M)6Jf!A6NPx=}y>odfWxWIA|Vq1GP{+)RhfF|0GD6BjBkXeTr&@N0bgmmP|nIRSh9sDxsV zy9ho`bljL-GD_hw+=%ud0oa_)9gQOl1i%3ys5A@RKzU>6m6U1;w_`qVAz?w&f^SZf z0JYG=`JBy)*}zRi=UfFUl(JL%X3gvFi`BnZ+V}yG@x0)SKsjOuS72TdNbmEQqV_Xi zBEVQC@l|7{Qc_L0oi=vbx>geL?+ubVygAyZ_0qbrAdK*-kRzD1BrcEH;RLUbaPo?K z5^hj?sCwfeQ5;$XuyazFS^|3`a8y{PuWd%iyG0x%8Nz)Xm8NY~Yme}nM|nzpmOW1R zKmyRE+=x=84lJF5-~%_BZ_Jhf&-7$n}0 zYyM7#xC>!?A{OS|^KkAHmv%>NU7B6fFU5#bA7Dpqal3S5uKQ{zERgt8~TBFki^TKoz?jnt%_+TiNHs<1iqy z0P_dU?C9WJ36B_sydVd~`-$5~GR&j^j+pC-fm%oEhnG776n#pZQ6<2O7qUgqtkJPy z0{Hl)m1#R{Jz@^qDtW+05%6;twO?8wjGectH)fW#z~6dMvn$?Y&gxg^$pb|4n`auf zte|%sajqw-Yv=TTTdX!ce*=#ALiu8nfQCo%pccYyuDRuH3g#Jt1|I2^=hHir^TM2} z!H^kQck4Vku|NQV$51+vx18qsCEV=h+#gVfN>yOI+`1x@hEv`Ap!7v7K)^(1TMu@f z*#oil=UbDZVDV6otaeX!?ggGyzz(y!veLuGbg=+K5%M?D>RU`QM(vsU7-OxU3L!-* zA6wWh0lB1ij$)y|<*XL@2e=xnGOOP?-05IC%9Z+EZuxWaD z(lhV}3NI**X6T|7mGI_=IASpMoIc)D?85a>3%|Q!Plf4;7`g7qGOYAV@qcX{IIu3D z{a+uYI6SRN&$XfEX1&uo>wve2EG`q@gu&(^aOQl?WS$&Ev`%;1ba>Fm19@C+(-Myc ze0=xO0J6*npT8Xdu(Jt9IqTJH3fwT2i)wd4s1>zv)h3>yBxtMCg1E-3vo=1icFgNv zmC|RSXC%^q!5tOBwhAEXgF|Lvkwtk8UC#8KBsD#FugRerQxdZN_B6rrsG?|$Kf>z;Iy;^vp%y0P8rt)hlW(_{Oq)!IN>^T1x)r`*_s@u-+## z`L+~w_fCl#-C=_9(l1GDrT~+G@yIt*2QhDpb2$C z>bI>Mhi8wp^n4J*m2Zb|@x%4jfVa@_W7Wb})w$G2l}4o^f44QEEDt!rCE?;wiL7W` zwL|p=vmeakPCFpIwFTnTby^j$G8VBSJk|2*D*}hDZ;mcm2}+&4GcWNZ8V2R4niyb$kbk~9 zon2%@Ir*pwG|aRJA#qJ24tJRvvQDUhMo2J5_0y0r@Es+KxLOvS2nfg;H^ih_#Q+?8 zPE-Xzpe#pU&0TgWAx?g*Nbp%*4)S3+gowK~QFzufM=TUKjk~5O&|p#8@U}%Jrfzx{ z{w_PypaDaO*XCd_14)J8uAnz~rbv>9z5Uu@P}uVK@!aBP0g>}kMe5c>sytV<%(QDoOQt=0wF1!zup+3JphCJZJOj~gHGSSDTxR*yg7L7$H^sFU4;-tWU3rX5sS& zljW>+j3U}^Prus&bR^H3Q9d_$TTh2l-(4q9l5)rrxaSQJjdOG86$f|IJ~TH!vCa<& zEZF2VO&J_;k4zqQR5C;k1@mrE5I_#28L!H6K&^!I-97vAY+zB?c;v|81ZTjJGv0oX zi!F1$q|%KEk?qA-Hb;+9j~EgrkW-8{^}3sj0_h2@+4ow-m3<_+d7X$5^{p+2Pohd_ zkwmup)maj_69XBZi(>ea^0N8A)7Czhg(&}}abk-=wEDVhg3pc_M*iLsX5OAK^4$@o zxG5_y?=7)j#7upCE)HwTiM0-oT_FXqv!n8^vtraH48@`dTX& z9X6A>cQerPv;~5F(_R!U%A;j(m2O_yFoXMdT=tF)KP-HAdSrV{cId@XOlj=lEpxmCQNb&`B`2Gs2qoyJ zraX9I0hi;oTSMAJY;4zEuK5G&YfYUO)VWx>gR=1fnS!glyQPp2O1i~un|OGhj%YkL zbp=Oi$`o>{s!0?2pfYaaqSBDhcynUesYC1>kH|SK5#AgAP(EIn;rYs>#5JeU;dioP z68iyYJm?%DWN{peduA90<+UsMuTs_`3mj&T8(09~d`)f!59SS?iGdeQ+}l zf`~y`Jg=$&Bg(dY_89~KHzTN7UvKj2bi;|GYkDAc2ic4q_Ed&Lu#F7=bxo7Wz3h>- zbRAs(z&Ntw1r<3IM=xBRsueuI!h1=4f@~JAp4Wv+E6JG3cZt!w+$WsJrGNtItP*q9 zmzG#JmgG-=ARNIIJ6%;riq2*=?^#!EM9~V*xa#iZw2tJ0gD#JV=|<3eCt>Z%462Ez z_E;m~g;;Z1BoVKb=<2^Ov_K}e3V5yR1`i{8e(yT^O9O>!>9Ge4(Z+OWo-08G-c3pJ zdDCi>HRz)J?d%?VBBkVHv+4y9;qIGuI+-tMu(&($LUsz7+P|4EFHweq|C^z*Q^yFM z2gi`CKqMis>@L}iLXd3DqZu;Si6uvM>gp_TH{x#6*y0lRTsh6Ig3G&VlF{N%g|)Nh(NrU~-ClKy3y@3= z?4C^pL_MuH->A|RXy-=uyuYFel~{JC5xhMc#A5O=y!0rXrcxcnh^7rWJTRULGi@#v zt?GAVfhe*hI=^gf)&ettdDqLM5pBXu_cb#bL*C(gWDkoUW@wx~S%q_f6I!MhW44I3 zM(X`<*qfw@y1(tfqtL+(sAE!`bOUCy0s))vAtjx97#eKE)syAt}^8fGOcj%QqVc}Ip%t(EhNTJYg!wI+-SWU2GSA+N496cN}t zsskOcU(X$!-p8Mj*SQZEBw@#B- zS#4*(Q+34+AOqssC}H$Ddqku|tS_TX-w*8rx#9{#%WlYzj9rMn|Zf5Jt{MIlv5 zV*VJPDcg7bdRANj{Qh!`f*FF(wJvw3m^Rv`RQdkJ}g=r zjdaL~zeB*fSkj>Tw~nr6GLGzFS;XL(6l1uk*n~Gb!{1wOv6}f@&~Q$;s|LwP8lDQL zVwG0{<6UVk9sHe$9yUW;gHW#xcLI8hN0``o<=T-fosPbW?brMIN%!}QfW0+mk@K#Y zO&C&k@OLo`1(qAKx5f?j;EaLboi`-bz)}pjVvbEOeN$j8~T z1*p^sc=;TbtZ*{}wBJqdo#gGh_??KB2&=IH|BI19_6S;jImJZ-+ZuWIE;Wr)1;^(e zR@VOF>HhaR!^!6iX0CFFlP65OyFwb98K}yAXEtFE1sR5Zm{s;;7EttUU1ladm|?Cq z0n3<1FUIHGnj8D-!%EmGrd))k+X36lP{{K_GWnBu)Ao`i9;bgmmc4X50vcv%u1Zuf3;QaaV;rXIAP z43S8cRRQpYV{=H+JeuBDLcr(P4wD<&2Fh6FpyQjs)`Bc2aJ*Fvf{QH!wST+qS|&&F zU6*;Lazg8Y-|D!5T~JwHRfRfXB9#@x<0gX}YELWfIsK89sq8+c}EZ(Yn1 zQk*pk9Wj0*91?Z&!3C)40k4f|mQzaWeC}Xx1DZo%SJ@cIoHCdPoqk)Ha6IrvFcdE| zGj6VjMdJwC1FrH3p({h6K=wGKnHg=1FThv&8I7O-Xs^Eh2?Rfe`B!C>8c-6 zei%9-ebSSwDAvW^16|hID?-<^3LA(#BQ1SCzwG@diJ!)?2 zWut*Ec*0eA1+;>WCOGE-R5qw$<%ilXka-rNIxQ+Gg~t*-*y@S)>6iM*H%4YDY~X%8 zBEi`LR=oT2hzNNlNxtj>t{(_&u@1_BD<}KK?TII57}vlhU9K$<8YwaQeI|^P0GR=w zDZsBLNQwz(mAbY?0^IrAIIq%Z^6R`XWNz%1Km0Al6Ch3m^Q8b7CkU@N{Ew-I4XRk9 z<53`zn+_BE-VhsUkO(@39JB!`duqN}ExpF7c=lCU%SIqmHlLLXCA~^L@!JG`xlW}E zK5O?!2_fe4tG#%}Fe}{7sUlYxatH5<1-L|jn0bC;S)@wr;f8ApDp@ECQavgJJirk) zzF(EL0w8t)#8G7k+KRZ)K4*gvhgdJur<`%wYr@9fKMk)&h&?sjm6jx!^MUennUO{& zb#VS`dRI$fm*&6Q1VT2*);)JM=R)Ol)mbxtXcXySIwu8^M$8Vurwp7?iVZ#Y&gQnU z5nwz|wOS2EyM4L)11@a_J*K0kwMc3Io_zBM*?3h4`-<5FER4VjxM2(*30Azi^ujHqtCpy?F0-bX(EN?kd1b9L%Dp0kP`KI9{Ns)2MzdBF^o_sq}Sm@rZ4cZ@+41 zQfk1f$0bRfB~K_){Vowl3+vtUI|gw@$|w{(5&~n6oXG-51+>B(`Pp&W4>K^C03`m+ zz{3vF8HlGn`FpW22I8nUfYQOGr-!UudjUKwIbBXR4Vz%bb2Ts7QsHefQAs<=7>?jR zu9O6n>xuA-+Y(e3kT0leeg5% z{Nc7!(T}M(Sa-jWQG#@Keveuc1qm+7|GBVMd$8(t%D=h^I0#m6Dot{z zo5J?5ax*dk8Gl(KFslVR0^XX!b2o#|mo{jy=Huz}ryR=6UTeJFmTyCnj-0p;W5u{| zzT959m1v8tsqM?bm|!YaJ&xSLtw0Z$&G*h+R_Y}Vy)}-+%~5ZDnbhLZme|ZMJJVUyU9cJVUNLYD;7?Of}gxRV?ti-I_h&0}f)m z1iEh4l59yUQ}`Ay$^x~6#CVTZ*PkvG=23(`u+dCG_+WoAH~M3Rx#kUY?VN9fvu?5q z2pkLX_h$o@zbhT*EfLy`eah2Bwt_qDVRU!U1!)&R&c9=D6rr}rI&aC@1|872zd?hw ztD{@aI*TQ`r+4>F2o`n+XHQ(w!vLup9TV<&CZUd66X%{8f~-i~9=&RY*uvi^le5|x z&=$5Sx;rKZjzQVAt73Yk2Dk$Q z_i>gl1l~RzM%xr~^LdwW#P9@uVU2B*D1fd`6^34a&x1McK~{Vb4N84xER9M$kJ3ia9Z zmQ^(t`$qLsTV$nq*qKG@Mzyl`A(U}zTK+q+Ia@3d@! zIH-*ZR#j=pZ@iu4i+;iIz|z0BXThT*)-gD8kiz$poCjtB_;|YJ9QBzC)|rDU3NbF9 zq}|;Bsi(QO#BC*gfeiUUem6yB1_n^?d0$yAyghlIm-fukMi+wb23QeWa4O=ppd^Al zcnY3+fU=6}&hL@G{t=+G@pv}HdyFN4=i_-LQPu7!T?S==WuJHk>0vsIWXx6s(niZKuPK{<&^1=x`Pga3(hcQE$fb z0z^qw?FombdVoy#xMou*$(ph5QD0b8LV%n7s`CjD1uE1>eNdX{=39NuKM6WzNJ>w< z(u;AHbo|8VQNmF~mUHsZaL76P@KsynuvIO+&&m}MQ%IrlTa}XPn%Lma&b@pJY4v>d zmQ?9R3->uEyhw(iy!%6IpFEe&n%mtlw>gG5xn57o&JU}-+qgn-W@C(VP1Yf>Km;$& zieWM_G0^YtN*pI>rJMI!$sUQ~zMa?X49zuK=LWnmwvA4?DRY~U4J?|-FVj>I7 z7h}7Tp0rZm_(9O!f@&P^U0ciLL3Vs>c8UXX4T_%XTqhu_`1h$DYK#Hs8a`Eb#LR&m zKtFvub)i8o_Mme}nYJ{@x0Ehn1pz?o508iew3W!Y+p@a{W~BL3K32dmL6>}=P!b1| z#whREfrsd$4)I1sLSvo>Y`zM$mmRTgv;u?$&uzKy0A7jsxM8{j2#(JH1OxO`Pu+g? zGAQyL5u>BL!i?fmR&zFrOc37YXjuir^5TRhbE{)4Cx^5OFd+Ah`juZB0EjjbIAj)> zbgm546PhVkrDY#~PJ$Z_6AM<{j<*$rAaC6nNlf8Qjx{(aS)L1+pV+%%K3M$=EIux? zREeCAuICbp*zltQ?`tQpttpK?A9b|RE;JDCkjj?Tgn3@C3sUBQnn3Hln^}Zv?{T3b!%B>_9F~}CK!P&5 zyA>GfrEGA%=^O|QO@G(ty3&RAq6rJPVwV4t9wzSFyO8rBsVuhs9gq6$F#WInZGQUJV8PQ<_nO^}WX!P+S94r;oa`=4e zM-FG+0_K-7mdK&4BgNx{->?k;Uv3u!lS7gW2#-sKqCpb&nETe-w7DiMXBf2u25&vkV1@|u+TX#wK`!Irybwxr)Y%~MHwBTQSt<}; z-F|>5LuAB!R@KZqnv4rzc3=v^C##rbKXfs`ak|!iJ@T60aI$^Hhn^ z@SKR085Z=;{ZM!pQP-^Qamg<_G;G{FP(%cVr4$3-6wzYjS4#X=88LoXHMjoVI6{C* zgNE-ime5I~>+xK-nPC_uBFC+13gCn)`*(yVEEfTI|EAVf{^2}2t&SJA!)&Fp?B7ILd=WcjpE9g)OdK9Av57nG;XbEDtq z+_mcxKY{`J8X!%^pZK6?ii|Zs6*5B02zU7>Kxe}QhiE^tBqG`MoxM_5+;MdS1E=v| zfgxjw9``fOlgh0MJto$RmA^a?SM7m~R>9iDYoifQBvB~vH=Iiz8ivo)9^n9TXI*^M zq9wi(*13;7P&GYwgL6K`9y4w(W^Op=x}Y&h_nO%;h;l>>pXv^Y5)mf8YlalQWMNHj z^^~4KpwPrsZD`1Sy19NT647Rfi-o_=kpB4U(fzx$62wU+&UblXq%G}^92baSv2{)9 zSBo`mz^D|xl1)ZVqnya~c-RPoq_jM4gyr+h8}x~1PDaa2w(ndjfQv~$>#C7H>iVXL z?z$mOz*&N|zm_G|Q1IyZS>eJSG=V#wOIcT0%tm-#9cA4otfRj#n~;f^j_w*F1+cR+ z_D2^$+K?igxh9a=7N99xkNnj#`5K}&89>0k%g<>W=T6^8SuA zWxW9M5^9)(p3mUt5lYdS@0{)`t+v7ZOB^;p`YyOV@vu|5f&dbiYuyg?AY;a_(mH3P z`XZc!U-kRMr{y+Nqh~4}G~Z!za_jWy$8BO%d!BGHodl#wq5>ThmqD4uraQ&=x&mPt z6o-PGkrar87YrHq^vQzJO98=E3m18inAYC)838hy8G^fRyCle6QFzrAx)oF}ZoVl) zQj%@;c|%A6E^5RYpAS;ZfzxQ|BTcpsD`VV_O4PeT#!_=yp8_s%!li#_1xO>)5ck^U zqq0p4b!QdO3c#xH>#DJ8BQ*DfZ#%#N4X@>^16v?Lvf}~Hi-}q^R?YbF-($S#w#CHH zVh$LAhr#Qe#FfJdENK5v)+&XWx%g#JFGEu{3=Zec4y@4{x}lQqX(8L=B`Jfx#1iaE8}plvGf|wCRLqMuG#}6J7?SL7Gqr%o`on#&|=5 z{>u{rIVcP@a2s=6!pd3R)@CA8WuQ-y8SWm@L7O8*`_9S4p3n&&u4rnjBKY<3b+>hiYIj55BLZa9=`4GtG7k}$ zrH6MPm_zcjdhmAy1YN9FQQu7_p=H3j@$VXyG(d$2?`jn{Qv-PC?yj^R0y@u*tAomn zvWDA-g>#fK@JJn*OP38#EBM2eMm|Pk1b!Qhs)Nu)!$S>NxQz(_eZfSd9$-#n|9a%` zgM{CWUjgtDG6FpM8V3qg32<~eooIA64|a$DJxw0^>AIfrB&YQ8E%TfMPK*=)9eXMS zP|~gqR%e|v(dc0^_^qEle0_T^9y_vRU^gnjagSECiUHYuu8A=}&Kd_^Teo=Jn)Uls zD9jXToso}x)Xv4ws{A<#qt3vcmxnsq!yq$S^9>ka&mz$79zWy!Y*=yP#XOXVa=0$M zH`&8;1|z}4`sn~7!lnAn6pR`IWs>>&c&PfloP3jqmBWP|?8A;=PL4p9;k?KfL>^q? zJvi665n6Tby<$PI@-!bG3k2;z?Q`xuJDjj6uDbC-uvpi#zdmh@LV&g%Z?Bu_5&HlF z`H`MSb}y6uK)$!SRL}qih(_~y#Y}ZwN{c0cd z&K-9I7Y&_07#E%nL4D$2_v7Z15FV~ZE*#5exuw?%D7ZC4n5{M)`OYE(2xN^KaQ6*Xsi18g_NI7 z0P5VAygA^~*#YjGeIk~6Y4mt$R~^hO@z-Nzz&Kh)VD{ZLX$NBzYX94!vXpKP=(^G> zz)oL2cbyRsbZ8CwOiQy+sXwgi1^HDGF=6>uUbr`sEkE2tvf);y>F;w&Gy*GD18**n z;$}Dl(~nI+K|X4c{EZeHgi^KmqbM%1JYr}k%z_a6lw@`v0H=Yf5eD4r!|gIFi^&PI zQfq{&MYt$67cHZRyuWjRDKyJPzPP&4hgi|rlX;m(R`LeiI0aM1lV0!NQFmS_o@qX+ zW&)(M80m$|PsAYrab5;y0TVNM?rnduLpD5Au9%|-vzwg=|4-~*$r=tAZwz8HDikB1%Y zfv1XtgcxaYTA(})VVK1C9BKqa;*tDcXNxf9Js;O9Fxo%{ll4!t#z>(A)ni7sM;KHC zd5&N#2az0uUvcQ8rkY3VS56mzge|zZnx72CItqy23xR-8@cQS9vCdb~F=Y4jytR@d z6!2IIe2UHxE}Zu{rO*jO@Pok&#fW8oUQ!TZ5hOdR9v}r^iR^A!Pv)iFx&; z*aCLL_Y;8^_)2%&zhCn!2^Ji1d_@(B!D&G*4m!e*j;4&mTQ9etslJF_dv#+Du?^<9 z5BkhN2qhnPf(8IGE4Z(nv+41-!}hJOVxJ7Ge^8}E>c;~LuK1#XvS$b0ul@D@P=!pq z63+@&p)ruX-UBh>ob0Zdmy;)1N#n0))h#^a={RqnFHR5(FBk3sv?HSuePJuw9~~-b z?_HkoLd1pYzcW#sU`;jsYbNo6r5D_X8fCv@9O<}XY6~%1RomBOO*?_)YOA@pw2{yc zIb*m_Ux371#Af^r3g3w;275DY9S+wukt4S#^3Jia@Zu1Ckw!?rKaRD)1}nqp$ro&_ zeGjc}{D8$8x~uQHe_CA5v66S2kYz-U3j64<5CTCMue+)x?V*6CUbh8S3+IQG`{KHA zR#*!D?xd8Cxu*-4ZLZ6$^gOz0>5TvogkZlkEtKK2mhnO!7{Y$cY`+bYMyU!homc(r zCAx5d;Z(LxGc(F!m#ek$wZ+x$Sx?KiXu!ZL$Z6@0>u)3=Yqc5Mz%JyfcL$Z zyh1TFsqgJwF*#G6`rX|%LMBYv+s1WS9~Y5673hcQ^oIalK%&1Lk6ZxAX(O%ma!3xC zq7!Li=rwI~(s&VmCWgl!oK0tMeak$H=#b*OT#vQ4E)HDyl*Gi~4!0Ax8b(Mtl=0yU z(Lhgk%J=;dB5B5K%W*pwfZqM%sy*9tRXD1bM8R2|F-82Ght3rdJ7gY3TrGnSPxm53 zUs`-O_;@rRr5}kO1|IF)_gibI;?T1tM$JXGcP{XxMBE2oKHx|r`US_4g*6{t3PAWV zb&wkekXe6hm;F0U;>L%c8TxEEE{Zg&;Q$|stBUoAwt_+V z+My`Z&L`RDlK#>pSyOoK&yLfj1LU-kDlJG~*oX1p#A`IBstms3O}5nCB9ko$cTfLr zKC~3UN8!KMQm?i*4nOSTM*)RF9Us0%sSP0^=)oPt96zMxJr_7DkV0JE-KZc$f zs38*hXXKa4Y?tFZ_cooK7*;&9TrROD@6eNFJP1*HMEUUutC52gD=w^|#qh%n1=rQt zVK_nU@~odXbzRM6??f(baYW4sb%+QhFscCL~D8vRt(X42gvwBKGrNjSoKa93KS(WYj<2yb}MoK8}#QO6wfZ&Ru(3LV8Tvd`_7#ef~qb~ z?&?7+D2w^!x@KxY8gO+#%*6u;iAwLs*G7vXid1hrBsE&9I(gn8R{=#=)@OAMa!^-d zdP~f|CmWl|c_Yr2|CETYXBRjp3Ebg@k}gJ$j^aD0I$gSRu(px%_t1+A8t~we z{qt(9^zh8zpSd(&o*jqZd}Or3@Xhea52}uI3~_$=LR6TB+?ex1A80@@)NNflxs-N% z9TgkvtR~s+l-Usk=$P(t#>7J0v+C<#8oF4Xl>0qt-B<3|vF%@>=Qv2o3I6?qfuT;D zhVRyiaCU)|JT73&i46~!zG`D@6Y;g{nGzV3Fgk%fqaWBNDB^`je)yo%LC*S@sj;Xg zoPFN}fQKs7M|IKd6&Yr-7Ti@0Hi75L+H-e!yf)B@ao_=Pm>L`%T$uT43PcUc{|ZP! z$YXeRTf`=xvp5bfb+IvwqXYECA>tDb4tA;L{=nE$1w3xsMC_frJUVBLD}rKjS_o%_ zk!Rw*Ba}{Yz=l4o3kmF_Bj~_8mfpew4;;61i43|$^r#MwE29)UFGMIP0pXAAb9!mA)w? zppVB$hudgS*`($(_B`bk*%S*hKPVuouR@B~FHKjwq)f7|Dl^EGrvU4<0rr?UeEI#o zqDD~$Y|_(m71=@XX?f~fhz-fD5Z_$D!CCg`=bVf^?4+=Le_8{8-~co13+o4-gk#8WX~!Y z9@aqZmZBfdI{}pLfk+{HV3gbsU^|~83WHyQ6>Qw=wk6bk2z)-V*QltDQSB{}$q}UZ zw=QtnMsy`y7Y%1K8}#|OJy>avt`VGdmPSg7i;{D8ZeWGLC3;Fo0BblQq6ghkli~ps z%~=gwMgSWS9hQe6qUaC+CIJ`hPyjEzS>;+4P`LQaBO@^jbTC|5u-7C7L-v)?v}i!! z!~AlYZB*F;!z&M+<~>*&{P5@IjB2yb1X z5RU)tl8A_C=sH|$TiT3|pVvW%t|7`)^u<#J*kmbO4+;Y*foO;6vr-!H(dp3Qu>~X@ zD^(KDD%)}vq7vw$qDCRSD;&>B>t(J}Cd)wbDnR1n6mheR;H|6nHAu zaSZV=xnyVCTa0xD9-qRb@}**=f|AU#NxY%^7S*s3?~4O(YoP5U+q-o`O$Ql5*Kl0Z zc?&~R;N4Mkz>v&^TyQt13DZyZuN^|eOx?eCkK&ZC-Fd(Rx%Mj~`& zJEV@1C8plU7mlbAWQ6&}`QRX1b$cv)F|VW$s`dGxBW2XOa+_zpyCJi+c{*&6>H~3F zhG*@y@g@E8;-gb925Mu5{z*2OQ&l7O?mh)@rm)A$wP)G2)o4Jl%LD+vohJbd$ zpndDKkLbE$zl*9hXvV?hedt4MrhX83>R5}-WT%j?4lBC;{H*vXrq|zvI~s&A+cXb~ z>uyHj#N!&6!IWw^q7?(cit>Pmr3gI&$JzPkE=!qxSkVlo`k|;w>{Z_Rf%J( zh7)5pK|>im9~bUxM%ISHL#YdG6cmPhP&B(Lgo&<)`I$UjmS+B!Cdc88!4n6@AQd99 z^7z7vBpj}$e*E}J00%0;-6vDJ)|Dl?9vNuG+%zzT`yQk4ur21jY)G^$F$$KalDMjH zpz6R=d*nDeK?Z*{&za|J>FlQme%Jvq4sgv0grqMrsC}^xB@StM_mjakK5neALEH0# zi-St|cTEhu)?BO8#?AG{F#lZjXhzM65l9ccka^*Ewd$(XED7*Hvu-;oV28&7vFohmvA1!UlK%3rc_}~_FnaXXXfh!|;9cU6tu`8IKg_*wMivi;7)8&! zr19rAlfG+Fp{+28=yO&%Dr7jTxZI2+4bU#m9~Q5If&EyxC?o_H)tErXeT;@vT7Eil zDy-&==+GyhfU)@~rakc{;b#?+h~wfJAS_|gby1)YJteBGH{=Qq0hJ(jHn$KmNL-9P z7Is-1XqNf3FMu?8WIO#$!D*RJf`<2suxJ6piNt~J5Il47Ik<0z6k*LWYQN=dK8uFX za(8N7Fer}u?!fCAWN*~ZK9|*)ND&`5Dv00q56sI7qm@hn$o}b;t1r=SfLoS%!pK8q zekS686DgFNueLFBP-bCtTrHv$y+M=L1tQ$3VKnBri`8~DL>Al~Q=ue|@oCu?3-%5h(HJPyYu>vAQjnt{!b&DjjM6~aysa`1_w!%KT zStDu;D&?#(ZWD1)WF1$L0dlY0;B}jyrN;~fk9&l5p_6Ust5UFuzaqZRgrgA~6g_>* zF9?5EX*53_LmB{VxpLWO*HNoX$m4=(R}z|-J?%=~3!V_`R|yB)(g1BeX)2Y~od}|< zUN`jXz=6-^8pF`E`EcG2K)IWKi5HeZIbj`U`*2$f=^C}O|3*oGk(`n`ZdQm-960Ew zHnP>O_#xntmJmcqaPejfqg>b=Vg5@3jsSu;`tHV;;%WU%d&22LV(bCEpWF`IAYuWG ze-inaiZDxZ)dOlKs?IL|Dp~2?Q5w7~hai~>mOpRJfJ9Tm8up^lDB4khp4a>!motOl z=d|p?P?k$`Q592>VFy6mj}`3@a>VNiyOIfmLVC_v+k71aH1L)u%f-{2T-U793~*pf z=9wfjA##@%PU$n(8)8J$H&0HrO-vwM)yGBpq^rVdU5KfoSZz42=4^x7?9IpO++U=PjJnhM9o6CGiJ#EWl@Ek%Xkad{fs7otkcsNGF~o@909jO&@aO}Jb^<9{Ir z38!b=IH6Yv0SH>3Z=7Krg=A>)TZR(5n&Jmne9+Y;XTL*Vf$X!WTM z6p$c@{=H@lqa2r76>cbfrT_)ritpi`XmZ^k-muioX~kcRN2>ki-kxMVQ?^F|#Y~%j zDuMySFtz!r3goB?bGEOofio=thl=ZRxiH^o5p!NqzaRvP%lGxwa(M-$2czQP@Z&&_ z3y;=R-+)~DuLG#&UK<9EOEv&dm-^+Yu#6;SAJ87@Xqm!_8S**SGN~DaD=ziL0xfSo z?t#&^F%9gg$6A&omIE-Kpe_vtz7co^nWnrBJSOX24?r;ZJV!Jp3@k8}Dw-$EM&CS@ za^(cV?Ad8oz`0nPi9YV|io?VXNv9pKGWn(f;HjG))KDp;9P`y}zzbIL87X|7mPNxp z6D)ScY7Eh{J~Q7K45Im6W*mYScJQ89bAs}8)!12l0Ahq=METVc*AmQ|j;G~8S{B9y z`CVQlr*`Vj14$x#4WC`kxaYR{SU2IE5~hM^C|jNy*5d(hmG`Yn%$QDmHD|TksdaJ5 z`>6>eKiLO3*JQMQ&|)+8L>(&b5M5`#r)Gf-8xZt*PkJ|Otf1Uao5G{))Y%7>PESS@ z*u7$lbJO3a&oyVfX7qX--z|ef#mNJB++eHQv8JB$)<}@~GbQw0W)qaea(~bLX^@Cv zMTWmwyvAbQLXJy$n#cqv;k79$F+|_Mo)vrR?3QK2K~MNOan;?vF$4_))MnY~fOI7e z0QEW+Fq<-tA>oB&R#K*dU=(6ne`o{oKHmmTR$~nt-Z-*hT2V%che|_+yg9Hv>@_Ei zrKIKSwzOo@KJuM+3MLwwnDn^SkWJSeF28Cj777F;>z5to5*@fvKCwp>8$n!(Z;YN?2xcuL8>kSY$AlZ?UWgDBmBnWU>Ug-tH2$X@UUo19s;|B%DrdeQoVJAY z3L{SI#ChyZx{R<821N$_G-$ zp$d){7<)PsVEcVeU(U3fLg#aAb*E@7@jVin?@pn>^F5mNb?B;kUlgBHgUiF?g+7qI zXz;)|g7Tk#Qj=DNo#zRQ>5MhVbPYdD!6JSQjHQKC#pv``w3&|0} zdv3Rc0urPk>gSrn9_DBoWVoe_1Vb*jAZ(ig)Mlk{e)jD=4d771Stki3AbW;!&=W5Z zHJhh@%-}v@sRa7Ol1l0?$Cob#sl^d99lYe|Hf)ISoMY1oHeY5X#MTAm<+Ef+N*m0?)Vk5x+pMY5l*4xK{ape zK-TLyXn_Ei3I^~npgGM10O_qee!Z`iQP1URgBswX_r6gaZcp%hy_ZK10lgH$t_$~~ z_iXF<+ZMVDd7X%Fl_67Yi(&4cRZfRfqAgFHJ1n7_WA2q%0pN@p!)0GZ)@1*mnWl~%vlBqOO8j{28TBVxdXVSY0u zZTWW$@|L97Ca0=ZS5$y4!sS8E?`$zdAIH2nnS-t`0*Zl;p-sezoy&L^n3XF-up$lv z^1<0z;m-qbK)9$jHzI#o@sFh68Uq6i~Yb@Y#H=Jb$qhd=l5L5<3Lv+wQr4j3q%gGd(f4tGi|utZ!WLU;echvIeW;+QDdp{ogr?xqN!Lq zX-Twaf)|dDRYlC;rIvDDQWdEX01+ST;HQEy^4@u6RE3s)>i8=$%OOgqhi_)~Ru7k& zxS|gq+Ag9!&l%#(v?|WQWt$BY0D7vsun82jIRG=R42DA(GIhn3DEZl{o%PuS3#9_Ciz1@=Dt8FcS>)Ju(u8|h4qtC% zXrAY_%ZWsbyM1uw=7PV=r28tx7UspE{r2P;nRfk2BbcBj7iU`}VJk|MJkIEla z-~h+sfT9zj`t2%`kA-o2tiDJMXU$|rkYk-V8$3~1bhjGDG-poG2U>2{O>ld-WFZ!n z7Vq9g)#x%zcHW(p59~N{>F=;N$DEEND0c_pvRsXxJLs-tB9JZshXe*&X$vG;D+Fsn zdipwzOatNR4@!wkt zRjL**{`$g(8`XO6wXvIzkOimZfFn8p*67{72Z|Z#w%*!n-DzFH=&)jSX^3tH-dcvj z`s`}?)gQYxxyT)VrEHck9QyTK7e2#R?UM7d>2RDGWBpt7!-}uj5qAes0i?)-jc49w zDFjc+7iC(j{`R=D220YdU?=|7F;pZ0NZ&A1iBcUnoPR`MFEQk5b2rs)ii9enQ zRAO0TN6#^<(1wg~Lheqyq$NXQ{oC*IG0)HRylJ*HQ%ez^Yrujf=!T!SLcJzy*r9dK zA?z$=IG@{y+_P$BO7J-qFi5KrWS#8JMi_t3Uf?)(_U71*N&ROgzFAYUN~FMdoR!$CafLQ zb8Bc*$ud7VdB|PvwqpR=SMHCD-z(X3VmFqBS1eA2IplgYQ36(_~6|!(Zn~G4~sDf z)16>@-{P1SayB^p4a0X0@Wg=AhTX{^0s4Gd5fgdM&Ovu$CvY%^yZTR_6(A}^Ctt)- z7(&8zkP~{%ifw>SJLc3D$^}r~H(rYtZZ00qtHO%Qh7QQzbjPh5PEYRM6(C@`8tR){ zQ6Y>N5ZyM$hZ{OPe@@E*%Vnol!(DUvcAPrD@5VL}Ni-Pav$;vNOjAJ4tE&fsE_d_6 z0Zvf1ae2<$=CsL`<>Ij|K=5r+*m2STgDPr$GY{&56y1qb^it>D#T5j!Pc_8K5+LN_ zr;{~*rA5d8OfXHdVvqHd%x=a4l#^a5ElB2jM|e^m*_RdtUU${8gXG=dpN%c9T0%*2 zSWVV6n8K>FiXqW(FiSgZDGBRF8Vb)n4W|J&aOdsn(H?DH@i*Nb4e7A*-EF_IJ77e( zB_vSjg*o0kT4_)ej6R;YCihd@wtLGd6SkdWt!p9&MfTvK-g0lli5w&$u84uRum{Wf zd^~=fm27vx1*vydbu*9b@L~bR0?9c`WN6xDS-vNx0uweS9oNii%G=Rhamg~`%Ty1E z-^}o(xJPq$RvE8m05sG*7gDVj29B`vMbiiKUK4?JvMXBrjf`_+$M@l^g#xq>lsSEKmdQ*= zQ0R@Sx+>GdJePb?j)Bt>bu+7~*_0su>#cMI6dxdW*RlL?z#;IT38<(5D1E;1O{@jd z>gAt0{@!D0toQt@-K?{~0yIPirgn7ETTRx^RP0FoJCw$#GKc8AN+~@pROI+Ou;w&b z3iGTJP)My)P|w*&`l79}_RUkRIaU`cSFJ1X69GVw)3TAWCJyi-`zhWUlVx1>Y~`8I5@c7K6SOZh1=PWr|R+u?NRb|(bPy&AICfIRM4Zv zS~%dMI9_K{gq}Dn0V^15l}CTIO;B_~kK}H*1+T0i?(PXXNJv5p;E7lyWUB$s&ISZc z5{N+dUMOfIZ>-4sn#hMC2`qxYt7hQ3^MNSOsic648p5B;6^al%2>3u*#tA2yb)I>5 zi)U?v_}0ZFd+F%HVMB6vel_8&3S5^zi_P!{g4&8i)yYgnoQ)u#7qptqV@P z;PfO!@YjVoIh-VBhQ4>{L!pA9a=r9%q3C|JZPT?ne7s7+~Ro9%V{2%U>@k%Q!9A%_P_=Um@ftW zE?7o`0%`)VH=2UlITHc-XI+V+86Vf(ha@ziQX>9ZI)XC!Ms@ce(gtQksecBO@cJ|3 zd*!GG*%%L47ZnT=L$(QgTO1F3m0hnd9Oh^}<^uG`mCrl|LDXCr*?B}qE7IRnU16X> zaeqCzf(mki_}69#)2usME*qv+L%=n}U0--*&aSXNI@W63Cqep?&7#RMLgW6ZA=Vgx ziOetMR?&i*l&_g%HgbYj>n*2Z$f$AMej*czh1#(Mm%FsSVIg@$QIR2ux#byCkjsmI zOLCX-b(@8aBl@nOAIJ|x{BoW$(x$I{k90se^d(~Rt2!<=WP7N%?wY2APGnu<1eeCk z(u((;a35!^qQ?poe$w4dxN6Y0>5K>xj|KhUqXm=fynIvHup-tE4(tE|!X<_KGL_hX zj4JtC$N`{bKtyL9Q6+ofQtPUN$b`2Y3LLhtldFld=DCA;6l!44c<%!rnjK)sUU#t* z26ff;ZKdk;tjE?aTl@ogZ;1A>m^M^fTR3Ocfl@Iw{5hyG0~hIc$sv8GfSVdfI3Znw z)l(PU>t*Hf0tw#uS`crqCyAuEkMUgN23g^EGxw|qrrio>EBT>A;Xw|1-_?fc9gLv; z-d|-PeS4_SHKO&A+q!$!7E&r&8st2)B#?Al3hseG+z_0!IQib_iiIOzC%0^66Oa*GRk$u^XY3N{CUiR;N1AaZ)U9iby$EqXc@;z9IxHVmU|suva3N)Z?yjs7I{M0xqjN zW2dwV^Rq!KjCCYP+*V8{)|Ex{b^8J%P2lj(%S%F4jh5nX$D>Cd(wnn1fEQ1%|FF4rU4Ula)2~w()Vce(7J@uf53zocIq}%XW!r!-h z;n;Es`+S5Z9!8|0xL&iJD>S8w^Kp-9yxPXk+Y6AOIdboIGR2aGBZXh2 zQ3WM21mLMppMswmNYAUtW9V|Kd1bK=XGcDXpQH$~!TIIC zxBwWyU={i>tpc?V32?l)#)mXH0&w?LgrlmaV|i>BEUSR772j;O>F{Pv<9OxCJ`Sl>kn}W~JP-xPIMcv}~SgwZ(4q3fz-md>Uvn z+qqAS79&RFkp2Q0Gl2-x_d3^Mx@a*ZpHXL^gLVf1%${^Se|kOh3J0$u9-y6=?=~#1 zX!*9BChrRaO1|2*TZ$m;@3GUE5oYj-oELD8jLEn0!H8sv)t-mA*K%rpM~%^f!x>D8t0oga%TO1e)<%(r zsHN0(4*6IQ+ny$L%x7j#Ssv>~i$VXkQG zqb=zM<#_-!xX=mvoNQEvsw#(!W9{Ja7y4P^G+3ygg3vxZ5LniNwhY%lr7_UOZ4Te| ztJ;`rgNqCMUCoB7`0r4p_Fm?Ie52Q_A4%b(!tet_uz}>GNxWo{fgxrMSSD%Fe zs}Ip4J*Xmq)vF}nsE~di^;VadOsi3OW0T-30AHLZuCgwrJW9E)r|Fueamrcye5r7V z-ajp@JFNxqzU+xE+-T1V?>%;|aNXN-;tZN0+B}LM?vOcQcN6EoHgInY#klV!$(-I6 zA^Gle*3Okpgy)tRF!mw%^4Q-3YcM^{Pd#A4S5*q@qdp9fLXk0tMe633+Elp!xVIix+4l;kwg? zcx!PmVd}b5TFysNBKJMfjFu@}Jn_n2Bc#jplW!0!OCC3$89|QhYY>+wkEHd=l9jxd zdH02u6^^%ZeL&!N{dLPSpXM0o1jjp^?h%4@;bJUCE?dAVn6ray_-fpW2MS@U?|k7kqDi`aXUb)@Z&c)2 z{ah4vIuP;POVlwhNDK!?+E(NVtokqv6GkN!OmAGkdYNqpha*Rs?Z64@@MHkbtftv7 zZd|0(CFMhPR8{pZI{ZmsDCq!e~7&(FvhVX z7RhmY1QsAw%f2sCPAE^W*@a&UiG*#0ycfe3Vxm}v!$y|$&<_3jCSQpg>plSIo7Qfe z+MaP4r?W%=x)3Uu9CdZB zc!TN`$gcA3qDVnBMD-j&{UEyYWq5g$pzvuH5WrgklT8nf2IO7>@{f z^0Z?ljUZhI-j%VqLdQ@!Z_9Hde1j>Ur(Lz zL#v-hbU&F6K9bpt^9cCRtc+SUuV z4<@})Hi?yf@>2-R6<>X4b~2ESg8Xx28f!?p7BF0Q#)@HIq3KOUtTlKJ{rn*=kDRMl ztAjC465}GszEBg77sxiPPXQpriyGCT=jz}?bO)4uy0PF1+LpOz6hWZO(dF%=7lL_u zqYNB(x%K*3=IF;Tg6Q?E0^WI0iiGk8^q~{$fK?4hzj=VCY7GJ^H-4*NXJfS%31qjcZdi#F zeH%{BY8Sxw!HgNdtZsE8aii&Zc8Ec;fL~9gJQP=iz2Q76wkND{xqXO9Ls@s@VH*bCFP5sy39spVH9&!Nbgzet1`Ca%!amw?fG93S7q64H zeG#Mg=8CYc5^f+MeG~2{G{o=rQwy;UZJ1DS+SB9+B~o6)Nf@j+@?y%i1xe{Kv&;+Y-_` zPUj4@Y?D)b?itIYv^ta4Elzu zUYwE6webgv!OO6T-7gZhKLmgR3+&J7$9y^w#Rg9u1)9=sgQ3QEWn25X(1pBT1Q!gZ z$i3eT+-}7(K;p4~LP?x0N(bJfpq1;P`N|EFPFw&`y_s6cWOmp2&bwu{&Nf45b}5X|s{GCMDmQ1MiMT8r=Vqunk~g!*~{rxRJTG%ku;PB2Mz5eHZ`+ z7Ib*qeRyY}RMfe4Jq?2Q*|GKRqLtduuLqBxMgjAPZak$17fok9FI@sdTc_@9M z3lBWMN*oTl*-{nU26tP`m%SR-ybCw6@HCRk`mrEek}vSig@x!$_}E*%Z4=OJ&yAOV zYNL4&a`t@T5rkiDHLWj`fbdj&e_s0SL0ap}TQr3PUNAf~PrPz!p}^hqTb;Goz%C5< zBMU+c5UN@njW&qMXbbQscujs-O?&8YyMq`xG3-#a8)|W&Dqrq-&=YO8>yjZbjZ2Jg zS5?$3DWQnsxhoVtuxdp(u>uoY#Ou&6AEo@b-2on&hO3y5fIs@g2wXvI<}Z)X*^ya> z;=LmwiAz9eJazdOY?+qql_nU-D6Apkp(kjBIbNCGwu^Z1x`6SqUri2I8mkvBfkfr^ zymi|bYd{#gD*UrJg;-aw+yhnc2;K6YI9CGU1`d*W-lM_P7XgN1o?=13 z0buIXPY5%=RPA&M(YD8R5L$FCj(B z6eKL_QA8$&ejH41Rye_#3u@$jjV=V1V^a=ET?vHAC&oWHjeZCSJ$mZZi?-%C=dXgP z&}A-JeQFEjT@aEe|2z>yL=y$!AJZ^*h!(hAk9b#z(nj=cxG#LOgJoA-q4C`Gruft& z;iC@A1>ty5h|p@|_*)j8wbUF6O6udrP)! zHbbVRYd$}Z2EouwbV>waEevMS*4@p08?dMgV|ypdVv_kkZA z*04pmpCri#n_`rY@VIab@%XYGLQDmydR z`R%YeW^-tYm;Rd6h=+m&@vAY}XMgkF6Y`BlPXLKGzFiOAc<#Ahfy2o{M5*Tq5!hRt zk~`k*C}hjm^5yOpe{!yhZq^zjn#?#j`)?1)sSGsXKy8TvRT?BX`qBW_1Bo^}jmUKa zP5G!Y{QP2_RsnAtJdQYR1io(-f)mrAf4M74#{%l%aeufRfkH{Rl1|@ygyu7!dP2=4V~07zb)(R)XSIXCG6_QHsuwO*vr#JF$>Vi^RE%yNfv~ejCc$r=(a^wS zMT&;4sTxsZX%VVw5PMN5U}Qu1({0M{P*#cy+;(6^u3m6c%>vsG%g?Q19t%|SUrp5n z`j%>cl&!||tHpE1&Vt6Bj0}D7Rm2LliJ`~M z;ohKojXR+d1 z_md6hD;{56U_*fp@Za4E6x3|hs<`VP0UFP7RCisR?xE^Jx~j~I)5#U7=j0$X_c0uA zysdoFUe(i@LMY8Pz*sv z{GBFwECK=Ib(f-YfPRktCU5`)@(AX;vI0KQpn-KzRKXRs6_nnnpsNNL?trflhFE|q zz+d`uT0KFn@DbAxfUSFWO!*v$!NF{Cvyc~}6(;<-O6V0FAmWTrK~Varcswu&h8ltm z>AMM^kYU0Z;WI{ffp`LP`Uy455+)v;`^Y|!*6baj5fwwC5KW|*K|7@Z<20=p zs7L+bv~qxT`?RYkZmp-uwW6kADEMHmYM@mD8;%mE4Ei;JEyv-05<;RUska|UEw`W* zGWVCBD;-)NrLMZAB3HO-;3F!Q0t7|@ZZO@Q*Ns8X{7l#1qy~DZ0w%b&dPH^ABgV`E*M)tE91H;u6a*jpbu1FjDYw5@?-kpVqYWTZWfWqRArhiB=)^F#- z9s@zb66$p>j`s%M$CotRnbtswwoy2v&EFmPm8oO zZqaDILuB|IuUbn>tlzP)@sOo})ZQw%1&iag$++Om3l(NAf9In4BWL);Dn-Qa2bTV$a@C1x+UTjr5U?P{$HhgRt&^B0h zL2Ch$ioT)8-WeN@zkkO#U{uCz zarcSZ=A;2Mu30o%N?IVtFFi%LJik>q>MGrWvSX>!-t4X6Sf2WOw5G_=1B|~(uC5a! zmyc_ND?;nS=5>9%Kz^PPJ1`Eq1T`!mfA|_zxet`_!9rD!cof#I`@;f77I4j9KeJ*m z?cSUe!JCK$d~dh3t?**ShS3d|W{tFU$Q({aoez!c7H%6)IaWDW?7*Xuuz2Ziyf{d1 zyR3(+X?sLHkO9&P#ny@9>?_GI1OrRy9FotLOw@$T3vHd6#k3Qz$Fk}wXSI4hmCH~P z8x=21&_~1Cr+Y-DEccGzl@Df6lu+5D`gTvv?>L~%qopMu6X}3ET!>&X%N24bG=fbW z;0k(3v&OSyC!1IH&Gm5_E*_E*cnt66=Y_Wb)_k5<-UopzLfEEtwszb)-+1%a1T)H0`0%w1jT9D3r;XK% z@#|atsatwDH;Uw|eu5~h!6IK9iyRZ-$iv^wKsZ;EEMAMXykg`4)l=ELbS0SjoYIGg zTrD!{8B=JREOJF&(-`HlMXT^pwN^7_Mz`;NB_JcfEb`qpf(L@g>fFsn)0{vg$Y+bD zHAu#JIB!gaH`~am52L__;VON7F$oi|K3cdA9GwEgghS4CgKodxYC}H@MXBSegLqZT zUo`N_?H!s|9-&b42r{{i5MeGf0M2-v2rDIQX48XNH_aw>!Mz4n3XAT`=i`FB z@JP_XhWlhAp~5Iaw_NYMqnyc{gZjeLF=#z4R9`j(k?6v<@aHhk1Cg3rNc# zQlA@xmO8%Vu)O$f21D5ho(D5wW5tNjI4@ucYIK){+lt8JAcLpGQFpKy@gSJT5)pNQ zl}v%VLC~;U>6-l8ctnF0VVD1A7|U6c?tv!K%dC2fezEZZ#+m_ir=`G?_WQeYqVVed z6rTMsC-w%Xs^cSRjVK%#aW^aNRHn1y`8R+R3aO4hRGnE(tQqZowCaH4g-VsHq8U4y z-l%X`)FKvtlfIWFGW@lQRJuD-3P|DF-`VvURmc-R1SGrSjIw@OWai>hO6#YD0UcY> z+AcL_Vpu6H_SH4${3j~-w*$AvBT|U?A%z$Yl;3psjIlxlR)E20i7ALe8smKU>_jw7 z&)l04WJQtOAU*nJ2Ka?U*Pj!7K2d8Q9{DNCHWc&tx@FA(MEX9j8pgB|y`39y5V@vSo6JWBY19@Kd|`p56d7oTVd8yV0&&Yh)V z<_GoHb}8f`&9M6&A5&-6CxQbC!k`Jm%y^=S+1_yU>?>vH5PssJuNIxdSyhCY`cwy?9n5GUPo*Vd-0xg@z z-EGG%k-iYVnUvu-d)US^WlvaqJQ_b0Soe_?6TxfYicu4MItq|r&oHHwWt11FpC-jRWBYadiLGSZLMoup5@yHSzZD3{=X0JVYFPy~wt-(4} zw$A6ad7e#P$~Bj*F>+vgX~S~^Imnth0=e<@7R^?6)tOgcj0H|KI5Ezs zrWTLq)8;q&r z6;UGYFmzU58h)hG8m_riEV7m2_<&tLQZzYm9#`0xOuA_LLOxv4+QjB>!bljF&RcO+ z$H@_AMB>k0u;V0cw7ss>m;|y>zYB*ILB%-bg##dqDA`m# z_eNn+GpNmF?_M2X+E!k*$!D=}2kM&7K#?7`6;D_i09HV$zd%%b#1b5U<-0pN9M=W2 zD7+eru3FZ?O~^;Z{4)?xz4^m?>$N_@)xvY%c?~N8<9hMwxo> zSK503x97kkEwMf^Om-gC1h%3C1R74;lk7x+QReT&56`Vnk-yF3u}FOgb62{`tQ%el zKdt=T0wv_VXP=TLa?^!Z5~Vqv!2CNR(*#lswlvRMOTCt3guEVSvTlXgl1KXBhGj2l zIBIN_fSai0SMU;{Fn(rh~EpVBmWd$<){ca@m&cC zI<971*OScBZRZ9X`hL~H z+D6K1jaym&8K?sSbF-)fT}8S`d>*hM8eR5o(1jHzJ$Z#kWp}jm%{ewJXuux% zav1}?t=_Q*Pag0~)6KcC9*L$MIBX-)VL~V#Wkx<*LJjq z-dKVwv~`ujFzuXyCet=DX`cKzseJER5_W($3#9E>vQUxIAlLi9-~X zTi=vqTx?Ls;)oAc7gTUYd|fdfjwTB*C!^GXGD~N7tk(pkm``^{`w&J%`(eckr5vD8 zZPoZDj|8X2C&a!gi+Wl+!-AZO)H)RxoPEw!I#kKt7%ifTeLDTjpFuUN9JptV@I%u0 z;pw_CvU^JG5c=={Xve7>%^&AM_;4KrbmSfdFtaf3ZcHL&4R)32z0qhSU|*Cz7B@^n z(x%-%c|cI5p1u4csE}L&JfiCL_-b7pYbea zz8E~Z44EG^DOgv3MM?7Ez&3=}5Otdn3LXAk;*n1!hzc*vGlhY2;`_=PH&US?tDf32 z0nmZw&2h1!ux>C*KA7p%)=yIS#uthpN4E|i%vzR$0JOw&eL_^NWaypN!=E05InCb| zV!9&5xcGYsa050e)oD?fNkr>VA9XRWRtm)0Eo;~SF~;U}%Qhb9Uoj6yeY~J@!|eCj z9?Q&NRX=VJ52=b_Gp9|lg2u|QcvQu+&JqJBkL=C$i&0nhy8((r5Y%Lf+ODsd-)tYW z;l_JFnB1%SrbuhZ_#VgVix>jJ;TN~LZU{KyJg8NOE#X|}Weq4ZUluc0%{t>Sz3B6I z5fc|&OWlL3+_6YS&Oh#;(gX7;#E-+G?&x~BFPtEbXGGWUZye6*H)fotN;Mg(miTc< zu-R7=6}i_<0rOB%hT>dJ03PIC?mbCoF$IAa?n|T)KyzEDiG9^}^Dl-JR(CnU>ThG-k8{@``UN;H-= z0)Z!d63|+K1k&AN>{NBxRvsfq5>}O8*QtnWD)n{c4!;kiFLpYG0farVOle7hRCB)BVY?krhljyap1^@-;V*WY$JISkE-<}WGBgl)DAr=m{yIp8k z^nmppvxiXJTC&_7@oE?m+W5TZqv?+k9a`=hB(%p5>*RIU+N2kE&>k#BW|B$7df^=> zR$00e-W&9{On2q?*yKH(A*~Ac4z-o*jp+|H-1dzHW;VsKaJa-uFz=f^*=R5|mL5s~ zx9H19+EecUPh7*nxVz{n>^HE$QQaR-nltcSGq*(W_9w&(b?kru9}NA>3v9bF0pVxl z<=p%@0K6P)vvN%d5{J}Xjm0pnbW|Ovc0#%vUVAsA;tOx{x*m47y}I(As|2IA&Ij>n zN1P@X_>gnfVP7M`%n@I$ouso>n))jX>K3d|y1$K-LBM#K= zDj9-iQ^vGkHm0P2`yu^B2rY6=ecZi~WGR=1E|m9{oEzTu_jSlnTnHdqiB4FPqn5ST z>uto*nmb(VmOSjvxEVtHld*)%#>^wBOS?VDN%5=`xDIHtr8SSNK_XHH*yX648h}V0 zn4R_lh!h6{9P~)#43=t&gpGiJ5O(Z=w=f)n%S7I&(iLduHg-@%9}IX|LwDuDK}^d! z_3smUwb+jUuDfI)5JC>M+h$jv8ht3dDvWH<4N@ehJmG^DMhWq|^Xyn&Qh3#_&)c#q z;#F*mL=+*%X@v#7)Gt`B69hSLMhWbzVurO4gQl>B5l?*^pi*eRf1&dm)=9v{Mpw_^||{7F1CM_PXNocE}L`oCQ#P4Nx@{ zD`0C0zhdCoOjRO49P)uz0?f)ys1f@1!2{uR8C%W-HxupYZ^8C zFo8zkgt@magnkF_#^;iNB&GIt$Vz^p&IiuuAP9vJPWzq$1|>g$hua59*=BEor_yNG zt(-v?rf><-C!+N=;uxz9J7L@f_GxS6S5Jjwkn zsMb06qfMeo*i6R8>Njrlm!HL4l~84 z+D3p=0a|mn2)`r`b}Nob1NVXfKa*>|lP~!Apt5#GW8^@v?5W%XhOT8+ZV=>B7}gE0 z{L%@lfOK}qtj=Q_coa^EMyR#j!IQ(~l+TX#GE_KWsHw7m+}K0%soMHbTSL&qF?@*j zHtb0s9hbY8A>F|?IWfe!jC=?@CDC|XvN9uyO$Rr84BhTa?s}vim1Jy`)-8V?L^yFU zc*y5n1QW1HWaI8tAfpFtY~Z6*0KKhg7a@bL8Ilr`j__ z=JCSSm4`A&Zw`#$@B#!G)#J{%Tj*He+J+qBSE^?9YR_y>Jt7Bhh$*V)h@kD9xmlVe zU~Js&Ag2#%x$d%hu`F#Es9!5PnLC6yd|Xhl0XK#Szgt4OcUte{yzi;Qtmu%x6S+X@x#^w1JHSS)qu)$nyG4eS@>EMRJ8GS()BY@AUX*kF8vsxU52EsCb;OX6 zOv5=U%mhW@WKFq2ccLKK(`8uNfR6aMaI?=@8%9N~6%hvsUeH`~$$_N+7}*y>h#~wR zDRIqFkZuikY?m%jZ-P0F$093(}y(lYCU3b{{%3s$mt0Y zuWIHq5cfCWXI+^P+?~s(L&OSk8l!u?%rdOw-50mBPO-cvDw~A?5-Ihk=SS44T{?C> zALRj#Y07r`anZP9CnzI_e9K;hz=1-o6#y(Qk>Tq)XE!%TeVsR0g#+4#+1}k?GQQ53 z_X;*Q&Xw&rFKPkb#+}s*V+c|n0Yq|ROJZX=4`wHh`N112{ouOMi8OnO7VbuL5bE1# zbIPJg!Ur%OUPmXuheMCai?t|J)kVE=y-X-%lF_73Ubq}gtC|tHE#T4^02L|zw$LE2 zCIjYkn=R;gla)Nz)dw!4a>;3Z#F;f>b9k!HfF%pgMaS$BB4o=v>Nb&sJsWc2ZvY|G zWqB&!Ros1<`|EMB`8vGfh`E6Sj2R60s5{o6GAAM)>3Lv+gjWsc3qkphjY^9h!@K zw#mJ03N!~HG^RiN*`whH%hivgZk@MdR37=V2X>~|&yCG!MT6j;&PxYey^0z9)S=z#g0B6?oN>1po5o0@;k4OZK4oduPYdJEd!G%x8!a86KF1qRk60a^$qqn_N z<*J-XaNQQrnK4z&&(*|S%#rf@Z4{L97LXi=&4tVMalCL<76)cw&`jTq`buu9Q#_;3 zg#)5M#Gmx7THxzo_oz~>ff`KLK8s`L>c-fD<6@IJ@Ik{Zx24?aoTIjzcx*FSnsZzr z7larb&SYMqxRjt{#T!rjT0pvNdt~TJ4Kf2AUI_|fn*o&DS>2kDww#n}K*cz525DCm zJ)#8Wm3!1Jk;EzvIric)n%dC~1Qn9;r0Tt16HGb-N&1{lEa(fh=phjm0ewORk4hpe zy2PU7X-S-bMx(a=o6v28^D2tpiU{TrycT{dXaYw=N)Ipn`U3$(hvS}~icJ!(qklXD zN<6}XbL0^OV1J`#Kb`~{HBmHnVK)J6VN5@sD`GkwV#RmW&kudF)rXgCP(eBfkBZOL z#ZgJn(0H%t=N=yd&yL@=he+L0wmRd6BPEN(J*0nsW zEQ>5A#lDx8up1ofPL}Q$Dg^t$$O`0;SJ2J8F#$pseSIsJ7JlM1Ro6MUc5A7 z0K261!#|32$i)))uQP-MJK!3ht6~I;46W&|85m=xxC-1;0xN+?A_=!)grh_VYwy+e z$T;_uPM^i${kB}ay_^`C-_hIhkZgXE3!*0+m4gAN7oi1CdxC1Bz0|RdHBQaAHA){)%&xOJbo?!I%AiT347ze-5 zL~4L80pvdxiJ0SAwS1No#{7VS_a{1T|a=;4H;E1JobQvNU=Z&5Q>O9AQ4 z+!!>fsHDDl0ZE~A5bb}Bt*A27wEhke**tRG;;g_fo+CDg2lXI=cg27nj~p?9?FQx0 z0|%@SE+D9Kwl{9Ihypq1f{HMuvZeYnQyrBwC!kL#2;hL#w%9cRU7SkdSs&F<6|jlN z;@u*)jaER6IBN1qUyejta$MkM0y%l zo};eCwNezq8O7JWddO`eu)N)HF5XvwtLx!0ZPS2&tPvDI?Df{K#GrGsjpV%hK*gAh zEJq2xB?$s=HkOD{0x5Pg$PJka5JY^JvZb-r-Qn*Dd?283HrzcbYFy*zea#hWICZ#3 zw;{8+3m}%q^<1fR8x?^bH#}p>xI6Q{6QaH0QNCLyg}A1qiTF+AmxIWkE9T$jAiOrY3yF0|fbmnl$aEFs=Hej@*X6mY-`rSs3NQ z;y`lG4#y@6SKsekVHxY%#p0KkAvI9ON<0zsQ?#1q^K)_|Uc5H8o$KataE0XOk%3p; zf*xugJjRA(<2iC$Ahb3E(%MJm`aL!P*|F&V$hk*pOi{({-Y9Te@bO=nOSb`RK;#u?MjY0&s^nz{jYB6_B=Kx_MYa(t5 zDfbs=o8gKDGCc?wO7T0T17WJy#TT=F^%cR}@EHt_B;Sf4uBD?$s5uk)XeM=1rY|g~ zYqHK$Bg1gLJ`WrMY^b@NaHbBnKkO_T3CKX+x?m#wHre#%-Icw)BF$O74m;37)CvOU z+q)At=CYnN?Q6AMNLkbL@n4bkgU1q@y_wN|D7MlTBU%Nr3?wzXk)n>|49wjn7 z6h%&MnilDs$3HGfP@wqbn+@7q48%KPC8`*MhCY#Z0fG)OG*8(h6Z54O>!`JPPp!Ww zJ{v?SC^buU^H_ReTF>v)>t#ZQ(e>0t(#S&`p1XcHzD*XshUF@nt0Zwn%+Wpe0DVp3 zybD%u87E8~fuflL;c1R`z*mMAuB>$$LUhFT!q;WhG)(aPR)GW=Ip)&7I@)9A%U>6K6FGy{1>(5P{aY7pkUM8FO*)ILZKXfdptg4|+z~c$CBy zy(U!B^}yT`2nQnpdCuQ_K;4pu*x{FyJ!C*{e$HrVEy`j8>;;o%mWRoZ&u1gop~vZk z}yJv|6%t z9o09oDu;l`IgLjpV{rrA(y0>Hnz{I$1UUF`AHO*2=$wF;ZSAl2U@^D#<+cX@YF}_Boqj}ulFs1El7j0(kGSv& zCGc7;RU9^=V~=WM!fT^|9EY4d#(9@by)MWMZqiM6_m-=PKm-9$X6KeHV7v? zx&&?A(arXkMnKm)7dz-m{z5&~a)ba>hqk+OgdZzefN*%y6*f~kMw@SS zpc$jh2J5(nl%%yNO6Mgc5Woiq=)AKi)?-x7e_zQbr!w$!bOdn;4kD*q>A-1AL-|SH z%d`YKnzyX^S|uildZcU2ZPo#_H=g}5SSoF~W&tZAv#XI4G;gu*CwV%UFI z)Xeyy3G?7GA5vkxe-GZmbJy8m{_h45FUK=txH|}kXalr-rD<^GSPQ0ij5r5DP_>sW zu*DQWmBZZ!2y6(e=Dh26O^sp_-``;|UMOm5f0q;yEro8~-!EK}M_$`!mBHFOWYKd^ z5rIX>z&GDi5xB!OH0`5$DPDYy6u%YBu(V3R;%}2mu;GCYJ$Hn~hHF)l-@;YcK?7WR z*duHan>d%ZnkH%!S_!>XjD;zZfzel8Gpr)U3_i6*4x+D;cP*a{n68eazd86Jr*OcF z+8~7O*FAU07>R1cBt*0pQ;yWCUFOxs29a#J7ZI5$E3tka%*Is>hO7B{$52tcRZnkh zB0GiKkZ?GE(oYQlfN}{o^Rq2`GG4U0&bx`lbz#x)#3-#g0I%0>%pk$8iV)`q zdmZ6W=)(LPuY}^GXxUXI3&^=|~v9O+2g^hsINcK-}K_s>k1~~o(%O=H+zP(F8aa`FtV=DFnn7~Y z<NK$q!9oskE0D^y>M{B!xQKJ@Dw&X?e!+yQ+hH0!f8C z7nh0*hp`WxIq>diq{01W(+~n9De>87ItpluOaF{vmu2Dz#UoLp_dHyHedysAC<%0e z&;Dozy2jS=-W?huNtCJ{{6Z%>!o%phZ9>Luy?l3NkV7co!|pSGew+x(61-lLB+KG4 z^sT14tOUQDexj$YYiKj}OFerM;7SDf)q_{T@Fcny9++x`(4mL1=u14%*x+EV99FNd zSbVHk#mNU6qu+Z>l;lxC=9oox$YankXXUFSY}Ebm+%RZE5~eEN+tv5#l*jwrvmdnz zf+Ztagosm`-23J7W{j>DBi|fg-Q+a4`4AMWn3nI8meO;ELBz3$r&59#D7an+Y%K^{ z08R zGVSNA93n%m>JrZjsx0C!^uv!Md3mo6xE%RL46sXF(}R~?#5#0v>{M_7e8E^MB&r~x?q{u zQU$skw*ex752H1qa~@Yr*k|RiE@2Ctak!l~&D0u30U8f}kVvS=I^n!{c#9i8DG!Td z4o9rV@1q!y=K4rHe)L3=ja^~JyPMrk*sAvab@hdXD=(+#=ak&opktDV9GETEr?2_Z zSbGs|TA|sAmu^G@)v`WZODTc_5yyE0 zTyuDKu;Z;iYD5uv1w6CB0p81kxZVm~02nQvA0w+mS)Pc{T{N%_A|^(In9ql`t&4xk zLi-^=Kypbdw;FggfZvHH*|=O6_1Tg>A0)aDE=+U^0?pX($}@him2{X~8OtdEWu>&N zF$Pu!IU&9&CU&K_pYuJqQ32hz;$to(0MM>cJDm$R6x<9_R|Ik(Sw;fjHG9mR1eqcC z(cHh`L>kaX9m~-mwAtyV%LFp;5X^AU7PbwYt1r*I^)VfY{rk>GO%X8&JJ0$#fOQPH z#^13(8&=+S&g;celg*0#cc~~bChGQEWeCNkF=M!?6&}g$^^;Veq<$E$bR0bcJ>p8 zG2e%7B;6oLV*QNgoLL22Kt7k~0@a9y&=V_TK1+PsxFzczUU81LZ=ytSFF&2!^0DvGO}h{pMK6(EDsL8Pz3)-UpN7UL&B;<7UV<6RLFJiW?jd z;8H|6DD_u1*G}MHwXZD90Pj8bq$TmF5cRxxzhA9^Gv_5a!$DgSd|W#bK`sdryp_c+ z2AQ(Omo5&m_)1zjsu+-I6h8>xx&ue03W%-4DsRbZIyas5gbti4iQ-3TU_K%@r+rjK zNlS5A>#(YMJg6&v&N~%ChwgOgz|Tp4V+?k1)KJc_D&u^7G%C-y{xmh(ypzp?vDgoLrh%JmBibRXD8$3(T8o_*{K+)IDoPgw2;Ms7w zPrbS~sQ3ARWK|CZ!Tv9~ZF;ao+xLK2JMy|sxE!*hYMzDb_jQP#K248NYJvJJ#`Y{e&*Cqp)@70X4*7AJ*>=AYNGS zCUHxk1SWmz$qc*KSD%mCsXQ3$ocd;%QHWL?>Puc-$Z^LA;+oDso~AC0xZ9byOa~L{ z-*}*^l+lLHa`Swd?;XL*G&4)ImMpfva++ojQ|o3_{1}RYJXFdt+?lc5J7ISvIBs~ zNH9S3m@rogmMCSPn5|t@c$ND+98o9i|f=Z$dJ7^4ofvVSLDe9=#*9%Z`?V^XW5TpwM*mt z$c0uA;LyOY4xoNH&kVzx*4K;le)1V(3V@dV(mhwL#W zERx>g(8WU*M(Lt3^+ZBwwgE`ex13ZFJs9@+O~(>)qD5_qQJj;zpsklX`XM09hmuny zJZB7w;VfDd&hDXz{20r18>6fC3u*9;2=coAQ~3!{?nKLFkv(-8y-426^2MTwW$wUj z$zVsjLFa9Rye8q+bKC(IJSYHB{n=LmI~txGKT9GA;4)#*UuRihPVo4-`vr-yhX$^n z4#hA~!>Ku&&BHo68uGSK#x(LSoUz)?IXai{_@@ibqg)j46MNr0q^q`VlxepeCY&74 z#;bd?jre?Pa3p5rT#k1cNExB5_-iB#3kOLptT+Rd@LH8Gg}KBv0KwFBMKmtv8wTDk z`k_ZE1JaStieO<#Ua)mv0bkZYgTFu46YWZt(EDW%qCtG2{l1)7nc`>$)RQ-DK%v-N z{&>4?(HA@D#YH8KVU&;G8&#s&Ey&@p4_4UzN;vqYfz}bKB(3kmP_y-*ug1xmw4@$C z;A^f=q*T%>*V7{5NK<9tgoH{!nAEO@k5&cH(36w~WF896 z6JJ;+Tf_E9G7$3M4YGF%_V;Kz1c$E@ew+)Y)kO7@?SNGz-__+FGIX_kmc|=Z#82e2 z*ux~Cp(Fej2XtmlM1$`RQtu@ei2bi^jV9y}82$^?62qqe?7V!+cRIEmK8vGLH##F- zM7f7$eE05V13PQaTN2(t5DJlDm2#^rL#>C5Y}*Glwk+Ikx~*x4MutD$)&+k0x?fsg zFojg=kJFM!=DN)=y*E^pCM{(4#4@pJGR}V|PX4?_Aq8+@r%N#+9K-`(OD0+0`uvy1 zWdsch6wkF$Vnb!ryqi?R9}=9{Ezjuc5@VK*wM?YR zh5|$MycD+!DhMdL+hI8@L*m>0V26d z=iRLpwv%Q$ECvRhMJy-ZoB;z9Xc`1rR^QZCHsohS2L;LfQqO@RH_yH;sa96>{DiMb zEO=tsqnmfqZ5<&pE;myRyH_*gd^C7ZoPfHYc%#OEhqvuHJ1|lL9c$kebr`1>rSZK= zxuGMhERQ@U0oKRKh&LBMUt@cidb9)*TwX>1pXCVyvZ`@*-K!nBlx;T-Oe5*#U_sP{ zRb*57U|!A!yH;W1vXl!1)bOLLr^iy?77POqE<&ekB}KC2S*vMHnxtOu`>`@3PMY?c zK5hsg`{82~ND@DA(fZyUH+G^$Lr%=(Ud+Yiap457quac{ZdHpoBp}C!C0)+?EI@hG z7;TO;6ySYxb%%}ZBit8jg4$FpsQX+h1YN3F%M;mjC1j~0+PNthj{f@t@&ijw&~*=M z17$iDZJaqqTFGtwkHico7KD!Led4{U__$rq6Z&t<#&qZF=k(184ioNXQ;7qBsN$Ze zIxPl%7N*;>p{0`OawEHSu4;N`q;ju9fml9i3?{mvB(mGmK3>-9u={X{KA&eR&@+QR zHR#NAacLx2Xps<|KaC|}!wd?0>vXFM3NGHKeYy~5BsbxSVQL^?cg?miPJ zKRu%GJ&|i`YpVsb=MSglE-NOj$5m9Dj4}H8H%BBS)KwZd)y>{pC;R$%oE*wXFr56- z#SY?5){C=V=7_B76S^+vZ;zU3=7W8BO)+pSzHnyY;tyN$dr@Fn-WX(YTIdE|v2QPa zI$=+n0*>5E+AwE0Ozd$zAd|LXW?1)H!T40bQRkCeZ$z)8CdMb zGQe6)$+KU6JD%Pu!oD(a>&7Wh-#b6T2ALDpMN6@8rqJd-Dr!@AV<+sQ0|1mTn@r!6 zwT3|Rhn-_ah0ri4^7TZ{La_}YK3tBLb(ETnEeK--AztLK1rgYRLb|#lB!(IY5cDoO z76h==0^engcjSnTD4%yxd8ju->4lLTsQe&yc(5dxrbx}os(D~3ft9Ikk*aM|w z9)1bf<9zWw69)=3Ij4_h+LM@C3kXwnTH&YNoz^URPc0iEvyPfUnP#mmUKYr)|a$dOvyX!Zj^dM_Vlk5Yva z&gW9%wWmwOD@jhoz2LNQ)eyM3gqOa@wcr>81%;^hvY?XbkZb))*a#?^MJe8y5=I3? z%JZQuoIzMEc(`X(%4@cz`z^ut*q_liWks*n>HS+1?7>zx2UAmQI0MNd-pVNf z7^%kSZAkeGfyW5Tugm~J)|1Br;NX_+L5dLzuvHWXm^u=G9)>rTg%7I?%OxOT_k1y? zSbj=OKC-9Ogv?CiK?6`#pAAt zjBsZkVm|uz0pvs3_eaIq6%r9mx0B4g;(_AtS*^J_C~RXmkr4Lha0?a6Nv=MNA~md@ zfV?{a*t#NAW2yHC_zbFcJmv~k--h&B$^@48ExJHt;2Z1u*b>cdpql3c4P9hLm+gGh zOeVIJw&bn0M8k6V({^Rc`6v-^{(U0V;^~UCm?N|cg%+b;y1Aa<2btwS^_<9 z7Xhy$7EOf8)0^sKaZC*uXepScY=jiCH-yWrZeSIvBo1uSkl1<(}@ zd@Tqt2X@;d&_iira9%nf(Ml11AH2c?8-gU4_xiXHLL{^8v)6UDeOed}T5DA#lH$e< zKTVRnXunS8_iO>SX~bC^auOLD`Zx!pNSR{`rnj)*y-^VGbJh^D!f-e;Jhtm6*i5m- zUq>}tBm;?k)$ndAu%-8=5v0oe2#X(D{Vd~~lzmVTL7y>NxV`g-i8B<|8WfX~ITVYY z+z-^6c9?9%lUQX0oHF#?a+_Z(WYB+&B@Do(+Tm_v$eo+^UR3VnM~TUj_hAkg%YBdM zT@p-C0#Cu;sd{#^yei(hMGe438G(!LfHX@Dar{w3O}!61oKGG6@T@$F_tX_VBQHc< zYtUXXa>3jM54c)r_Kh-W0=@Gk0|j7<&AeU@2cTTfD3|-8I%dS9=xs%!E-!*4&&#Po zQ$|$FjdlI}Mm(u~xhAes29NZ~TEsqj_baRLl9~d8C8qZny+4KdlV3>OsQbt$?J}x|?c`b*uR@8JK%(EeI6WObGT~ z^?`C%JG<{Q@O%=9bbwxtQiIuu5+dSkV0ce>d!jIOW#T1;>qWS2q3~tmz-&4?2^3jg z`3M3RnQMnbpFxOvLDur;qyi>($v}QgBTxlZG{xs9gDIQGpOeT8<~btrQ@xhGc7EzdC$#Od_m1Z z@W2-+a{>`pF3jQu(do?V*GFdvmL@>=?~;O1TFF~dQ^#(-0Dj|jVO`)=lfVleusg7J z9B;?Vj9V76PmEPS_5wq>@0)T-n-c)F{+WFxWnhBgTVXD&KrJNw`_`F>idW{mX;~$O ziPe9HTE}?o!oFKqM|J0{jJ2Xzsv;Sgo}YmxdTbE7doB8>hUe|Cp~9pCj85;yLy-=- z;rZu=Cj=;R;NBR_95)@S{op#492-isewhpMmk3SArNz{0{I2Fc+SUXO&4>(7HYLqX zPBDDl>6jfbAe1<3iyA7gx2uzeAUlOT&3dPYArQQl){jh8v?;{Xx#6~1<(e<;8PRgO zLO6c@^MFpI1(CD6<4)Z>RMh_Z*;_^ebj=OtzM(=%#a}@cj4j}!zWYtIZG+f|d*%>3 zO!ScW#YUwN(!0h#jKGqV@*3cr4A?fXsJc3=m#W$6P|kbf#IBk?`eLdFFmyO{KmHSn z>M>j6$9_(X(@1JBevzh0br}QJC5}hgGwhn$W7tVSAI^7rjjGdOh1K6N&I}CQU-3{) z;BpgjD%b!=1kThRr2spc?0DCz8~Q}$y*%3h1`dpRj;>c5ghS#F=7H0vj;7lWzF5Hl zV$KG>cLqMch1dge*df>+LRMAhrGl!&L@GNl_3~J)&WZCLb#@hS3j1rT3_7bsyK83W z=nBY5a6TMcrjBg7AEVIXmc?fHya2(mi=$PylrdJ@qTO~knnF1R)@E-)(8035CT^j=D@c`&9n(AckiJeE)Mv zbAu8i;3Gq+*wzWKbYABZUXq#MzbpI%?nvTzv0+&ogr|y zU5`i%D;;czz{!AAtaD6}XdrJbp?5ofKtT%j_}sA&tGJ;8|IPj!7Q5i#!a;-6F5Yy1 zfsjD+JH~#lS>va*k({TCAlg;KylY1jg9vIg2(NK;Y%N zhh06~Vjr-Fh*Hr`Rl4yOVv#5)`~kR^QwcT+KFWvd5}6V}IeWiNA3gyzl&^@ao2x+^ z!8d887zq0|ofYv*>!qZ_-=-J3a8NFu7pkh6Hm>$>K(D-$n7O+}zj!f~(0-}9gKJtb z!};n+3T#vAU5$~5@UeFHeLG%KmXR)8GYE5KEy~qjQ6~-jHYuI=#b3%0G1`M=#AeK< zn0jx?B3O=2)MrU5TcSM7{Is(+C9Dg_-Yl<3qixmA?$xkKqeQz3b-)lCEO^Dhac}yFd(zS<;13sebr!n*JTx8lNDBVR0`k8X$Az4akGGCO~nDPja^Ud6Y6YHp3+l{d2>LXEVw#UW!&mAxm*uK#SFgzo(Rae zLk9==OA{BWYL+&KN`JtfRnB4P4!b2vxncr|Qda4*Rfi2{=UA;GLBz zZjJ!Z^+Us2Vw@FVd1PKv(oh2iFN9(>34p@a+-Y0q>A54k zwib~gAIyhV*8|S<#Di-v%EstETmZ*7cW3Uzdm&UxI89#Jz!!nR?FV<339!_3mf_6q z-?p0yswYl0vK7E!^|ntWP>Ih5p6bJIMzh81t*q4t0JFXR?gSR9Wdy+G%sY1y;+tb0 zYL`!A?q(JNoin2Nx8m7lV&dL;X|N%!9B}hm-*K*F^Fcy>O$O0Eo{gsR>kQH-&fJ-3wTh-4kINlf1#H;9(kHG{Xl!Pdr(>;5EZ6~ zK~NGsN4E<6z?E=!--HP?P_!O4LIw~B?qa-k!b;}L8r@Gb6`{>Q_+6Bx3HoZR?DKMl z4HZ3~cgB=vX3Zp?GdT8=RtU&B$Cybf82IqZ*T~mpGI%4T&=0U(>K`+SCUL|d`lpW{ zL*^QMXZ1ZGf`QEHw8~2`qLBsv?%8o7C&bTpIftikTzmZ&L#+$Rz{R7=5Ovyct?=4a zX+fi|!UO+Au=c|o`tg*RGBFB-BioSDNv3gblm~n;anoGfB}kJ@Hx8;AoIGG_5 zQYs2eNDaH~(f#rp10nFxZoV&P7XWGr#D<6M9a+Dz4S(&@b!&=(wV>JC2U`PtSHxXm zNdnqJfOaz#b@n}LW9Jm?#(3s!5A_^#Ov=<-j@e|UR_VM^kbR^(Xn#jXNKrEudsmmd z_XZ{DkM@|n)nIygQ3TJHB8Vb>nnj^SdWGHHLIw~?eOUggowgjq;^wh0j4%7@VB8&~ z`O*Ll!hf~`!i zN`Ou77}}Maqg1tIWzcHCj(i5S^g_<=1&4Mfg)yS z_FXEmwt)A{K6W1jU9kAF6^Rs?6}ltqG1!ruLi4^rc4*xwM2BVJ^;Wi~d`=t*l#K7J zADIWWBMHXsFIQ108ONP^(V&0{Ve>@OtsjH1k=Hw6IESpbc=tyf6L|6V-u^kA;F)&K z^0QbHhLkCTKf+a=H4m*kZ^N1a*}3C$F^iRi0g*b?aE=jGg)7d5Tsl++N#k+_M(j%D zg7_i~2$TjO%>9!TQcFPg^WEB9ep7WUhh+)Esk_2_H|gsNZA{fYZ`8;eF`?s$YowEl zX}4GNO;+@t}VlLbn{)%_Sh)b0k8V(GEaORz<=M~0@ zaY8o0X-NnbsSk8kExQ_K5}d;tP@~bI;rrWG<-fq^uj|^dAVSm}^NZq=F)zl$Kk#Otb%Y&g&nyUWn^gV_slyo zC7;4l-yGp?Q`gYMlaZlHSo-8R@rf2%gWccnPJ6)6NIT-PMFOIfRMcD)I!q^}GI{%@8@mqm2c%`tA_}D?}5J?DPXSJIhs-c76pJ|KQ09t*X=tj}>>00nL zl^VE0TCR7Zoe{i2A$trwTohv{)Z+;=P|H#5b4yIgZ6462cPlQM6`E%Fn~`qok(v5E zn&`H(#&$O^_6?1y$X_}L386W*dR_q=7MQx;r*en9ou=XV4Q(|KB8HmVfYk;uY%}^) zBp!<^Rvo{ycB=xQOXqG&QoMm+#NT%x#C%)u&T9s!k`1Tgxiw^(T_T8H*1AqW%}e5E zQ7GkDrWv^|q9xM`OU(mUKPqr^0pi3n5X{w$aNc-jQx`I7!HE;cSS<}$crRFr3!9e# z9(za$=2NEiQ5b&1rfU`6N#lol8~661B`+2d@kU&gMS>)w8z;YwgyKD^A@gAsMw6#& zL02}68N%hVJmdqj8`5-lFJ%F80LOL7Z&f}NUS0vY@C+8BZr$UPLG^}b#$a4I2XEFE zsLqdPy6#RgRlnP&VI>vA&s}BA*-3w<{`1jH=L6!0H-?-|f}la+c0OiWIGz~qC7o6{ zgcx-hIhul&(oqVjfcMsts3@Z#abiw3QxX%hZ%pA? zc;wQ>iS3S4l^&-bY%&H0!RN+xlWuhIutjuO4J@EM90>5$u1`DO9Y5|0U}cIEK!D4( zP}RFL(BQIxyd3PL1W)@SDdSMl`nfw%k{g_p->YM!1c6QR19_arVVPO|62grdw`&o1 zzi5;6P<+0w>Vjjgt)LsHoI<(f=sGegp59oKhYydo`S{7^9hXh;c2l*+OLJTZJ6WNg zF`EYh-SzD0us*SvBVnA2GlY?y*w>LVb8$clLb%bD;&unlp<`8MLdRGN^0^Yc(zn3$ z#6{1a0o!iJL@9LPc*XqBy#Ndj6tZ|~nE?u=z!6ScLBvk#Bh!0_njUdnYCV{QDwu2$ z-+8y>s2pP&|2lga3-sIKn$0PpD1bOP-;M_avvMb&QP-)AdpKk0+JsOsTZKyi-u&Et zj{E0*Ur8>={8PKe*B^ofAB_Ziz^G#Zq&8d0E4M}oKqC%H_f5PZDr9lFuNg2;;sT=Y zhH3YtjolnqH%Qp0BF$qZKqs;YHiE@!BL?QryLX17Oyj1b|Lgp*pZ)2u)TnqC( z7=r0lHEQUKeE?}=ERf*9LkJ@0RGEhGUgE0Ooy4PYWvEP3~|jk2`| zSC92^y-i7t;Ky+v0JpaBIeoV?NcsZt?R!^eo|uY>p9>?PxvT;0-O>&6aZ+S{U~j+bZlK5(i%sp z%GT968*AqZZSWn`eZoT&sOd!$TyJe@_^1IU*sSa}zqJ~P(@{d7(u4yx>GrhHTOv0J8Hd4fEwz#b!K%oBKpqisVUO)yYoyY(o8&27#FH^yF4r{j>!OZ@K-GV5sp!R%1E^yc>(YM4Np$&`0y*pCE z8)lHdzjb?zNv{#Um(VEC#!UCThe^4fpMTE{Ud0@|S^RrBgvx1c@3>+kIyWGe{~8U* zgZTj0LyJk8YbAA-M)cBlLNtFxg(t!jGh9$rC~m{`i*Xysyfm8`u3M1y;NinG>s_=l zXpW)eIok-`Qb2pbWY3WesW8rQj3t7*77I^h<7Etf6mars%0BF2> zap@&)*8N!GPfejk0q131d(r6`?<%2%2U?FFe+6>ghsyi;SRA!#%nPb# zm5>d(3UmF>Y$6b2q>z6swt|K^!SO~!9^vCEv|mQ3YLVOYz2-A+HjNCWTQ26Et_48( zB8SQiE81g6yw;44P{Zt;NC_%bo@E~P#e!*B#KZRnkbCb~P5ST$9X>{xpD!#&XBtyg zc3;V*8!J+HTo;J1vrdfzxBWah8l1uYs$fiE?Q3*ykf099*~)IgkEme7uq}t-x8y?f z>ac6$ysiXVK)l-Vo zebi!e$8)t7A$+ZscrPBPS&G)A>jHQZpq1kKuDvc|h*@PlG>Pgt4DaZm(8{66C9>X{ zeTyO{O7giqCcVVGT<3*dmNWg*eQr>Q-VUa{!)CbUn}Jn!R=WiwVTR4OWf9@#8G!12 zkBcIB9WcFdgI|}e77{m>i)Jx~!u{VjZ5{!&p~v;Xqa3?<;le8zAk#Q3J1~Y6Z$lW6 zuFK=Mf+ZsKup%W4u&kK8=`tLM1~;Jpi~`U%MicT(yr`@j*$#g+;G>6K54J=8C~Cii;^k3;{@VS$D&nmx>We+kyl9euj^rbK~s(n2%UF~ zZb9fV*nzhLCqyp`pSKPu#6b?@#}1yPlb83Vv=O9@Zr7gLBgN#pR^_z1JAk-r8JstS zp;rpA)BCRVS}@`LyY3E*4Fdv491jll4LSjNvDZ<_1H{tzcJ5hwNM(6!-qYLWZ^}Vu z-Qoan*uN0|#BpuFTnoB8ku&C3w1Lhsce^V3OGc38uhE$E@&Z|2a}I&>Pl zpQzPg&)U?(0(=;HjW&D?WK0nmfL2@vt}cl(Nv?Bsrg$Z4`28D+0R*O6=e`&OgW_hM zJ*cy^B^r(7w_<6FbOLH$Rsdm!2T?ZON>zON$@hca3jf4#8oDAiSoQ=K2+mgGsL`pi zz+L1}SVoP8y$PL|(lik+zMJ4aq6<~O3@reonfdkZ+!xU|bkNtuF|>j7Gslft5Iam_ zC3td_FZRnY-j7}AJmYNKeYf#Ys?gWutb9XVWiCWM8hksmoI-Qf=oCp*y#D(#~HWeKuXjd1(I zgW@@r=5QL+MV(WJgB}tjSfn~$wyliHAJ?^?59H0S~ zwh794clm;)6N^4uoI-F6Zs)8uymErqet&(jL020#<+N}K{)kZ4PAi-FswdRovm2U> zFZek5?E+dsHv=EvU7-Luf$;KsKVeM0TqQhLGX)kC@r}1Sqkb-Td45w9jsR`R>j~RR zIF(im-%h5Jhw3@?dMhzGo=!T>Hzt9Kj@0*hDy)a@oDskKK~RbwGH^;em4*s+VA7zNnkSLxvgXmpK4wM4k-4NoAK~?d9;8K5$4-ml*y?m{oy= z^YyrnIYiuymEQJ|%n_yngum|9-Pm1l@zfg@(kx%VKb=)naRrs&tfM}f0c5anSRic{ zDUO!kIwLi}j2jWY>H(C+Ekg0Hf@U@{E&QH~;{3oyf!6ch-63(Sd3S zP&k)8FY7~&q{MKD=W5Z@N-z9 zxDwhy4onZX!i1Rni1B-#5c;mm2UYNUI*7v3IV8sxh9v%RY-O*9z) z+0{eP0)xU!9!KtoePaDm`^!Xdgx9=Q^0KBQyrr1?IBwF!6GauDazViFx^mbyfH?83}j ze=NQM>CPrwAQY3xD6REr~&@K5rPCSgrxA=bD)KqA4)2 zYNOdJ;wbf2EEPBu{6!vCYX(=VMA$9NETU}tecBOOxHG-9_o^TnC{1E|uo~KgI6n7# zU%2DoW>P+T>~b2b_~EIdJa{ojpd1xXP%yv?qqh#t9of7lf0jjWb`Kd?r&VC&p+O$x zU0XJXOJ-2`sU>27x}OX`6_B(Noy+@5T|g6i#>3sgD0^d=B^Ny_?2!BTXmr+505k+C zW&GXxLvc#N^?BJ^EKJ^B|9!b#^r*(;Zp;%)$x}#pA3Tk4V;s1ZYhP1<2hrCe6X1HM zbK-w56s(C>cmDHDMkNW9$#Lb3NLk)Mjy!>-)dZ+?%oEyn5G3CEgp9_cQORfS@fndm!#p4rGh+_X5p0sZA2k7g;TG z>T>(MOBj?YC@eqjh>dHIy6thgNNze*R9x@U@CfMz?)_FoA^HdH~O96vN1hujiaB zF3eh88h>+?0E)su=z~&T^?)O>bXS@ItSn#!JU3X|PzFbj`~JAZZg5NfuHb>m(agxx zLZ-lsQv-0fjX)CjJ7cw zT=qIEWNk#vWs5(oT;J+GH%!h0c97h6soTbD3dmF;#6Xvjs>c!{W6r)ApV#c@!rv#| zfpe^&y5>Nh*N7x)MV;?oNi$=ta)+*YETV)`1?Bm4;#epdvr&F8kHxmuqSQCda6c^3 zsQK&x(KZm6Oc&;5O8&Az_hqvjWm>2d-kgJCM#*5|lM&}mWb|GimcCgc!Y7lM9KCNivQ*~Hy$WElD(qMU21lOzSP+P9J;H7{l4p6meUjOeZR@uXfg z7imqec*D!aO$nG|nyk5XIg)(y&NGK7gWoSjENh=2_juxMgi(|$7N4u!Af_Y9_*-eu zNT!c2E?gIe)S!v$z)g&{LGVPct0%RpRV?yZrVK;1cR-i57FN;4d-7Z{)0P*KLgyu8 zfav;_;qSl}*OR$cHZo+Wb7U?)pxal)6tUL{QGqC^auNQd+eb9>$mUTC2m-CSEu1#w zgYON1=I2J;_Dx1OeXg@3)Z--B-(^NM%*rdgRhiut)+@uzzC_(4r8VN!`x^=&o%ZNRaTygCqjCD2T@JC2H88k)4~)SGlLGey9-um6sRc&_hh{9n_hmVLF9Ga~Z}&C2 zr9uPZA*YB}bdhX-+Icn07ejVh+tcMJryt){fZS)TM0{T+AXZ znKm&K;k^NhVz913UKi8saF46Rd40qVfb#0SR~LBZ)GD8g*r*_fio<1InK77Dli-Byjk_Xx6 z;r)q8ps;($muIBtrqzS$$aST~_Pr`96|->+)ZHG8#bC5hIBHF=P!Oyh=ftZap=$y5 zj8R^d6es{WUW#H*8fGBwg#m`v7U#f=1UTUU2JNJ=qj4Dk_3{gca)2e_kYKhPsj%qL zJzs?zOL9RwpyA=i;Bz7A@Wjwy-s*&Sn0(^vpVy9wY)ggTEK@Np86)`3KOGHj5&dqd zs<%Tf)8~mcLVNV8?>_genIpn$g>#|)c+MSdeG5-8tHs91^;jf*Weo^EW)X5|74z3y z4-85euw(VQvk68l54cQRMdVS5*{5QD7Rs^1% zzaP~IU@gEstzcYpP=T+@YAB&0ib9FA=J+v#v!=#Pf5e1p2l3ognR6Hhs)5($qq@-Q z4tEQ(9Yzso`KO$VBcmJLAE7tVKv50B0;;y)uC9^Ug0KV_#9PcbH zhn(^2)Y-*dglmex6xsXHanMdJ9c&0t9xCRn^P_gbH=zUs7~KkS#>Aom$){?+n|ew6 zG;DCK%AP9W{7P!0lTg_&($xAyqVP-%Rogfz@GhSVUQ@1u(Vuy}-8$$+O_bw>`JT z8UP&G3kOB39fcqNXvLIiK0PrdR16XsM&~u_OKFrK{Mb0A2$jH4KP>^u0QJP(J&&6+ z1)GCBlLC%!kI9Zdq9OrNU_s)KlEzbGSJj&dD2Sp^Aau(?5-NCfdAQ;UQ*=&T*!c`? zEDN17c;Kwk^#;zQM>_CiR*Q>42V^JoS>?}2B58rEX7?5`n};$5=#C`<2zxJW#NC_^ zsO>Bv_~6hK4^bUzrzB(1U@s-YN1Ls6xhTNARR|0c#v$cl9Z6Oc$)MfMOs!Plg^PP` z^XSpAxb#H0UIsYO!dC-MA!1uA$6q{2adEg(k_S!10>1Y^9L)2`Jx!W#)iOt;3AQqBmviB&K6~#q|To8 zw@1{1JlF3&keV?ByzyaL9w;yOgCDZF%T&Vt6{52sQwFif`vYY{hI2mh(XuXJ8=0 za=+=TIJ+Ih*RuhsK@b=9ZqFzCNUgohTR>SLjZH=j(aMvW(vbBcw&vMcL*2O*LYDje{vD;)dErkNlfYI3@xv+ zZyB$M>9awmPVSp%6f7&b5 z8!9XA{D|KMroB+`%cV1=)tgF)LMA zWpbC_sG~*jFNNbn68i{I3?ljHQwl1nrN+;y;JfUpVDw(7=#{DfE>2w38E8jM_+l$8 z;s;%y|1J4xEO6`e*=yebG$@b{8YslKLjmAzWEd{SVW9N3CT8XY*cx7DYl1?-g`>Nr z4))Qxk*-$_#3x2t^nN!uK_w@co-d{ZhA1a0R7z=Xed%l|(#0HdWVl3EDb*I6^^9XdH>+|JnHBci2UJ@Ix z_GmQai@ZXnma-|79WN8HYZG0&s|AabwuHHRu0+%&;*tSx3skbhmGJpQPtW7G5kCKP z;jkovN_JK|SjN!isn6CTK*5qpF>4zHkOK+lvmPdWMP0A>=MGH`jLpZ7#9)}*RUpAF zHJgH9NI!TgUWu3~q1@xfJ@p9TmK`|l-Qt!4?uV649(ic8?hAsE!5}BqWe0F#Agzt@ zPMjjwHI%gj%AhU!Frn*c1-1qF5Sc!m3PB#E;ytmgQdes2g`$Ec|a z7baktMqMH8y&-0eLP(7ct4aX20}KP-41Lmi9U%5UzzQbV#fIk-5yLlDNavQKW;ob> zhH}G7r0CmV@DvAiPh07*gN#%3;RMJWg;DeB%*uW}G4#?{@sVXJ5|uAdv$L?BO6^S?ek^Tq^Z37a5!p=%ZV zFAz8t!2!(AJxj6x;fLN?LvCB@P=q{_r-<)llfoG_7+1gO5p_-p04lf-dA!xxXmJ6N z?Q{L8QF(cI->V|biQ%^Oxe2xwcrn&EYmpDQ6|O|btg}N9Dw0|Fpy zKhP)s8Eb0o_TDpe!Cxd>@!e}zE;BeH|N41FHMU3S`A%hi!~DHs5a1D|nkG(&`fN;u zr}a%8)gP<~k$-*cF=wJx`@A=XCLfGH58OHekIeJyyd5;?MCKO#RqzBt(gw?G4%ZfG z{|xw_h)7i9Rs;WIv4aI2@!B+|N`5n2e4;KDgZUl-gsK6rH2 za(aM{^A^54=&&(!TqB%N0xCgoeI4#4kXZW9G@+H0;>{<{p+OB`;&3%esM)D56;~)^ z2AyG8UJiN+lK_<2WrG~80HQ*Ca}De=g!@Mw8(#~B4u_Q&YV(>=Enx3G08A7=Sl+vd zum?^tToyj#aYYpgO3d>;m0+P@({W8~&l_8_4~KPe%06L1`CcyOVQInwFLs)=t8p(M zBU&e2p$Zd^`}GW9$nsJ0i00n6asqd+z8alXpxV4AqQZhJALGZ=rkM)58ekAW2f`5 z#{2$4knNBJyJc}5t97dG-EXJ3pe;83e#@q1jS4<5ioP8V5J~=yOU7yqB6|1XixO@y z%g3DKO&yM^xuSz>C>byMt_iEEWCr5#t}2kQ0)8Z~E1050h85|)r$5MS3_oAD&CSMa zE$v%DDIl$^*m$N^g0NcWqW6h#f&uCv?R&s2T)YkAZs`}K0GZC^iQ3_wz`6|6g++3hOPbk;yh;{UA zA}(rY+NLY$N~lc;3;qTe7_fH2KwH4>RuLm!9_HZzDJtlI>utrkb-`TR(l*19h1KD^ ze}|Xduxy?yHatiIFvx)gn2;ue0eRsuJfSiln-`wV{K{d#$AO^_XpuRaNuVfG?J5_&g()%<#vgp!_K%hD8Q)wPSLq6(gH%4bc}o6Wq@S#Shi#1 z1DBxZ1r>sUIJ4luJjQT=_*ih>9??3JK)t^jLezonz(FZ;{zB}b#FC*)!YFK?4X>Q zFh(@ZU2^coN{=YHYYtji;CV&jtN^x1ah6K_o!E2}rg-4IPHr?SGqC<1bx~27qWx9| zWv)J-5DuDYL*TE|^Pn0mU64}Ee6_CxGTNSl<01eYIfHxfU!e#y*>05=u63)5xD~v2 zi{z%67VxmSE~dnub9~b>!vfdYoA=d;GQHXGP9`~N*C7bY=d5t_i4M?tq7%t#h6g>j zq>*VDIk@}H0hKIBQDI?0sgNJI;w?EykSq_IQsf=tN?ZA2_oHPfAOFL7yl zN9viXQJW!dLk@}wlvPIEt_ZWk`X~ehLB7c>utKUS z?y|D8I@%b@{QJ6T5bd2EA~Akir>4_mgZQpniWr8vZu~Zd1Tp|1uC7a{?b@Q4c3%g8 zU#yTY?n{UIb6Gm!xi?BrB8_Wz6ASrhYGiTC(^%aQ9Yx1et3gDl`S=+?QZ-Eg=su$| z5>Q7?H(SF=0sgWgzuhYa~7b^C#=yP<7d zT--Bj`*oQ7?~mmB<^bl#bPdvAnyDXBl^osHPkG{nmEB*=0f;Vv!>M8Q^v@{-f{yGh5$^S2K1&oQizer&S6 zwxw|Y71rY`a@Mv#&khT1QQ*DV_0G ze&jMu65r%n-xh#|rVEAkiSDo-;w=E;`5QI_@jd;ztF=$vd$r;&R2Y|_bGNi@2WdY{ z2xC>-mhd}7Z_hFhn`>53J=OBQ14&!8W*lVGRWu-XY+zRZ-c!Z(U(f6B*fn|JhT}4~ zmRGyZe=nHS);H@kH8>1Hzq{hbuVaWec0hZ*;0kWFYlzu8wQ=d&hP)=lpZJR5WAIEq zFVh2*FMyes{{8GcSfhQlQjcCXYnfcu@m7=bzINB7KU2$jRicbJD!&or`_I&bO^<&< z_|^O)Y4^`=$#YHZcwP6CXosjZj>EMn_4f6=Vi)GfQHtHSVy<5|fZ?@7WToE!4QjBC zeK9>4hKsnWk9zqiV6Q1S+5P4+f)OgVjx$>@=V>7!13)e5?J*I5PD!Fq59mLuR;_(D zKxNf!tRxxlI_u1@r@p!OKAJts9x`$^sX zs%^kW3&3LiDSOhv5>kUQ@nA4bs5SG7xTy0K;zgFUnJwu{WENq}d+($>?Heq@v4*Zj zFESCxmSuNDW(qPsP__u)6=Kub^kMWj29^Wh!ApBLZr8f($F$wB!K}GagGp=Z-SOEz zW{bQ`ayXFmD@nTN0LPAt2f}%mN4EAkbbfJvpxUhwYY#rq;!7yyxaPGw)NP4eK<|0 z5yGQINe8svf5x;ul+K4?XWBes1-HL0fb9>~f7PZZY$;5w5u^ zb}})BZZ?OdyC-9o1=DWZr|=p$eo{uBK7y&!K0 z6QJ~TK}g@E3H*9paHek#zg@tl??LNkDE;{(LH^}OyiB?KIo$1vvA2F`@2bM>9$>bJ zvNyiN@JKr5<;G)}sm!e2gWUGi=Kz8DUR_D1o#5Sq-edspyG7Z(9=ex}0%ecT_N)2R zdNsbbR~tz*{Un?o_3u0IS4;pz%RK;hcM(L1ZPm(Cuqkg6X7B5mf-N-Q5|+!&=XJU@ zcD-Eo*ufSVIW@0X8Po-wnaI%&h;2`HcL|zUaXnULj6yM|cud_XC4)!-3@iWD;l=L9 z?>acb%7;S-DHcp^!_Hi84(@m_GKS`Sh)Lyk`^sfWPd*^rU$c-_-O`gJnEQGpGLqFM z>+ZPr-Ea25SsildfPghdUstMH2>KSd4kXPupzthRs}gL|U!=N6g|l#ycwCK)@3RN} z%9>5#OylRFHgpQozyY${t&?AM9(E=_ADb2?j+i82a|NI&foC`w(TpFg7T{inAAC*uG-|lvw?*3ZP9unw2|7i zb8H)WJjme5=`B`B&%sOc^tQ91oYDkmJH7(fCXd^Tci8A8GTqk*#wRssm>1IiGCyDC3r)5seA*fNDOu1GKVu*1=-Gr!9EZxUH%5 zq127Ks_~1J^Mt}-m}bPoen+6EJw6O}`Y`x3(JdLf!Gfz`?>TTUV3w83yr)}<-PDoWzmue%4CxkZ zwDtQ0*|09-ztg|G99mOHlIm;lUg!FU$~T{L)_FyAsZlo(&_AAq~{ z-^;vD(2V!k{yQvVGHYR#D!~Zy@cAq4+m4%-rmDJKEXWx4^$a zxHE{nkwZ96tN-*4{H*h5b#X$3>&zq<4jZXv`cKP!SOAjqpG(7r%CfIoLGd>7F3xqI zeSN}rfe90s{@g}`f(?Es;7o{>^Nt(&J&gu@8piED4ITh)_W+`wy;t1`+bBa%vp;U` zrrcDL6!UjILm3#(?Ri1%l+H|E37{)MHeK>JJI9w>nHxXOb+6YeC}Y*>I>>F1{G9eTa(1M}+>r}djM;};8N!`%QDS8|^3 z`LCDob%Jn~61xWnNER3ChsQtueLc^I`8Qk00Q3Q@{ffJupJa8=0QVW7{IMUQbV`SP zUgOULus8s3dMbZ-=vqL>cubq`(Kw(PUN!)^1B~g@@Xdli_5omd$ z0H&9N%ISvi?3IoTYo_N6ur{>6l=C`Nu`s%AiT+me) zel|A=`bWcDl0oQwbN;$sAJ8w52s*xSG|r1^5bkEpaDN>P$mVZ?x{fe;kq2@1!GN}- z)mz<{mn)_Lfyb_cuq)F&$}M0;Z_Usxw>G-&ZNThkFbDHC$Hi^H+*{>hHX zNTRm^RR-iJ3YV!vPKy48tiw|A#F#i;iV$a?vzPzStlweZB z6YZF@6lcC315LgYdaXqqD(#Gqtphej-9yfa*Wkw#>lOvyqzsm&0pT9ocnq9>Cv0iK z(bv4EENf;UBV{%xnxvU1m7A}|Ks#W~XVfStaEP^TAq-_=qs|C2Xrr z#%98;e63$SjQ=p*SKSgB69hC@^deVBtc_X%$Mg5`X?xYcx;pngyIlk)Wo;f-8C~oy zJ7w|_1V{?WpT_g@&A!_-Xnk97t=U0M!7`%GE~)lEKG3pOxq9{eDzoIg+i4wYVX^0&)dmw>YE(M& zL|$2KiNDA5+vlVry%YEr(GwI4_5tjE!u%)(?e6OPtgEJP-XHpO)7z+2H>ol?CsvC- zX*Pa4Jw~f@J1KD!Ba1G`^90fDET$C}$|uF{LWtx{q8VM|)$yyfwmXQqv?c zX&7o(`i|+AM+ELM0Jin>e(-Ef)@gy*HkjlbL5|P`Z7vf(?!o9;n#2yHYnF1B7~5pkjb9=|~VJg7!7+gwG6` z-vH@w8+wW0W>h$bWp5!zdTg_rN3$@rf9!m-g~Sh+SY$0QApaGnz(=-q93w?0I0Mpo z-(ArCY(TM-oEzQ=y!{tUy;FsyW9ivnUM$QbxSMI=>JtDsj{n}nVPS46y#H?KA%dyw zr2}a;Kfe&E!79*tr7t(nsi6qiMxc1;n3f`!*&wO&E)%2G8ajF(QAi zyT$f7IrVSoXXfFNEtG_M6579eoxQDp9?|^h>p$&|^Z*$jI=C<3j;2(WAG+x-)3c$n#Uf_~pO6Hg(|O7l2mU zd(BgZ{cQ@*lSFV+_Vapyb2;p8aJPs5El=8@Ucl||W72KhIB{4JcE-4jISpQ{(#zn; z2Hpb1{kKu;EAlm$Jh5}Ido-FcnMj*3oz^hQH_yCorC}R6ZT8%tETHaR>(A7UK2D1{ zF3vXl-E8&@+7PfHZnlM7akYm%wp{|n9v$o~nn`D&1;~zlUu#7MZg2kL4fhLP`htK- z!+f~D_mG31u!fTf^c+r?y^vY~E>FIo5Y&Q%>8$ktXv z%0Z00+Ie3GBNNs-!HRO3I&ATEX*&*ciDdJ(3iZ}?LOWn%$2oc}!4A78-u-5Q88)*h zR*}g;ptJ#eUJbWnXUA}SHr!||HWvpT_6hUhT^Lr=%@f)IVZZI9YGQKUlCQhgEZ{w- z(*RSrvs$*id~gcR&gKbXKx^LA`>K=s(&JGj>#K}Oe%_|1Z)WjtdsCAAov^5?Yjw}7 zLE4$^$?>lR)_K7_${J=k|CrA!pML7!M++3IUt|r05|`%A3s-U^`y(iXu9q1Zy&+LktIs zY`;ZuM$^fx)pNo0v1~_!G#5U0a&ViNCFnK_-~UG1qa4#rdFa;U*lL^0&pTVa+=QE! z@0}hQSby3vG1vY&Usx~C0UUGI?WY>p4-3k{{NA)o4(4w>u)yxg{lRA3IXyam*5ZQ{ zM#X-#?-@mt#hdFv-E|kU3rKqH&;42YC~^{jx5Mnu3CVx~&-yp_JvV&fWQ6L3i<=AB zJ@;3eFlNM!Z|{o(j^moh?ywfRdNTUv$M|G61RlUGE5SIPyJTmCui4D{s%t=EV>fUZ ziq~3K^0sga<^vcq9^O5C8SiEn^e%W0&}fQD)Oi^MlbvIUzJr?gbHE&PO_YaB{6DI2 f8=~~-fRp|}^DzWNrvv818T~QV&p9AC!ex{qU}4d= literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_image/0/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_image/0/zarr.json new file mode 100644 index 00000000..ca44cb71 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_image/0/zarr.json @@ -0,0 +1,49 @@ +{ + "shape": [ + 3, + 64, + 64 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 3, + 64, + 64 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "c", + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_image/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_image/zarr.json new file mode 100644 index 00000000..daa726e2 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_image/zarr.json @@ -0,0 +1,102 @@ +{ + "attributes": { + "ome": { + "omero": { + "channels": [ + { + "label": 0 + }, + { + "label": 1 + }, + { + "label": 2 + } + ] + }, + "version": "0.5-dev-spatialdata", + "multiscales": [ + { + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "name": "/images/blobs_image", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "cyx", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + } + ] + } + ] + }, + "spatialdata_attrs": { + "version": "0.3" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/c/0/0/0 b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/c/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..30dae7066a898a5570ca7be00e8eb970fc24cb2a GIT binary patch literal 79769 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 literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/zarr.json new file mode 100644 index 00000000..724ab4cc --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/0/zarr.json @@ -0,0 +1,44 @@ +{ + "shape": [ + 3, + 64, + 64 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 3, + 64, + 64 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/c/0/0/0 b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/c/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..b685afc60a8a624686edbc387b6c3da34fba1ffb GIT binary patch literal 22390 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
    KJXAvY^B$>I_^3bt>7NGpt5hKKgDDa-Mau)@Q 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 literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/zarr.json new file mode 100644 index 00000000..48c80911 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/1/zarr.json @@ -0,0 +1,44 @@ +{ + "shape": [ + 3, + 32, + 32 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 3, + 32, + 32 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/c/0/0/0 b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/c/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..48cbafdc5addc0753e2e29bca2a8d61b97bafc72 GIT binary patch literal 5822 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 IgF?%|80N|HWB>pF literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/zarr.json new file mode 100644 index 00000000..c33e2c1d --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/2/zarr.json @@ -0,0 +1,44 @@ +{ + "shape": [ + 3, + 16, + 16 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 3, + 16, + 16 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/zarr.json b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/zarr.json new file mode 100644 index 00000000..8080f7d9 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/blobs_multiscale_image/zarr.json @@ -0,0 +1,128 @@ +{ + "attributes": { + "ome": { + "omero": { + "channels": [ + { + "label": 0 + }, + { + "label": 1 + }, + { + "label": 2 + } + ] + }, + "version": "0.5-dev-spatialdata", + "multiscales": [ + { + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ] + }, + { + "path": "1", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 2.0, + 2.0 + ] + } + ] + }, + { + "path": "2", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 4.0, + 4.0 + ] + } + ] + } + ], + "name": "/images/blobs_multiscale_image", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "cyx", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + } + ] + } + ] + }, + "spatialdata_attrs": { + "version": "0.3" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/images/zarr.json b/inst/extdata/blobs_v3.zarr/images/zarr.json new file mode 100644 index 00000000..d1f97dad --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/images/zarr.json @@ -0,0 +1,5 @@ +{ + "attributes": {}, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/c/0/0 b/inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/c/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..e6022913738692024a9b9de6e5696bfbc5dfbaa1 GIT binary patch literal 431 zcmV;g0Z{%ZwJ-f(03R(4076C!k6;5c03b0!0fi_+xUdA7$jZvPYqw6G$js@f2D2Z0Qzp>mA@(FP?WW77c=pus>2Mo@@iAP9pPA%qBuz!*Y^fEYrE zGDRhu12`za8x2TjXG}flNmrzHtaL`xzth~E&o&kqFaFlIQL|=61}l+GT`EA&8X$o9 z-X?PJYc|{~=hK9)A~2ZXp~`~g8akDO+~1mWOAko$^V&xd%HDS{TTA?O9zSdr{0{eu z;kf)eMP|ac`}R?Qr7vKA+I(fr7!-|az>7`E^e_^e5c`d>a7a|bDfAQY{e{g-fLB1sVrripCxa8!i5!TS6EO}x?G5rdPr z!>hb*Q4kIkp^qnsPpvRj0|Ktn{+9<3J}HeKN#65pRO?0J9V7#@73{+`&gcUr`)&6@ Zzn@Xl^L)1>^638nkEChG;Et$j%71R`%&Y(a literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/zarr.json b/inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/zarr.json new file mode 100644 index 00000000..dac77af5 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/labels/blobs_labels/0/zarr.json @@ -0,0 +1,46 @@ +{ + "shape": [ + 64, + 64 + ], + "data_type": "int16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 64, + 64 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_labels/zarr.json b/inst/extdata/blobs_v3.zarr/labels/blobs_labels/zarr.json new file mode 100644 index 00000000..f68c3f89 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/labels/blobs_labels/zarr.json @@ -0,0 +1,307 @@ +{ + "attributes": { + "ome": { + "version": "0.5-dev-spatialdata", + "multiscales": [ + { + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0 + ] + } + ] + } + ], + "name": "blobs_labels", + "axes": [ + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + }, + { + "type": "scale", + "scale": [ + 3.0, + 2.0 + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "scale", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + }, + { + "type": "translation", + "translation": [ + -50.0, + 10.0 + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "translation", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + }, + { + "type": "affine", + "affine": [ + [ + 20.0, + 10.0, + 30.0 + ], + [ + 50.0, + 40.0, + 60.0 + ] + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "affine", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + } + }, + { + "type": "sequence", + "transformations": [ + { + "type": "scale", + "scale": [ + 3.0, + 2.0 + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + }, + { + "type": "translation", + "translation": [ + -50.0, + 10.0 + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + } + ], + "input": { + "name": "yx", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "sequence", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "unit" + }, + { + "name": "x", + "type": "space", + "unit": "unit" + } + ] + } + } + ] + } + ], + "image-label": { + "version": "0.5" + } + }, + "spatialdata_attrs": { + "version": "0.3" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/c/0/0 b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/c/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..e6022913738692024a9b9de6e5696bfbc5dfbaa1 GIT binary patch literal 431 zcmV;g0Z{%ZwJ-f(03R(4076C!k6;5c03b0!0fi_+xUdA7$jZvPYqw6G$js@f2D2Z0Qzp>mA@(FP?WW77c=pus>2Mo@@iAP9pPA%qBuz!*Y^fEYrE zGDRhu12`za8x2TjXG}flNmrzHtaL`xzth~E&o&kqFaFlIQL|=61}l+GT`EA&8X$o9 z-X?PJYc|{~=hK9)A~2ZXp~`~g8akDO+~1mWOAko$^V&xd%HDS{TTA?O9zSdr{0{eu z;kf)eMP|ac`}R?Qr7vKA+I(fr7!-|az>7`E^e_^e5c`d>a7a|bDfAQY{e{g-fLB1sVrripCxa8!i5!TS6EO}x?G5rdPr z!>hb*Q4kIkp^qnsPpvRj0|Ktn{+9<3J}HeKN#65pRO?0J9V7#@73{+`&gcUr`)&6@ Zzn@Xl^L)1>^638nkEChG;Et$j%71R`%&Y(a literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/zarr.json b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/zarr.json new file mode 100644 index 00000000..a573f4af --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/0/zarr.json @@ -0,0 +1,42 @@ +{ + "shape": [ + 64, + 64 + ], + "data_type": "int16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 64, + 64 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/c/0/0 b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/c/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..826823f1ca0bde3ea215ac1c44869a9a3e2dd0b6 GIT binary patch literal 239 zcmdPcs{c2EfnArKA%&TNfro(?hy{R<3kdmvkQE3S1bA3^SXo%OK?<08n0dIEnR$S0 z4h9Y$Uf#?V0pU{22F#PfWQ7toYE1FUU`bYKRB}7eq2yx_G||H#QH6D@jDo#pY-CIS zCkCI(ay_9>1YbSb^k0&(e8z&EOAlF@Yu7mM&PZ7FVexx`*PFsbpEWOc{oKYqtF%Vr zc%O{dha-(m57om18zT5LpIw-~G{NMjMOkTx-b3bjKkCwT$@Hs4W_ literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/zarr.json b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/zarr.json new file mode 100644 index 00000000..c5a39ce9 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/1/zarr.json @@ -0,0 +1,42 @@ +{ + "shape": [ + 32, + 32 + ], + "data_type": "int16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 32, + 32 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/2/c/0/0 b/inst/extdata/blobs_v3.zarr/labels/blobs_multiscale_labels/2/c/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..3d9fa2dd36080aacf4e007c3cbd4434f59a83c17 GIT binary patch literal 119 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 literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/points/blobs_points/zarr.json b/inst/extdata/blobs_v3.zarr/points/blobs_points/zarr.json new file mode 100644 index 00000000..889834a0 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/points/blobs_points/zarr.json @@ -0,0 +1,51 @@ +{ + "attributes": { + "encoding-type": "ngff:points", + "axes": [ + "x", + "y" + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "xy", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + } + } + ], + "spatialdata_attrs": { + "instance_key": "instance_id", + "feature_key": "genes", + "version": "0.2" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/points/zarr.json b/inst/extdata/blobs_v3.zarr/points/zarr.json new file mode 100644 index 00000000..d1f97dad --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/points/zarr.json @@ -0,0 +1,5 @@ +{ + "attributes": {}, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/shapes/blobs_circles/shapes.parquet b/inst/extdata/blobs_v3.zarr/shapes/blobs_circles/shapes.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0bfe0ec6899a9cc02e47f17cd606164fa775c08d GIT binary patch literal 3214 zcmbVP-D~666_=9`ve_wQm&&z=JOo3-w9PpBw5?$VEZeayZ{)Xn5 z^Es+2p z=F&gT{(V5@^WP?44U&8%nS46<P6c?aOV z{b&u|+p+caACw75V~70uF}a45*xKC$6?!1jyJUP7DY3QGtX6U2(Ho>V##?()jMz3r zNIvC>#O7GPG#!=PAjk*bTuW^A%%NlYqY(74-5jZg6^x1ac5*j)uutrg50X5wxszNO zl56qp`0v-_KU&}aM{IxV|H(ah0O=Cs8p>`5zMjTEjU_je`@}Y6jDl_~f-d0TkgsC# zKgIU{yuSa__1#tOtJPPs@LIHQEkniswGrRm-->^_K_s6hHa*of)N$f>8&@gIHO$$C zKC}bJ9jBh`y|_vZKnAPSNOgN=>R>PBs*VW)H?VEkjeT|G!Z1+JwJjR-;Dc;Ss=sP#?VN64U{s-gNSm|vw# z7tAfUht8g#l^^F)*SEmjMUh%|1M+eQ%N@-lDSc$B{&0jDvMp_-jzUC`7pkMtaEjbd z%+c5yx=6~fX(~;p(76Se+sZVXW>eQhVjaj!{Cw>y<&7){{4LrCIk-*hs;RD4^81BS z-YXys`Uur$(P2v`xZt>Msna2(okAIE+HeM@FX&u4%Td`Z%W^qh;Dy}59-!y5S%J+k zG)-lBS~%F_GHE&sGM>%mSenmfI4t96F30AW9K$h!z+~RPMj78ql?EE92-h3g1B4Mj z_xPjJG@=MNdLTBNC(4s(1|@#>wL|oLr*|ZZVgo+tc~2BMvCtFCqSzCQJuE8}?&zmT z2xk`h+O!ny;f?G|Q!qz*QS?qHhlgSBW?z|txH`7a%i@MzQ)XGP2sE)OSHO7 zYoaMFN_Pj=aZ$87j4(D7I?zKYBf+`b`V*^=m7G=pHUm}RU8z6pNw#s^u%xk6to3zQ z2A_q%P-bJ~YjxIsX;kfLw~h6hWEUoIo|0?|owGWQz#(7C{D?d5>y%v5S$Qr~C#7?~ zem3C1S15-ZU7zc`Jnw|^+0nE zRDZaiQ@F<~&K)~2N*yZ78S<}`;e7M*rbnu@f%*Vsl`MD7*^icI~t^_iVG;w6$n{WKeG|?gRD&hamq3$^(&v zz`}zAVqVaUg}WbWtR5JQZ(%jg@+AWq>wk^caWhq4&} literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/shapes/blobs_circles/zarr.json b/inst/extdata/blobs_v3.zarr/shapes/blobs_circles/zarr.json new file mode 100644 index 00000000..9a1f0a06 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/shapes/blobs_circles/zarr.json @@ -0,0 +1,49 @@ +{ + "attributes": { + "encoding-type": "ngff:shapes", + "axes": [ + "x", + "y" + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "xy", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + } + } + ], + "spatialdata_attrs": { + "version": "0.3" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/shapes.parquet b/inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/shapes.parquet new file mode 100644 index 0000000000000000000000000000000000000000..86952e519354c1996fc3a883285852a8db858f13 GIT binary patch literal 3346 zcmeHK&5zsG5$9^x3%fB8+kmzbpu<8mHZtRl!rm~cl+Lb8DaSeVgml8=a zlDPU(%7hLASWM!rY)MHEzrP+pa&m%2-;JMUfNTOegIB7?@{uugt$eD z9!r4ado%N9e)F4okJ27jiwD<&Z(RufY;_j~#5IEWF!0hnf*@Ab;SpH5M69k{_$>_A z;QmMd5&7k8p4~1Phutr+>^pBi`puJHWZ4_Hue|@ox6|w^Z$16#CuD~GO6ZfP^h1Wd zMG}-iG#~TE{c63${zNJK>G5B)?CHeFJ$+IL-nn`+a)0Z07ykJJ7MT+FR=!tzK>YmH zn;+9(yOw9)y!_zc#ZNi*_v6XaD}g58nIeTlvgQBJ$zi-hI9P%_52tB!bt18{3fV zmEd}CJ(40`xwqmci?UutkN+~=WqKBzJ%@y6T+SEI30bxeJrCvQrfZ4b1(eU>@Qmx( z;yIzq_E)b0cb7H;h{2VW)j)79yqzRM>jU*jx8-1f2wr|U5V~mSJzF34C&b2DI1-L* z5s~2KaFSTR51_-+5u<$CtMLu)1~e|AqQr7b=W40fA~lz+MOtuWo$%LU=vA zg||9ler@X_uup_v4XwMfqsfENuUAi^rlaZOyK2w!Y-bR?v3dI>+65VmqJ7yh^yv0x z)RAo+1deA}FdGcz{t%`l9`{@rQh2DNVf^$COniTLv2%UqcG1hBsax9JMVSxl?r`Fw za8b`O)t=@f@N9STOwm(1x;jM2pzUN$9?D>T64f0rHys15tsfMwr=zFmz$~JO&U+Sl zS-`TOSwf`S?r?$``OkO|TR6Nt9QIAc8)6<#P7ynOT^{!O7_?<7eYrnDnB9ro@ApQ? z|FPa5m^}wcsYHy7#iMB549$6EED=jYPl?bf>NvC6}RElOOCcQ&4REj~eRE$Zdm^4Y!sU(w1?A$p;d7n!- z4rGu%y3gbgaQZAQu^5lV5mCU@Wv*7MN;mu?EcDI`Uq#o{$a5Sg!i}!iIF9Br23O!X zgUcFNmdPyWM@R^3X8N3%^XKqH_IVM^k)GqEy(#$Tp3Qj?#HE3$mddUo6()Lv=_u4V zu1K_5H950QHI5ajL8?yI+|6=ki(&?v6!+8#pWhL`8r!`9cm?O)NW$E!gu3wrBrk4MP{H&+OcAmH5}V6AM06& zTNq3$COxg@_(WwYB&!GFq?&8RE7_4DbVRSx5vIa{N>+2?sFD-Gu0G&%BE8#bQI(FE z6gpkHGUediN>=tqMkUwsss{!I_ejWzROO&c2~$lwY?25!)ozxjvcz~epHAKrvh;{I zM+R?KCT*!eDhVFt1-XyimQy}LT+O*tiv_P;EKISz)3#Kn;wqHSt*}pzcRPX!aZqVc z%{asfS3Y2%3+ku?P@yXr6BjtzYgA3^_-dNYIl@-}12cB7+Zv3}TY z){cNnvb8_!Jt+yjT5}a!#r_(NT56buGPAvlM?HrO?^bl4?g<(O(IR1avgQn@6qzEL=Ds$o>7*YL+KT zLiIFiXd)TP4%t Met{tN;NQ@{052&kkN^Mx literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/zarr.json b/inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/zarr.json new file mode 100644 index 00000000..9a1f0a06 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/shapes/blobs_multipolygons/zarr.json @@ -0,0 +1,49 @@ +{ + "attributes": { + "encoding-type": "ngff:shapes", + "axes": [ + "x", + "y" + ], + "coordinateTransformations": [ + { + "type": "identity", + "input": { + "name": "xy", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + }, + "output": { + "name": "global", + "axes": [ + { + "name": "x", + "type": "space", + "unit": "unit" + }, + { + "name": "y", + "type": "space", + "unit": "unit" + } + ] + } + } + ], + "spatialdata_attrs": { + "version": "0.3" + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/shapes/blobs_polygons/shapes.parquet b/inst/extdata/blobs_v3.zarr/shapes/blobs_polygons/shapes.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0a1f16732a4a7356778a8bcffebbe2ea999bfd01 GIT binary patch literal 3067 zcmdT`TWlj|6`oDfkWE);i*T@4d58>z70o&`9#8C9ZI}7SiS2Q0C$Zz}whD8zJ@(Al zwa1s4C?M?vLZ$G~3aQdoqFPi06*nz?qZP0cpdyM8;<7Jj-;iKYg#ZcBs)h5QaDZpXOG&%gc3J0#CN>VN0$$TwK-I!W+* z#1HR0_vgQ&yNG-1LH9mNrn#?mzx|mvzsGT(^u5v!Fi9@-O!R)6(VX6 zJwEYszLWUX%OAYXJ#e1AfALm~yE*rgc&mPmd-K&7U-;XzQSRP9?EF_>4Re=$qiyQ^ zbAZ-Z#IzL0rzBo5dvlj|842+&>-eeG_zs&_^BtTn}HK zf#1b*v$LO15Wcy<8bkQ!`*Kq?MBfbIyYTTD|3X9U7^>B?iKWHBa$tFtSoU2AFvR@D zz=_T`v$VMMv-3+&Ev$a&q1A=|hvWlm(DVe(l7NcM5Z9J&&IjfLtHdHOf^%71U4VRu zz%~DTS2Pu|@BiuSVNf#_b$lRq^nqdagI8B>90uDUgSTK$G#hGgZ6#=mh6)06pzE;e zTVl_GCF#C*y70=luPT0}j-G&p7w^Cgu1>;EN2zP7t{hCuJXi;o-NoAl2c{->6c53` z=-S7Mj?_|R3nPOO)%HY2KFuPC$7YTN2kC{qX@p+1vttSmMP5{M5@uX zY?qP$jt8^lZZBF^Pm>0g%fsOjW~Zl$R;TBJ)-|ao_H2x~Ym2>JXN2Pq)m~rgm{=O7 zLu7~wV!sKR)5;JXqJu|-e-`rbKRLeU5rjnZ@^#;^<1sn9O0Btv!!L1zf0 zLKNl*NV8;@x+ixK-cVnW;uCu;wkB-+VEKc<`IhF28<%F%4*p?I?Q&Ksi?Py4=g-b)JTq0$& zt!3g!tA^RWQlG;WjFK&f^`WFeu5n?UX`w+jKFP%>HI*c5;~Z5x-51MgyIM&z zZqA0HOElkP#!nucQm9!(4*@3HXZ4I)8h$EO33w41j3pa1p$}yzx((CK&-f?}# zBdNR)!WbMSnXr{O$Trbr z&5kgh9Ol@}k+llKamMu$iS?5{VCs{-b+_j3o=j6J7pH4!=(?25*WY8`Q5CRPD><*m z7*jk=Nrn#b^SYqn`U1uZ&s69ArT1E1m|PS*upb+iJnBjUkpx{aS(@RLhj^)(6)w411=a zo*17wsU^jdYK5QJlq@-w_w?@*zhsvBQi@XC^Hj-`vY~5ACGYW@vfSRQ(rF#?9m=@( z;G9WGU4!~!%sAhX3TIU|SlYu^(`7T)#peZcmu+RCPa|W&bnAG+|8zgw8Ej{#p$DtD z|0Z_07xDOUh4@*(!zYeV5+T_gEO?s0Fe+drTs+U?Q5Emt1=xYa8&b#X4rVzyXeKs58-O_`i(4f?nh_-p zwN4;F@d$xWn8+zWtU}S-3h)U)x&ygZin9?wD|jKHQVu3SfS#a4d&CPs;^cWfNuCTq zCdkD&4qy5|^F}4oGxr`q%1`f&TSFi~h9_AclSHf}j(H3~iWOAX zM;_)s3VfsQs3t@}M3v(fyy`$eQ^L(WS!zi@VR<@w5Y)^+A)*I(Wd#C2y)l%W4NGD_ zr@i+`xMuS|pqUR-mU-kqce)Qci4L$oRTL|%4J;f$m43)nWB3z5k+{D#CpH>DR$pdK literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/tables/table/X/data/zarr.json b/inst/extdata/blobs_v3.zarr/tables/table/X/data/zarr.json new file mode 100644 index 00000000..c9cc2cfc --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/tables/table/X/data/zarr.json @@ -0,0 +1,40 @@ +{ + "shape": [ + 30 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 30 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/tables/table/X/indices/c/0 b/inst/extdata/blobs_v3.zarr/tables/table/X/indices/c/0 new file mode 100644 index 0000000000000000000000000000000000000000..4ca94ff76a24d12bc64f76ee0eea42176b684a3d GIT binary patch literal 28 fcmdPcs{dD^VlD$i00R&(0x=U1K2>GpQD6iBT=oRa literal 0 HcmV?d00001 diff --git a/inst/extdata/blobs_v3.zarr/tables/table/X/indices/zarr.json b/inst/extdata/blobs_v3.zarr/tables/table/X/indices/zarr.json new file mode 100644 index 00000000..18266fe6 --- /dev/null +++ b/inst/extdata/blobs_v3.zarr/tables/table/X/indices/zarr.json @@ -0,0 +1,40 @@ +{ + "shape": [ + 30 + ], + "data_type": "int32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 30 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/inst/extdata/blobs_v3.zarr/tables/table/X/indptr/c/0 b/inst/extdata/blobs_v3.zarr/tables/table/X/indptr/c/0 new file mode 100644 index 0000000000000000000000000000000000000000..dce39b80e7fb05879e80d73a2bddaf2fdedde2ab GIT binary patch literal 53 qcmdPcs{dC(Cy|i>1ek%C4Tw2`m Date: Sat, 11 Apr 2026 17:56:03 +0200 Subject: [PATCH 133/151] Zarr v3 --- .gitignore | 1 + NAMESPACE | 3 - R/AllGenerics.R | 7 +- R/{coord_utils.R => CTutils.R} | 43 ++-- R/ImageArray.R | 14 +- R/Zattrs.R | 9 + R/read.R | 6 +- R/scratch.R | 33 --- R/validity.R | 312 ++++++++++++++--------------- man/{coord-utils.Rd => CTutils.Rd} | 10 +- man/SpatialData.Rd | 8 +- 11 files changed, 212 insertions(+), 234 deletions(-) rename R/{coord_utils.R => CTutils.R} (90%) delete mode 100644 R/scratch.R rename man/{coord-utils.Rd => CTutils.Rd} (94%) diff --git a/.gitignore b/.gitignore index 7ac4af56..a110dd34 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ inst/extdata/xenium1.zarr inst/extdata/visiumhd.zarr *.Rproj *.html +R/_* diff --git a/NAMESPACE b/NAMESPACE index 449d3f02..87098855 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -116,7 +116,6 @@ importFrom(S4Vectors,coolcat) importFrom(S4Vectors,isSequence) importFrom(S4Vectors,make_zero_col_DFrame) importFrom(S4Vectors,metadata) -importFrom(S4Vectors,setValidity2) importFrom(SingleCellExperiment,"int_colData<-") importFrom(SingleCellExperiment,"int_metadata<-") importFrom(SingleCellExperiment,SingleCellExperiment) @@ -130,7 +129,6 @@ importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) -importFrom(ZarrArray,type) importFrom(anndataR,read_zarr) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) @@ -157,7 +155,6 @@ importFrom(graph,nodeData) importFrom(graph,nodes) importFrom(methods,as) importFrom(methods,callNextMethod) -importFrom(methods,is) importFrom(methods,new) importFrom(methods,setClassUnion) importFrom(methods,setReplaceMethod) diff --git a/R/AllGenerics.R b/R/AllGenerics.R index cc2107a1..62ae3d87 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -40,7 +40,6 @@ setGeneric("tables<-", \(x, value) standardGeneric("tables<-")) # trs ---- -setGeneric("axes", \(x, ...) standardGeneric("axes")) setGeneric("CTlist", \(x, ...) standardGeneric("CTlist")) setGeneric("CTdata", \(x, ...) standardGeneric("CTdata")) setGeneric("CTname", \(x, ...) standardGeneric("CTname")) @@ -79,11 +78,13 @@ setGeneric("meta", \(x, ...) standardGeneric("meta")) setGeneric("query", \(x, ...) standardGeneric("query")) setGeneric("mask", \(x, i, j, ...) standardGeneric("mask")) +setGeneric("axes", \(x, ...) standardGeneric("axes")) +setGeneric("extent", \(x, ...) standardGeneric("extent")) setGeneric("channels", \(x, ...) standardGeneric("channels")) +setGeneric("centroids", \(x, ...) standardGeneric("centroids")) setGeneric("data_type", \(x, ...) standardGeneric("data_type")) setGeneric("geom_type", \(x, ...) standardGeneric("geom_type")) -setGeneric("centroids", \(x, ...) standardGeneric("centroids")) -setGeneric("extent", \(x, ...) standardGeneric("extent")) +setGeneric("multiscales", \(x, ...) standardGeneric("multiscales")) # tbl ---- diff --git a/R/coord_utils.R b/R/CTutils.R similarity index 90% rename from R/coord_utils.R rename to R/CTutils.R index 91a1517b..a8b0eb01 100644 --- a/R/coord_utils.R +++ b/R/CTutils.R @@ -1,5 +1,5 @@ -#' @name coord-utils -#' @title Coordinate transformations +#' @name CTutils +#' @title Coord. trans. utilities #' @aliases axes CTlist CTname CTtype CTdata addCT rmvCT #' #' @param x \code{SpatialData}, an element, or \code{Zattrs}. @@ -38,30 +38,31 @@ NULL # axes() ---- -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("axes", "Zattrs", \(x, ...) { - if (!is.null(ms <- x$multiscales)) x <- ms[[1]] + ms <- multiscales(x) + if (!is.null(ms)) x <- ms[[1]] if (is.null(x <- x$axes)) stop("couldn't find 'axes'") return(x) }) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("axes", "SpatialDataElement", \(x, ...) axes(meta(x))) # CTlist/data/type/name() ---- -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTlist", "Zattrs", \(x, ...) { - ms <- "multiscales" + ms <- multiscales(x) ct <- "coordinateTransformations" - if (is.null(x[[ms]])) return(x[[ct]]) - x[[ms]][[1]][[ct]] + if (is.null(ms)) return(x[[ct]]) + ms[[1]][[ct]][[1]] }) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTdata", "Zattrs", \(x, i=1, ...) { stopifnot(length(i) == 1) @@ -81,31 +82,31 @@ setMethod("CTdata", "Zattrs", \(x, i=1, ...) { mapply(x=ts, i=names(ts), \(x, i) x[[i]], SIMPLIFY=FALSE) }) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTtype", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$type, character(1))) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTname", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$output$name, character(1))) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTlist", "SpatialDataElement", \(x, ...) CTlist(meta(x))) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTdata", "SpatialDataElement", \(x, i=1, ...) CTdata(meta(x), i)) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTtype", "SpatialDataElement", \(x, ...) CTtype(meta(x))) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTname", "SpatialDataElement", \(x, ...) CTname(meta(x))) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("CTname", "SpatialData", \(x, ...) { g <- CTgraph(x) @@ -115,12 +116,12 @@ setMethod("CTname", "SpatialData", \(x, ...) { # rmv ---- -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("rmvCT", "SpatialDataElement", \(x, i) { x@meta <- rmvCT(meta(x), i); x }) -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("rmvCT", "Zattrs", \(x, i) { nms <- CTname(x) @@ -155,7 +156,7 @@ setMethod("rmvCT", "Zattrs", \(x, i) { # add ---- -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("addCT", "SpatialDataElement", \(x, name, type, data) { x@meta <- addCT(meta(x), name, type, data); x }) @@ -173,7 +174,7 @@ setMethod("addCT", "SpatialDataElement", \(x, name, type, data) { if (!.) f(t) } -#' @rdname coord-utils +#' @rdname CTutils #' @export setMethod("addCT", "Zattrs", \(x, name, type="identity", data=NULL) { stopifnot( diff --git a/R/ImageArray.R b/R/ImageArray.R index afc52939..92936c96 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -43,11 +43,11 @@ setMethod("channels", "ImageArray", \(x, ...) channels(meta(x))) setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) #' @importFrom S4Vectors isSequence -.get_multiscales_dataset_paths <- function(md) { +.get_multiscales_dataset_paths <- function(za) { # validate 'multiscales' - .validate_multiscales_dataset_path(md) + ms <- .check_ms(za) # get & validate 'path's - ds <- md$multiscales[[1]]$datasets + ds <- ms[[1]]$datasets ps <- vapply(ds, \(.) .$path, character(1)) ps <- suppressWarnings(as.numeric(sort(ps, decreasing=FALSE))) if (length(ps)) { @@ -59,10 +59,9 @@ setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) return(ps) } -#' @noRd -.validate_multiscales_dataset_path <- function(md) { - # validate 'multiscales' - ms <- md$multiscales +.check_ms <- \(za) { + # validate 'multiscales' + ms <- multiscales(za) if (!is.null(ms)) { # validate 'datasets' ds <- ms[[1]]$datasets @@ -78,6 +77,7 @@ setMethod("channels", "ANY", \(x, ...) stop("only 'images' have channels")) } else stop( "'ImageArray' paths are ill-defined,", " no 'multiscales' attribute under '.zattrs'") + return(ms) } .check_jk <- \(x, .) { diff --git a/R/Zattrs.R b/R/Zattrs.R index 9bae799b..558cd859 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -38,6 +38,15 @@ Zattrs <- \(x=list()) { #' @exportMethod $ setMethod("$", "Zattrs", \(x, name) x[[name]]) +# internal use only! +#' @noRd +setMethod("multiscales", "Zattrs", \(x) { + v <- x$spatialdata_attrs$version + switch(v, + "0.2"=x$multiscales, + "0.3"=x$ome$multiscales) +}) + .showZattrs <- function(object) { cat("class: Zattrs\n") ax <- axes(object) diff --git a/R/read.R b/R/read.R index f0dbec78..7d3dce5a 100644 --- a/R/read.R +++ b/R/read.R @@ -203,8 +203,10 @@ readSpatialData <- function(x, args <- as.list(environment())[.LAYERS] skip <- vapply(args, isFALSE, logical(1)) sd <- lapply(.LAYERS[!skip], \(i) { - y <- file.path(x, i) - j <- list.files(y, full.names=TRUE) + j <- list.dirs( + file.path(x, i), + recursive=FALSE, + full.names=TRUE) names(j) <- basename(j) if (!isTRUE(opt <- args[[i]])) { if (is.numeric(opt) && opt > (. <- length(j))) diff --git a/R/scratch.R b/R/scratch.R deleted file mode 100644 index 6de4c1e4..00000000 --- a/R/scratch.R +++ /dev/null @@ -1,33 +0,0 @@ -#' @importFrom methods as -#' @importFrom anndataR read_zarr -#' @importFrom SpatialExperiment SpatialExperiment -readSDasSPE <- \(x, i=1) { - if (!requireNamespace("SpatialExperiment", quietly=TRUE)) - stop("install the 'SpatialExperiment' package to use this function.") - y <- list.files(file.path(x, "tables"), full.names=TRUE) - if (!length(y)) - stop("couldn't find any tables") - if (is.character(i)) - i <- match(i, basename(y)) - sce <- readTable(y[i]) - colData(sce) <- NULL - for (l in setdiff(.LAYERS, "tables")) { - y <- list.files(file.path(x, l), full.names=TRUE) - y <- y[match(region(sce), basename(y))] - if (!is.na(y)) { - t <- basename(dirname(y)) - f <- paste0("read", - toupper(substr(t, 1, 1)), - substr(t, 2, nchar(t)-1)) - y <- get(f)(y) - xy <- centroids(y, "matrix") - xy <- xy[, c("x", "y")] - } - } - toSpatialExperiment(sce, spatialCoords=xy) -} - -require(sf, quietly=TRUE) -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -(spe <- readSDasSPE(x)) diff --git a/R/validity.R b/R/validity.R index edd7ba70..be41841d 100644 --- a/R/validity.R +++ b/R/validity.R @@ -1,156 +1,156 @@ -# https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions -#' @importFrom SingleCellExperiment int_metadata int_colData -.validateTables <- \(object) { - msg <- c() - sce <- \(.) is(., "SingleCellExperiment") - for (i in seq_along(tables(object))) { - ok <- sce(se <- table(object, i)) - if (!ok) msg <- c(msg, paste0( - i, "-th table is not a 'SingleCellExperiment'")) - if (!ok) next - md <- int_metadata(se)$spatialdata_attrs - nm <- c("region", "region_key", "instance_key") - .nm <- sprintf("'%s'", paste(nm, collapse="/")) - if (any(ok <- nm %in% names(md))) { - if (!all(ok)) msg <- c(msg, paste0( - i, "-th table missing ", .nm, "; must set all if any")) - ok <- all(vapply(md, is.character, logical(1))) - if (!ok) msg <- c(msg, paste0( - i, "-th table's ", .nm, " is not of type character")) - ok <- all(vapply(intersect(md, nm[-1]), length, integer(1)) == 1) - if (!ok) msg <- c(msg, paste0( - i, "-th table's 'region/instance_key' is not length 1")) - ok <- !is.null(int_colData(se)[[md$region_key]]) - if (!ok) msg <- c(msg, paste0( - i, "-th table missing 'region_key' column in 'int_colData'")) - ok <- !is.null(int_colData(se)[[md$instance_key]]) - if (!ok) msg <- c(msg, paste0( - i, "-th table missing 'instance_key' column in 'int_colData'")) - - } - } - na <- setdiff( - unlist(lapply(tables(object), \(.) if (sce(.)) region(.))), - unlist(colnames(object)[setdiff(.LAYERS, "tables")])) # don't flip! - if (length(na)) - msg <- c(msg, paste( - "table region(s) not found in any layer:", - paste(sprintf("'%s'", na), collapse=", "))) - return(msg) -} - -.validateImageArray <- \(object) { - msg <- c() - res <- length(object) - for (k in seq_len(res)) { - x <- data(object, k) - if (length(dim(x)) != 3) msg <- c(msg, paste( - "'ImageArray' resolution", k, "is not 3D")) - if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( - "'ImageArray' resolution", k, "is not of type double or integer")) - } - return(msg) -} -#' @importFrom S4Vectors setValidity2 -setValidity2("ImageArray", .validateImageArray) - -#' @importFrom ZarrArray type -.validateLabelArray <- \(object) { - msg <- c() - res <- length(object) - for (k in seq_len(res)) { - x <- data(object, k) - if (length(dim(x)) != 2) msg <- c(msg, paste( - "'LabelArray' resolution", k, "is not 2D")) - if (type(x) != "integer") msg <- c(msg, paste( - "'LabelArray' resolution", k, "is not of type integer")) - } - return(msg) -} -#' @importFrom S4Vectors setValidity2 -setValidity2("LabelArray", .validateLabelArray) - -.validatePointFrame <- \(object) { - msg <- c() - if (!length(object)) return(msg) - if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") - if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") - return(msg) -} -#' @importFrom S4Vectors setValidity2 -setValidity2("PointFrame", .validatePointFrame) - -.validateShapeFrame <- \(object) { - msg <- c() - if (!nrow(object)) return(msg) - if (!"geometry" %in% names(object)) msg <- c(msg, "'ShapeFrame' missing 'geometry'.") - return(msg) -} -#' @importFrom S4Vectors setValidity2 -setValidity2("ShapeFrame", .validateShapeFrame) - -#' @importFrom methods is -.validateSpatialData <- \(x) { - msg <- c() - typ <- c( - images="ImageArray", - labels="LabelArray", - points="PointFrame", - shapes="ShapeFrame", - tables="SingleCellExperiment") - for (. in names(typ)) if (length(x[[.]])) - if (!all(vapply(x[[.]], \(y) is(y, typ[.]), logical(1)))) - msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) - # TODO: validate .zattrs across all layers - for (y in labels(x)) { - msg <- c(msg, .validateLabelArray(y)) - msg <- c(msg, .validateZattrsLabelArray(y)) - } - for (y in images(x)) msg <- c(msg, .validateImageArray(y)) - for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) - for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) - msg <- c(msg, .validateTables(x)) - return(msg) -} - -#' @importFrom S4Vectors setValidity2 -setValidity2("SpatialData", .validateSpatialData) - -.validateZattrs_multiscales <- \(x, msg) { - if (is.null(ms <- x$multiscales[[1]])) - msg <- c(msg, "missing 'multiscales'") - # MUST contain - for (. in c("axes", "datasets")) - if (is.null(ms[[.]])) - msg <- c(msg, sprintf("missing 'multiscales$%s'", .)) - return(msg) -} -.validateZattrs_axes <- \(x, msg) { - if (!is.list(ax <- x$axes)) - msg <- c(msg, "missing or non-list 'axes'") - ax <- ax[[1]] - if (is.null(ax$name)) - msg <- c(msg, "missing 'axes$name'") - if (!is.null(ts <- ax$type)) - if (!all(ts %in% c("space", "time", "channel"))) - msg <- c(msg, "'axes$type' should be 'space/time/channel'") - return(msg) -} -.validateZattrs_coordTrans <- \(x, msg) { - if (!is.list(ct <- x$coordinateTransformations)) - msg <- c(msg, "missing or non-list 'coordTrans'") - for (i in seq_along(ct)) - for (j in c("input", "output", "type")) - if (is.null(ct[[i]][[j]])) - msg <- c(msg, sprintf("'coordTrans' %s missing '%s'", i, j)) - return(msg) -} -.validateZattrsLabelArray <- \(x) { - msg <- c() - za <- meta(x) - msg <- .validateZattrs_multiscales(za, msg) - ms <- za$multiscales[[1]] - msg <- .validateZattrs_axes(ms, msg) - msg <- .validateZattrs_coordTrans(ms, msg) - return(msg) -} +#' # https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions +#' #' @importFrom SingleCellExperiment int_metadata int_colData +#' .validateTables <- \(object) { +#' msg <- c() +#' sce <- \(.) is(., "SingleCellExperiment") +#' for (i in seq_along(tables(object))) { +#' ok <- sce(se <- table(object, i)) +#' if (!ok) msg <- c(msg, paste0( +#' i, "-th table is not a 'SingleCellExperiment'")) +#' if (!ok) next +#' md <- int_metadata(se)$spatialdata_attrs +#' nm <- c("region", "region_key", "instance_key") +#' .nm <- sprintf("'%s'", paste(nm, collapse="/")) +#' if (any(ok <- nm %in% names(md))) { +#' if (!all(ok)) msg <- c(msg, paste0( +#' i, "-th table missing ", .nm, "; must set all if any")) +#' ok <- all(vapply(md, is.character, logical(1))) +#' if (!ok) msg <- c(msg, paste0( +#' i, "-th table's ", .nm, " is not of type character")) +#' ok <- all(vapply(intersect(md, nm[-1]), length, integer(1)) == 1) +#' if (!ok) msg <- c(msg, paste0( +#' i, "-th table's 'region/instance_key' is not length 1")) +#' ok <- !is.null(int_colData(se)[[md$region_key]]) +#' if (!ok) msg <- c(msg, paste0( +#' i, "-th table missing 'region_key' column in 'int_colData'")) +#' ok <- !is.null(int_colData(se)[[md$instance_key]]) +#' if (!ok) msg <- c(msg, paste0( +#' i, "-th table missing 'instance_key' column in 'int_colData'")) +#' +#' } +#' } +#' na <- setdiff( +#' unlist(lapply(tables(object), \(.) if (sce(.)) region(.))), +#' unlist(colnames(object)[setdiff(.LAYERS, "tables")])) # don't flip! +#' if (length(na)) +#' msg <- c(msg, paste( +#' "table region(s) not found in any layer:", +#' paste(sprintf("'%s'", na), collapse=", "))) +#' return(msg) +#' } +#' +#' .validateImageArray <- \(object) { +#' msg <- c() +#' res <- length(object) +#' for (k in seq_len(res)) { +#' x <- data(object, k) +#' if (length(dim(x)) != 3) msg <- c(msg, paste( +#' "'ImageArray' resolution", k, "is not 3D")) +#' if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( +#' "'ImageArray' resolution", k, "is not of type double or integer")) +#' } +#' return(msg) +#' } +#' #' @importFrom S4Vectors setValidity2 +#' setValidity2("ImageArray", .validateImageArray) +#' +#' #' @importFrom ZarrArray type +#' .validateLabelArray <- \(object) { +#' msg <- c() +#' res <- length(object) +#' for (k in seq_len(res)) { +#' x <- data(object, k) +#' if (length(dim(x)) != 2) msg <- c(msg, paste( +#' "'LabelArray' resolution", k, "is not 2D")) +#' if (type(x) != "integer") msg <- c(msg, paste( +#' "'LabelArray' resolution", k, "is not of type integer")) +#' } +#' return(msg) +#' } +#' #' @importFrom S4Vectors setValidity2 +#' setValidity2("LabelArray", .validateLabelArray) +#' +#' .validatePointFrame <- \(object) { +#' msg <- c() +#' if (!length(object)) return(msg) +#' if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") +#' if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") +#' return(msg) +#' } +#' #' @importFrom S4Vectors setValidity2 +#' setValidity2("PointFrame", .validatePointFrame) +#' +#' .validateShapeFrame <- \(object) { +#' msg <- c() +#' if (!nrow(object)) return(msg) +#' if (!"geometry" %in% names(object)) msg <- c(msg, "'ShapeFrame' missing 'geometry'.") +#' return(msg) +#' } +#' #' @importFrom S4Vectors setValidity2 +#' setValidity2("ShapeFrame", .validateShapeFrame) +#' +#' #' @importFrom methods is +#' .validateSpatialData <- \(x) { +#' msg <- c() +#' typ <- c( +#' images="ImageArray", +#' labels="LabelArray", +#' points="PointFrame", +#' shapes="ShapeFrame", +#' tables="SingleCellExperiment") +#' for (. in names(typ)) if (length(x[[.]])) +#' if (!all(vapply(x[[.]], \(y) is(y, typ[.]), logical(1)))) +#' msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) +#' # TODO: validate .zattrs across all layers +#' for (y in labels(x)) { +#' msg <- c(msg, .validateLabelArray(y)) +#' msg <- c(msg, .validateZattrsLabelArray(y)) +#' } +#' for (y in images(x)) msg <- c(msg, .validateImageArray(y)) +#' for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) +#' for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) +#' msg <- c(msg, .validateTables(x)) +#' return(msg) +#' } +#' +#' #' @importFrom S4Vectors setValidity2 +#' setValidity2("SpatialData", .validateSpatialData) +#' +#' .validateZattrs_multiscales <- \(x, msg) { +#' if (is.null(ms <- x$multiscales[[1]])) +#' msg <- c(msg, "missing 'multiscales'") +#' # MUST contain +#' for (. in c("axes", "datasets")) +#' if (is.null(ms[[.]])) +#' msg <- c(msg, sprintf("missing 'multiscales$%s'", .)) +#' return(msg) +#' } +#' .validateZattrs_axes <- \(x, msg) { +#' if (!is.list(ax <- x$axes)) +#' msg <- c(msg, "missing or non-list 'axes'") +#' ax <- ax[[1]] +#' if (is.null(ax$name)) +#' msg <- c(msg, "missing 'axes$name'") +#' if (!is.null(ts <- ax$type)) +#' if (!all(ts %in% c("space", "time", "channel"))) +#' msg <- c(msg, "'axes$type' should be 'space/time/channel'") +#' return(msg) +#' } +#' .validateZattrs_coordTrans <- \(x, msg) { +#' if (!is.list(ct <- x$coordinateTransformations)) +#' msg <- c(msg, "missing or non-list 'coordTrans'") +#' for (i in seq_along(ct)) +#' for (j in c("input", "output", "type")) +#' if (is.null(ct[[i]][[j]])) +#' msg <- c(msg, sprintf("'coordTrans' %s missing '%s'", i, j)) +#' return(msg) +#' } +#' .validateZattrsLabelArray <- \(x) { +#' msg <- c() +#' za <- meta(x) +#' msg <- .validateZattrs_multiscales(za, msg) +#' ms <- za$multiscales[[1]] +#' msg <- .validateZattrs_axes(ms, msg) +#' msg <- .validateZattrs_coordTrans(ms, msg) +#' return(msg) +#' } diff --git a/man/coord-utils.Rd b/man/CTutils.Rd similarity index 94% rename from man/coord-utils.Rd rename to man/CTutils.Rd index 3bc530f8..190a1ded 100644 --- a/man/coord-utils.Rd +++ b/man/CTutils.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/coord_utils.R -\name{coord-utils} -\alias{coord-utils} +% Please edit documentation in R/CTutils.R +\name{CTutils} +\alias{CTutils} \alias{axes} \alias{CTlist} \alias{CTname} @@ -24,7 +24,7 @@ \alias{rmvCT,Zattrs-method} \alias{addCT,SpatialDataElement-method} \alias{addCT,Zattrs-method} -\title{Coordinate transformations} +\title{Coord. trans. utilities} \usage{ \S4method{axes}{Zattrs}(x, ...) @@ -74,7 +74,7 @@ element type (e.g., numeric(1) for rotation, numeric(2) for scaling in 2D)} \item{j}{character string; name of target coordinate space.} } \description{ -Coordinate transformations +Coord. trans. utilities } \examples{ x <- file.path("extdata", "blobs.zarr") diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 2eb34b55..5c16f970 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -50,8 +50,8 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY-method} -\alias{[[<-,SpatialData,character,ANY-method} +\alias{[[<-,SpatialData,numeric,ANY,ANY-method} +\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -88,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} From de2ad434b037581ff978cac32e5341c83a2235be Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 18:39:28 +0200 Subject: [PATCH 134/151] Zarr v3 support --- DESCRIPTION | 2 +- NAMESPACE | 3 + R/CTutils.R | 10 +- R/ImageArray.R | 6 +- R/Zattrs.R | 7 +- R/validity.R | 311 +++++++++--------- inst/NEWS | 6 + tests/testthat/test-PointFrame.R | 3 +- tests/testthat/test-ctgraph.R | 55 ++++ tests/testthat/test-ctutils.R | 128 +++++++ .../testthat/{test-reading.R => test-read.R} | 0 tests/testthat/test-zattrs.R | 255 +++----------- 12 files changed, 416 insertions(+), 370 deletions(-) create mode 100644 tests/testthat/test-ctgraph.R create mode 100644 tests/testthat/test-ctutils.R rename tests/testthat/{test-reading.R => test-read.R} (100%) diff --git a/DESCRIPTION b/DESCRIPTION index 8fbcafc8..67c47bd1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: SpatialData Title: Representation of Python's SpatialData in R Depends: R (>= 4.6) -Version: 0.99.28 +Version: 0.99.29 Description: Interface to Python's 'SpatialData', currently including: reticulate-based use of 'spatialdata-io' for reading of manufacturer data and writing to .zarr, on-disk representation of images/labels as diff --git a/NAMESPACE b/NAMESPACE index 87098855..449d3f02 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -116,6 +116,7 @@ importFrom(S4Vectors,coolcat) importFrom(S4Vectors,isSequence) importFrom(S4Vectors,make_zero_col_DFrame) importFrom(S4Vectors,metadata) +importFrom(S4Vectors,setValidity2) importFrom(SingleCellExperiment,"int_colData<-") importFrom(SingleCellExperiment,"int_metadata<-") importFrom(SingleCellExperiment,SingleCellExperiment) @@ -129,6 +130,7 @@ importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) +importFrom(ZarrArray,type) importFrom(anndataR,read_zarr) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) @@ -155,6 +157,7 @@ importFrom(graph,nodeData) importFrom(graph,nodes) importFrom(methods,as) importFrom(methods,callNextMethod) +importFrom(methods,is) importFrom(methods,new) importFrom(methods,setClassUnion) importFrom(methods,setReplaceMethod) diff --git a/R/CTutils.R b/R/CTutils.R index a8b0eb01..04339650 100644 --- a/R/CTutils.R +++ b/R/CTutils.R @@ -59,7 +59,7 @@ setMethod("CTlist", "Zattrs", \(x, ...) { ms <- multiscales(x) ct <- "coordinateTransformations" if (is.null(ms)) return(x[[ct]]) - ms[[1]][[ct]][[1]] + ms[[1]][[ct]] }) #' @rdname CTutils @@ -84,11 +84,15 @@ setMethod("CTdata", "Zattrs", \(x, i=1, ...) { #' @rdname CTutils #' @export -setMethod("CTtype", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$type, character(1))) +setMethod("CTtype", "Zattrs", \(x, ...) { + vapply(CTlist(x), \(.) .$type, character(1)) +}) #' @rdname CTutils #' @export -setMethod("CTname", "Zattrs", \(x, ...) vapply(CTlist(x), \(.) .$output$name, character(1))) +setMethod("CTname", "Zattrs", \(x, ...) { + vapply(CTlist(x), \(.) .$output$name, character(1)) +}) #' @rdname CTutils #' @export diff --git a/R/ImageArray.R b/R/ImageArray.R index 92936c96..4859976e 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -31,7 +31,11 @@ ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), ...) { #' @rdname ImageArray #' @aliases channels #' @export -setMethod("channels", "Zattrs", \(x, ...) unlist(x$omero$channels)) +setMethod("channels", "Zattrs", \(x, ...) { + v <- x$spatialdata_attrs$version + if (v == "0.3") x <- x$ome + unlist(x$omero$channels) +}) #' @rdname ImageArray #' @aliases channels diff --git a/R/Zattrs.R b/R/Zattrs.R index 558cd859..3b793da5 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -40,11 +40,10 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) # internal use only! #' @noRd -setMethod("multiscales", "Zattrs", \(x) { +setMethod("multiscales", "list", \(x) { v <- x$spatialdata_attrs$version - switch(v, - "0.2"=x$multiscales, - "0.3"=x$ome$multiscales) + if (is.null(v)) stop("couldn't find 'version' in 'spatialdata_attrs'") + switch(v, "0.3"=x$ome$multiscales, x$multiscales) }) .showZattrs <- function(object) { diff --git a/R/validity.R b/R/validity.R index be41841d..f30ed209 100644 --- a/R/validity.R +++ b/R/validity.R @@ -1,156 +1,155 @@ -#' # https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions -#' #' @importFrom SingleCellExperiment int_metadata int_colData -#' .validateTables <- \(object) { -#' msg <- c() -#' sce <- \(.) is(., "SingleCellExperiment") -#' for (i in seq_along(tables(object))) { -#' ok <- sce(se <- table(object, i)) -#' if (!ok) msg <- c(msg, paste0( -#' i, "-th table is not a 'SingleCellExperiment'")) -#' if (!ok) next -#' md <- int_metadata(se)$spatialdata_attrs -#' nm <- c("region", "region_key", "instance_key") -#' .nm <- sprintf("'%s'", paste(nm, collapse="/")) -#' if (any(ok <- nm %in% names(md))) { -#' if (!all(ok)) msg <- c(msg, paste0( -#' i, "-th table missing ", .nm, "; must set all if any")) -#' ok <- all(vapply(md, is.character, logical(1))) -#' if (!ok) msg <- c(msg, paste0( -#' i, "-th table's ", .nm, " is not of type character")) -#' ok <- all(vapply(intersect(md, nm[-1]), length, integer(1)) == 1) -#' if (!ok) msg <- c(msg, paste0( -#' i, "-th table's 'region/instance_key' is not length 1")) -#' ok <- !is.null(int_colData(se)[[md$region_key]]) -#' if (!ok) msg <- c(msg, paste0( -#' i, "-th table missing 'region_key' column in 'int_colData'")) -#' ok <- !is.null(int_colData(se)[[md$instance_key]]) -#' if (!ok) msg <- c(msg, paste0( -#' i, "-th table missing 'instance_key' column in 'int_colData'")) -#' -#' } -#' } -#' na <- setdiff( -#' unlist(lapply(tables(object), \(.) if (sce(.)) region(.))), -#' unlist(colnames(object)[setdiff(.LAYERS, "tables")])) # don't flip! -#' if (length(na)) -#' msg <- c(msg, paste( -#' "table region(s) not found in any layer:", -#' paste(sprintf("'%s'", na), collapse=", "))) -#' return(msg) -#' } -#' -#' .validateImageArray <- \(object) { -#' msg <- c() -#' res <- length(object) -#' for (k in seq_len(res)) { -#' x <- data(object, k) -#' if (length(dim(x)) != 3) msg <- c(msg, paste( -#' "'ImageArray' resolution", k, "is not 3D")) -#' if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( -#' "'ImageArray' resolution", k, "is not of type double or integer")) -#' } -#' return(msg) -#' } -#' #' @importFrom S4Vectors setValidity2 -#' setValidity2("ImageArray", .validateImageArray) -#' -#' #' @importFrom ZarrArray type -#' .validateLabelArray <- \(object) { -#' msg <- c() -#' res <- length(object) -#' for (k in seq_len(res)) { -#' x <- data(object, k) -#' if (length(dim(x)) != 2) msg <- c(msg, paste( -#' "'LabelArray' resolution", k, "is not 2D")) -#' if (type(x) != "integer") msg <- c(msg, paste( -#' "'LabelArray' resolution", k, "is not of type integer")) -#' } -#' return(msg) -#' } -#' #' @importFrom S4Vectors setValidity2 -#' setValidity2("LabelArray", .validateLabelArray) -#' -#' .validatePointFrame <- \(object) { -#' msg <- c() -#' if (!length(object)) return(msg) -#' if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") -#' if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") -#' return(msg) -#' } -#' #' @importFrom S4Vectors setValidity2 -#' setValidity2("PointFrame", .validatePointFrame) -#' -#' .validateShapeFrame <- \(object) { -#' msg <- c() -#' if (!nrow(object)) return(msg) -#' if (!"geometry" %in% names(object)) msg <- c(msg, "'ShapeFrame' missing 'geometry'.") -#' return(msg) -#' } -#' #' @importFrom S4Vectors setValidity2 -#' setValidity2("ShapeFrame", .validateShapeFrame) -#' -#' #' @importFrom methods is -#' .validateSpatialData <- \(x) { -#' msg <- c() -#' typ <- c( -#' images="ImageArray", -#' labels="LabelArray", -#' points="PointFrame", -#' shapes="ShapeFrame", -#' tables="SingleCellExperiment") -#' for (. in names(typ)) if (length(x[[.]])) -#' if (!all(vapply(x[[.]], \(y) is(y, typ[.]), logical(1)))) -#' msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) -#' # TODO: validate .zattrs across all layers -#' for (y in labels(x)) { -#' msg <- c(msg, .validateLabelArray(y)) -#' msg <- c(msg, .validateZattrsLabelArray(y)) -#' } -#' for (y in images(x)) msg <- c(msg, .validateImageArray(y)) -#' for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) -#' for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) -#' msg <- c(msg, .validateTables(x)) -#' return(msg) -#' } -#' -#' #' @importFrom S4Vectors setValidity2 -#' setValidity2("SpatialData", .validateSpatialData) -#' -#' .validateZattrs_multiscales <- \(x, msg) { -#' if (is.null(ms <- x$multiscales[[1]])) -#' msg <- c(msg, "missing 'multiscales'") -#' # MUST contain -#' for (. in c("axes", "datasets")) -#' if (is.null(ms[[.]])) -#' msg <- c(msg, sprintf("missing 'multiscales$%s'", .)) -#' return(msg) -#' } -#' .validateZattrs_axes <- \(x, msg) { -#' if (!is.list(ax <- x$axes)) -#' msg <- c(msg, "missing or non-list 'axes'") -#' ax <- ax[[1]] -#' if (is.null(ax$name)) -#' msg <- c(msg, "missing 'axes$name'") -#' if (!is.null(ts <- ax$type)) -#' if (!all(ts %in% c("space", "time", "channel"))) -#' msg <- c(msg, "'axes$type' should be 'space/time/channel'") -#' return(msg) -#' } -#' .validateZattrs_coordTrans <- \(x, msg) { -#' if (!is.list(ct <- x$coordinateTransformations)) -#' msg <- c(msg, "missing or non-list 'coordTrans'") -#' for (i in seq_along(ct)) -#' for (j in c("input", "output", "type")) -#' if (is.null(ct[[i]][[j]])) -#' msg <- c(msg, sprintf("'coordTrans' %s missing '%s'", i, j)) -#' return(msg) -#' } -#' .validateZattrsLabelArray <- \(x) { -#' msg <- c() -#' za <- meta(x) -#' msg <- .validateZattrs_multiscales(za, msg) -#' ms <- za$multiscales[[1]] -#' msg <- .validateZattrs_axes(ms, msg) -#' msg <- .validateZattrs_coordTrans(ms, msg) -#' return(msg) -#' } +# https://spatialdata.scverse.org/en/latest/design_doc.html#table-table-of-annotations-for-regions +#' @importFrom SingleCellExperiment int_metadata int_colData +.validateTables <- \(object) { + msg <- c() + sce <- \(.) is(., "SingleCellExperiment") + for (i in seq_along(tables(object))) { + ok <- sce(se <- table(object, i)) + if (!ok) msg <- c(msg, paste0( + i, "-th table is not a 'SingleCellExperiment'")) + if (!ok) next + md <- int_metadata(se)$spatialdata_attrs + nm <- c("region", "region_key", "instance_key") + .nm <- sprintf("'%s'", paste(nm, collapse="/")) + if (any(ok <- nm %in% names(md))) { + if (!all(ok)) msg <- c(msg, paste0( + i, "-th table missing ", .nm, "; must set all if any")) + ok <- all(vapply(md, is.character, logical(1))) + if (!ok) msg <- c(msg, paste0( + i, "-th table's ", .nm, " is not of type character")) + ok <- all(vapply(intersect(md, nm[-1]), length, integer(1)) == 1) + if (!ok) msg <- c(msg, paste0( + i, "-th table's 'region/instance_key' is not length 1")) + ok <- !is.null(int_colData(se)[[md$region_key]]) + if (!ok) msg <- c(msg, paste0( + i, "-th table missing 'region_key' column in 'int_colData'")) + ok <- !is.null(int_colData(se)[[md$instance_key]]) + if (!ok) msg <- c(msg, paste0( + i, "-th table missing 'instance_key' column in 'int_colData'")) + + } + } + na <- setdiff( + unlist(lapply(tables(object), \(.) if (sce(.)) region(.))), + unlist(colnames(object)[setdiff(.LAYERS, "tables")])) # don't flip! + if (length(na)) + msg <- c(msg, paste( + "table region(s) not found in any layer:", + paste(sprintf("'%s'", na), collapse=", "))) + return(msg) +} + +.validateImageArray <- \(object) { + msg <- c() + res <- length(object) + for (k in seq_len(res)) { + x <- data(object, k) + if (length(dim(x)) != 3) msg <- c(msg, paste( + "'ImageArray' resolution", k, "is not 3D")) + if (!type(x) %in% c("double", "integer")) msg <- c(msg, paste( + "'ImageArray' resolution", k, "is not of type double or integer")) + } + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("ImageArray", .validateImageArray) + +#' @importFrom ZarrArray type +.validateLabelArray <- \(object) { + msg <- c() + res <- length(object) + for (k in seq_len(res)) { + x <- data(object, k) + if (length(dim(x)) != 2) msg <- c(msg, paste( + "'LabelArray' resolution", k, "is not 2D")) + if (type(x) != "integer") msg <- c(msg, paste( + "'LabelArray' resolution", k, "is not of type integer")) + } + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("LabelArray", .validateLabelArray) + +.validatePointFrame <- \(object) { + msg <- c() + if (!length(object)) return(msg) + if (!"x" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'x'.") + if (!"y" %in% names(object)) msg <- c(msg, "'PointFrame' missing 'y'.") + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("PointFrame", .validatePointFrame) + +.validateShapeFrame <- \(object) { + msg <- c() + if (!nrow(object)) return(msg) + if (!"geometry" %in% names(object)) msg <- c(msg, "'ShapeFrame' missing 'geometry'.") + return(msg) +} +#' @importFrom S4Vectors setValidity2 +setValidity2("ShapeFrame", .validateShapeFrame) + +#' @importFrom methods is +.validateSpatialData <- \(x) { + msg <- c() + typ <- c( + images="ImageArray", + labels="LabelArray", + points="PointFrame", + shapes="ShapeFrame", + tables="SingleCellExperiment") + for (. in names(typ)) if (length(x[[.]])) + if (!all(vapply(x[[.]], \(y) is(y, typ[.]), logical(1)))) + msg <- c(msg, sprintf("'%s' should be a list of '%s'", ., typ[.])) + # TODO: validate .zattrs across all layers + for (y in labels(x)) msg <- c(msg, .validateLabelArray(y)) + for (y in images(x)) msg <- c(msg, .validateImageArray(y)) + for (y in points(x)) msg <- c(msg, .validatePointFrame(y)) + for (y in shapes(x)) msg <- c(msg, .validateShapeFrame(y)) + msg <- c(msg, .validateTables(x)) + return(msg) +} + +#' @importFrom S4Vectors setValidity2 +setValidity2("SpatialData", .validateSpatialData) + +# TODO: version-specific .zattrs validation for all layers + +.validateZattrs_multiscales <- \(x, msg) { + if (is.null(ms <- x$multiscales[[1]])) + msg <- c(msg, "missing 'multiscales'") + # MUST contain + for (. in c("axes", "datasets")) + if (is.null(ms[[.]])) + msg <- c(msg, sprintf("missing 'multiscales$%s'", .)) + return(msg) +} +.validateZattrs_axes <- \(x, msg) { + if (!is.list(ax <- x$axes)) + msg <- c(msg, "missing or non-list 'axes'") + ax <- ax[[1]] + if (is.null(ax$name)) + msg <- c(msg, "missing 'axes$name'") + if (!is.null(ts <- ax$type)) + if (!all(ts %in% c("space", "time", "channel"))) + msg <- c(msg, "'axes$type' should be 'space/time/channel'") + return(msg) +} +.validateZattrs_coordTrans <- \(x, msg) { + if (!is.list(ct <- x$coordinateTransformations)) + msg <- c(msg, "missing or non-list 'coordTrans'") + for (i in seq_along(ct)) + for (j in c("input", "output", "type")) + if (is.null(ct[[i]][[j]])) + msg <- c(msg, sprintf("'coordTrans' %s missing '%s'", i, j)) + return(msg) +} +.validateZattrsLabelArray <- \(x) { + msg <- c() + za <- meta(x) + msg <- .validateZattrs_multiscales(za, msg) + ms <- za$multiscales[[1]] + msg <- .validateZattrs_axes(ms, msg) + msg <- .validateZattrs_coordTrans(ms, msg) + return(msg) +} diff --git a/inst/NEWS b/inst/NEWS index 155ca6fa..57d86200 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,9 @@ +changes in version 0.99.29 + +- revision of Zarr version-specific .zattrs handling +- added Zarr v3 example dataset 'inst/extradata/blobs_v3' +- reorganization of unit tests to facilitate v3-specific testing + changes in version 0.99.28 - validity checks for 'table' elements diff --git a/tests/testthat/test-PointFrame.R b/tests/testthat/test-PointFrame.R index d9397525..c8f089a1 100644 --- a/tests/testthat/test-PointFrame.R +++ b/tests/testthat/test-PointFrame.R @@ -37,7 +37,8 @@ test_that("filter", { n <- length(p <- point(x)) expect_length(filter(p), n) expect_length(filter(p, x > Inf), 0) - expect_error(filter(p, z == 1)) + f <- \() filter(p, z == 1) + expect_error(show(f())) }) test_that("select", { diff --git a/tests/testthat/test-ctgraph.R b/tests/testthat/test-ctgraph.R new file mode 100644 index 00000000..b230d71a --- /dev/null +++ b/tests/testthat/test-ctgraph.R @@ -0,0 +1,55 @@ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, anndataR=TRUE) + +test_that("CTgraph", { + # invalid + expect_error(CTgraph(list())) + expect_error(CTgraph(SpatialData::table(x))) + # object-wide + g <- CTgraph(x) + expect_is(g, "graph") + # graph should contain node for + # every element & transformation + ns <- lapply(setdiff(SpatialData:::.LAYERS, "tables"), + \(l) lapply(names(x[[l]]), + \(e) c(e, CTname(x[[l]][[e]])))) + ns <- sort(unique(unlist(ns))) + expect_true(all(ns %in% sort(graph::nodes(g)))) + # element-wise + for (l in setdiff(SpatialData:::.LAYERS, "tables")) + for (e in names(x[[l]])) { + y <- x[[l]][[e]] + g <- CTgraph(y) + expect_is(g, "graph") + expect_true("self" %in% graph::nodes(g)) + } +}) + +test_that("CTpath", { + i <- "blobs_image" + y <- element(x, "images", i) + z <- CTpath(y, j <- CTname(y)) + expect_identical(CTpath(x, i, j), z) + expect_is(z, "list") + expect_length(z <- z[[1]], 2) + expect_setequal(names(z), c("type", "data")) + expect_is(z$type, "character") + expect_length(z$type, 1) +}) + +test_that("CTplot", { + f <- function(.) { + tf <- tempfile(fileext=".pdf") + on.exit(unlink(tf)) + pdf(tf); .; dev.off() + file.size(tf) + } + g <- CTgraph(x) + p <- f(CTplot(g)) + expect_is(p, "numeric") + expect_true(p > f(plot(1))) + p <- f(CTplot(g, 0.1)) + q <- f(CTplot(g, 0.9)) + expect_true(p < q) +}) diff --git a/tests/testthat/test-ctutils.R b/tests/testthat/test-ctutils.R new file mode 100644 index 00000000..4f61b212 --- /dev/null +++ b/tests/testthat/test-ctutils.R @@ -0,0 +1,128 @@ +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- readSpatialData(x, anndataR=TRUE) + +.CTtype <- c( + "identity", "scale", "rotate", + "translation", "affine", "sequence") + +test_that("CTlist", { + y <- CTlist(label(x)) + expect_is(y, "list") + expect_length(y, 5) + z <- Reduce(intersect, lapply(y, names)) + expect_setequal(z, c("input", "output", "type")) + z <- vapply(y, \(.) .$type, character(1)) + expect_true(all(z %in% .CTtype)) +}) +test_that("CTdata", { + # invalid + expect_error(CTdata(label(x), "")) + expect_error(CTdata(label(x), 99)) + expect_error(CTdata(label(x), Inf)) + expect_error(CTdata(label(x), TRUE)) + # identity + y <- CTdata(label(x), "global") + expect_null(y) + # scale + y <- CTdata(label(x), "scale") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + expect_true(all(unlist(y) > 0)) + # translation + y <- CTdata(label(x), "translation") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + # affine + y <- CTdata(label(x), "affine") + expect_is(y, "list") + expect_length(y, 2) + expect_is(unlist(y), "numeric") + expect_true(all(unlist(y) > 0)) + z <- vapply(y, length, integer(1)) + expect_true(all(z == 3)) + # sequence + y <- CTdata(label(x), "sequence") + expect_is(y, "list") + expect_length(y, 2) + expect_true(all(names(y) %in% .CTtype)) + z <- vapply(y, length, integer(1)) + expect_true(all(z == 2)) +}) +test_that("CTtype", { + y <- CTtype(label(x)) + expect_is(y, "character") + expect_length(y, 5) + expect_true(all(y %in% .CTtype)) +}) +test_that("CTname,element", { + y <- CTname(label(x)) + expect_is(y, "character") + expect_length(y, 5) + expect_true(all(nchar(y) > 0)) + expect_true(!any(duplicated(y))) +}) +test_that("CTname,object", { + y <- CTname(x) + expect_is(y, "character") + expect_true(!any(duplicated(y))) + y <- CTname(image(x)) + z <- CTname(meta(image(x))) + expect_is(y, "character") + expect_length(y, 1) + expect_identical(y, z) +}) + +test_that("rmvCT", { + y <- label(x) + # invalid index/name + expect_error(rmvCT(y, 100)) + expect_error(rmvCT(y, ".")) + expect_error(rmvCT(y, c(".", CTname(y)[1]))) + # identity is kept with a warning + expect_warning(z <- rmvCT(y, "global")) + expect_identical(CTname(z), CTname(y)) + # by name + i <- sample(setdiff(CTname(y), "global"), 2) + expect_identical(CTname(rmvCT(y, i)), setdiff(CTname(y), i)) + # by index + i <- sample(which(CTtype(y) != "identity"), 2) + expect_identical(CTname(rmvCT(y, i)), CTname(y)[-i]) +}) + +test_that("addCT", { + # get 1st element from each layer + ls <- setdiff(SpatialData:::.LAYERS, "tables") + es <- lapply(ls, \(.) x[.,1][[.]][[1]]) + .check_data <- \(z, x) { + expect_true("." %in% CTname(z)) + ct <- CTlist(z)[[which(CTname(z) == ".")]] + expect_identical(ct[[t]][[1]], x) + } + for (y in es) { + t <- "identity" + expect_error(addCT(y, ".", t, 12345)) + expect_silent(z <- addCT(y, ".", t, v <- NULL)) + .check_data(z, v) + t <- "rotate" + expect_error(addCT(y, ".", t, -12345)) # negative + expect_error(addCT(y, ".", t, c(1,1))) # too many + expect_error(addCT(y, ".", t, ".")) # not a number + expect_silent(z <- addCT(y, ".", t, v <- 1)) + .check_data(z, v) + t <- "scale" + d <- ifelse(is(y, "ImageArray"), 3, 2) + expect_error(addCT(y, ".", t, numeric(d))) # zeroes + expect_error(addCT(y, ".", t, 1+numeric(d+1))) # too many + expect_error(addCT(y, ".", t, character(d))) # not a number + expect_silent(z <- addCT(y, ".", t, v <- 1+numeric(d))) + .check_data(z, v) + t <- "translation" + expect_error(addCT(y, ".", t, numeric(d+1))) # too many + expect_error(addCT(y, ".", t, character(d))) # not a number + expect_silent(z <- addCT(y, ".", t, v <- numeric(d))) + .check_data(z, v) + } +}) diff --git a/tests/testthat/test-reading.R b/tests/testthat/test-read.R similarity index 100% rename from tests/testthat/test-reading.R rename to tests/testthat/test-read.R diff --git a/tests/testthat/test-zattrs.R b/tests/testthat/test-zattrs.R index 8321fd26..ccfc0edb 100644 --- a/tests/testthat/test-zattrs.R +++ b/tests/testthat/test-zattrs.R @@ -1,204 +1,51 @@ -x <- file.path("extdata", "blobs.zarr") -x <- system.file(x, package="SpatialData") -x <- readSpatialData(x, anndataR=TRUE) - -test_that("axes", { - # image - y <- axes(image(x)) - expect_is(y, "list") - expect_length(y, 3) - # label - y <- axes(label(x)) - expect_is(y, "list") - expect_length(y, 2) - # shape - y <- axes(shape(x)) - expect_is(y, "list") - expect_length(y, 2) - expect_equal(unlist(y), c("x", "y")) - # point - y <- axes(point(x)) - expect_is(y, "list") - expect_length(y, 2) - expect_equal(unlist(y), c("x", "y")) - # missing - y <- image(x) - y@meta$multiscales[[1]]$axes <- NULL - expect_error(axes(y)) -}) - -.CTtype <- c("identity", "scale", "rotate", "translation", "affine", "sequence") - -test_that("CTlist", { - y <- CTlist(label(x)) - expect_is(y, "list") - expect_length(y, 5) - z <- Reduce(intersect, lapply(y, names)) - expect_setequal(z, c("input", "output", "type")) - z <- vapply(y, \(.) .$type, character(1)) - expect_true(all(z %in% .CTtype)) -}) -test_that("CTdata", { - # invalid - expect_error(CTdata(label(x), "")) - expect_error(CTdata(label(x), 99)) - expect_error(CTdata(label(x), Inf)) - expect_error(CTdata(label(x), TRUE)) - # identity - y <- CTdata(label(x), "global") - expect_null(y) - # scale - y <- CTdata(label(x), "scale") - expect_is(y, "list") - expect_length(y, 2) - expect_is(unlist(y), "numeric") - expect_true(all(unlist(y) > 0)) - # translation - y <- CTdata(label(x), "translation") - expect_is(y, "list") - expect_length(y, 2) - expect_is(unlist(y), "numeric") - # affine - y <- CTdata(label(x), "affine") - expect_is(y, "list") - expect_length(y, 2) - expect_is(unlist(y), "numeric") - expect_true(all(unlist(y) > 0)) - z <- vapply(y, length, integer(1)) - expect_true(all(z == 3)) - # sequence - y <- CTdata(label(x), "sequence") - expect_is(y, "list") - expect_length(y, 2) - expect_true(all(names(y) %in% .CTtype)) - z <- vapply(y, length, integer(1)) - expect_true(all(z == 2)) -}) -test_that("CTtype", { - y <- CTtype(label(x)) - expect_is(y, "character") - expect_length(y, 5) - expect_true(all(y %in% .CTtype)) -}) -test_that("CTname", { - y <- CTname(label(x)) - expect_is(y, "character") - expect_length(y, 5) - expect_true(all(nchar(y) > 0)) - expect_true(!any(duplicated(y))) -}) - -test_that("rmvCT", { - y <- label(x) - # invalid index/name - expect_error(rmvCT(y, 100)) - expect_error(rmvCT(y, ".")) - expect_error(rmvCT(y, c(".", CTname(y)[1]))) - # identity is kept with a warning - expect_warning(z <- rmvCT(y, "global")) - expect_identical(CTname(z), CTname(y)) - # by name - i <- sample(setdiff(CTname(y), "global"), 2) - expect_identical(CTname(rmvCT(y, i)), setdiff(CTname(y), i)) - # by index - i <- sample(which(CTtype(y) != "identity"), 2) - expect_identical(CTname(rmvCT(y, i)), CTname(y)[-i]) -}) - -test_that("addCT", { - # get 1st element from each layer - ls <- setdiff(SpatialData:::.LAYERS, "tables") - es <- lapply(ls, \(.) x[.,1][[.]][[1]]) - .check_data <- \(z, x) { - expect_true("." %in% CTname(z)) - ct <- CTlist(z)[[which(CTname(z) == ".")]] - expect_identical(ct[[t]][[1]], x) - } - for (y in es) { - t <- "identity" - expect_error(addCT(y, ".", t, 12345)) - expect_silent(z <- addCT(y, ".", t, v <- NULL)) - .check_data(z, v) - t <- "rotate" - expect_error(addCT(y, ".", t, -12345)) # negative - expect_error(addCT(y, ".", t, c(1,1))) # too many - expect_error(addCT(y, ".", t, ".")) # not a number - expect_silent(z <- addCT(y, ".", t, v <- 1)) - .check_data(z, v) - t <- "scale" - d <- ifelse(is(y, "ImageArray"), 3, 2) - expect_error(addCT(y, ".", t, numeric(d))) # zeroes - expect_error(addCT(y, ".", t, 1+numeric(d+1))) # too many - expect_error(addCT(y, ".", t, character(d))) # not a number - expect_silent(z <- addCT(y, ".", t, v <- 1+numeric(d))) - .check_data(z, v) - t <- "translation" - expect_error(addCT(y, ".", t, numeric(d+1))) # too many - expect_error(addCT(y, ".", t, character(d))) # not a number - expect_silent(z <- addCT(y, ".", t, v <- numeric(d))) - .check_data(z, v) - } -}) - -test_that("CTname", { - y <- CTname(x) - expect_is(y, "character") - expect_true(!any(duplicated(y))) - y <- CTname(image(x)) - z <- CTname(meta(image(x))) - expect_is(y, "character") - expect_length(y, 1) - expect_identical(y, z) -}) - -test_that("CTgraph", { - # invalid - expect_error(CTgraph(list())) - expect_error(CTgraph(SpatialData::table(x))) - # object-wide - g <- CTgraph(x) - expect_is(g, "graph") - # graph should contain node for - # every element & transformation - ns <- lapply(setdiff(SpatialData:::.LAYERS, "tables"), - \(l) lapply(names(x[[l]]), - \(e) c(e, CTname(x[[l]][[e]])))) - ns <- sort(unique(unlist(ns))) - expect_true(all(ns %in% sort(graph::nodes(g)))) - # element-wise - for (l in setdiff(SpatialData:::.LAYERS, "tables")) - for (e in names(x[[l]])) { - y <- x[[l]][[e]] - g <- CTgraph(y) - expect_is(g, "graph") - expect_true("self" %in% graph::nodes(g)) - } -}) - -test_that("CTpath", { - i <- "blobs_image" - y <- element(x, "images", i) - z <- CTpath(y, j <- CTname(y)) - expect_identical(CTpath(x, i, j), z) - expect_is(z, "list") - expect_length(z <- z[[1]], 2) - expect_setequal(names(z), c("type", "data")) - expect_is(z$type, "character") - expect_length(z$type, 1) -}) - -test_that("CTplot", { - f <- function(.) { - tf <- tempfile(fileext=".pdf") - on.exit(unlink(tf)) - pdf(tf); .; dev.off() - file.size(tf) - } - g <- CTgraph(x) - p <- f(CTplot(g)) - expect_is(p, "numeric") - expect_true(p > f(plot(1))) - p <- f(CTplot(g, 0.1)) - q <- f(CTplot(g, 0.9)) - expect_true(p < q) -}) +z <- list(v1="blobs.zarr", v3="blobs_v3.zarr") + +for (v in names(z)) { + + x <- file.path("extdata", z[[v]]) + x <- system.file(x, package="SpatialData") + x <- readSpatialData(x, anndataR=TRUE) + + test_that(paste0(v, "-multiscales"), { + y <- meta(image(x)) + z <- multiscales(y) + expect_is(z, "list") + expect_length(z, 1) + y$spatialdata_attrs <- NULL + expect_error(multiscales(y)) + }) + + test_that(paste0(v, "-axes"), { + # image + y <- axes(image(x)) + expect_is(y, "list") + expect_length(y, 3) + # label + y <- axes(label(x)) + expect_is(y, "list") + expect_length(y, 2) + # shape + y <- axes(shape(x)) + expect_is(y, "list") + expect_length(y, 2) + expect_equal(unlist(y), c("x", "y")) + # point + y <- axes(point(x)) + expect_is(y, "list") + expect_length(y, 2) + expect_equal(unlist(y), c("x", "y")) + # missing + y <- image(x) + switch(v, + "v3"=y@meta$ome$multiscales[[1]]$axes <- NULL, + y@meta$multiscales[[1]]$axes <- NULL) + expect_error(axes(y)) + }) + + test_that(paste0(v, "-channels"), { + expect_error(channels(label(x))) + expect_silent(z <- channels(y <- image(x))) + expect_length(z, dim(y)[1]) + }) + +} \ No newline at end of file From 0c367a1f3f05e432de5509251c2facf83ed6b47d Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 18:43:35 +0200 Subject: [PATCH 135/151] rmv scratch script --- NAMESPACE | 2 -- 1 file changed, 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 449d3f02..b0081863 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -122,7 +122,6 @@ importFrom(SingleCellExperiment,"int_metadata<-") importFrom(SingleCellExperiment,SingleCellExperiment) importFrom(SingleCellExperiment,int_colData) importFrom(SingleCellExperiment,int_metadata) -importFrom(SpatialExperiment,SpatialExperiment) importFrom(SummarizedExperiment,"assay<-") importFrom(SummarizedExperiment,"assayNames<-") importFrom(SummarizedExperiment,"colData<-") @@ -131,7 +130,6 @@ importFrom(SummarizedExperiment,colData) importFrom(ZarrArray,ZarrArray) importFrom(ZarrArray,path) importFrom(ZarrArray,type) -importFrom(anndataR,read_zarr) importFrom(arrow,open_dataset) importFrom(basilisk,BasiliskEnvironment) importFrom(basilisk,basiliskRun) From ee41039884ecad6d74b2a75d838bc42836bf1cbc Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 18:46:09 +0200 Subject: [PATCH 136/151] fix R CMD check warnings --- inst/NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/NEWS b/inst/NEWS index 57d86200..18c4e5df 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,7 +1,7 @@ changes in version 0.99.29 - revision of Zarr version-specific .zattrs handling -- added Zarr v3 example dataset 'inst/extradata/blobs_v3' +- added Zarr v3 example dataset 'inst/extdata/blobs_v3' - reorganization of unit tests to facilitate v3-specific testing changes in version 0.99.28 From 56ee8a77d56ee1d585d1f3867da48366931f3166 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 18:47:40 +0200 Subject: [PATCH 137/151] protect against missing sf version field --- R/ImageArray.R | 1 + R/Zattrs.R | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/R/ImageArray.R b/R/ImageArray.R index 4859976e..58539966 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -33,6 +33,7 @@ ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), ...) { #' @export setMethod("channels", "Zattrs", \(x, ...) { v <- x$spatialdata_attrs$version + if (!length(v)) stop("couldn't find 'version' in 'spatialdata_attrs'") if (v == "0.3") x <- x$ome unlist(x$omero$channels) }) diff --git a/R/Zattrs.R b/R/Zattrs.R index 48910d07..43f9982d 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -40,7 +40,7 @@ setMethod("$", "Zattrs", \(x, name) x[[name]]) #' @noRd setMethod("multiscales", "list", \(x) { v <- x$spatialdata_attrs$version - if (is.null(v)) stop("couldn't find 'version' in 'spatialdata_attrs'") + if (!length(v)) stop("couldn't find 'version' in 'spatialdata_attrs'") switch(v, "0.3"=x$ome$multiscales, x$multiscales) }) From 79f28253e63298b9908e0af3080513db7d25aca2 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:03:31 +0200 Subject: [PATCH 138/151] fix check warnings --- R/CTutils.R | 1 - R/ShapeFrame.R | 6 ++++-- R/Zattrs.R | 1 + R/query.R | 4 ++-- man/CTutils.Rd | 2 -- man/SDattrs.Rd | 3 +++ man/ShapeFrame.Rd | 7 +++++-- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/R/CTutils.R b/R/CTutils.R index 04339650..c1e06576 100644 --- a/R/CTutils.R +++ b/R/CTutils.R @@ -5,7 +5,6 @@ #' @param x \code{SpatialData}, an element, or \code{Zattrs}. #' @param i for \code{CTpath}, source node label; else, string or #' scalar integer giving the name or index of a coordinate space. -#' @param j character string; name of target coordinate space. #' @param name character(1); name of coordinate space #' @param type character(1); type of transformation #' @param data transformation data; size and shape depend on transformation and diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index 201785e0..9f776b4c 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -10,7 +10,7 @@ #' content describing the overall object. #' @param name character string for extraction (see \code{?base::`$`}). #' @param i,j indices specifying elements to extract. -#' @param drop ignored. +#' @param drop pattern ignored. #' @param ... optional arguments passed to and from other methods. #' #' @return \code{ShapeFrame} @@ -55,13 +55,15 @@ setMethod("names", "ShapeFrame", \(x) names(data(x))) #' @export #' @rdname ShapeFrame #' @importFrom utils .DollarNames -.DollarNames.ShapeFrame <- \(x) grep("", names(x), value=TRUE) +.DollarNames.ShapeFrame <- \(x, pattern="") + grep(pattern, names(x), value=TRUE) #' @rdname ShapeFrame #' @exportMethod $ setMethod("$", "ShapeFrame", \(x, name) data(x)[[name]]) #' @export +#' @rdname ShapeFrame #' @importFrom sf st_as_sf st_geometry_type setMethod("geom_type", "ShapeFrame", \(x) { y <- st_as_sf(data(x[1, ])) diff --git a/R/Zattrs.R b/R/Zattrs.R index 43f9982d..eb969bec 100644 --- a/R/Zattrs.R +++ b/R/Zattrs.R @@ -119,6 +119,7 @@ NULL #' @rdname SDattrs setMethod("feature_key", "list", \(x) x$spatialdata_attrs$feature_key) #' @export +#' @rdname SDattrs setMethod("feature_key", "PointFrame", \(x) feature_key(meta(x))) # TODO: only tables can have this? diff --git a/R/query.R b/R/query.R index 90817684..01fa1524 100644 --- a/R/query.R +++ b/R/query.R @@ -97,8 +97,8 @@ setMethod("query", "SpatialData", \(x, ..., i) { nrow(mx) >= 3, ncol(mx) == 2, !is.na(mx), is.finite(mx)) if (!all(ok)) stop( - "Invalid polygon query; should be numeric matrix ", - "with ≥ 3 rows and 2 columns (= xy-coordinates)") + "Invalid polygon query; should be numeric matrix with at ", + "least 3 rows and exactly 2 columns (= xy-coordinates)") # ensure polygon is closed top <- mx[1, ] bot <- mx[nrow(mx), ] diff --git a/man/CTutils.Rd b/man/CTutils.Rd index 190a1ded..ebb83d79 100644 --- a/man/CTutils.Rd +++ b/man/CTutils.Rd @@ -70,8 +70,6 @@ scalar integer giving the name or index of a coordinate space.} \item{data}{transformation data; size and shape depend on transformation and element type (e.g., numeric(1) for rotation, numeric(2) for scaling in 2D)} - -\item{j}{character string; name of target coordinate space.} } \description{ Coord. trans. utilities diff --git a/man/SDattrs.Rd b/man/SDattrs.Rd index b9843893..c5cb65ae 100644 --- a/man/SDattrs.Rd +++ b/man/SDattrs.Rd @@ -7,6 +7,7 @@ \alias{feature_key} \alias{instance_key} \alias{feature_key,list-method} +\alias{feature_key,PointFrame-method} \alias{region_key,SingleCellExperiment-method} \alias{region,SingleCellExperiment-method} \alias{instance_key,list-method} @@ -16,6 +17,8 @@ \usage{ \S4method{feature_key}{list}(x) +\S4method{feature_key}{PointFrame}(x) + \S4method{region_key}{SingleCellExperiment}(x) \S4method{region}{SingleCellExperiment}(x) diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index 00b5d853..0aef9cbb 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -8,6 +8,7 @@ \alias{names,ShapeFrame-method} \alias{.DollarNames.ShapeFrame} \alias{$,ShapeFrame-method} +\alias{geom_type,ShapeFrame-method} \alias{[,ShapeFrame,missing,ANY,ANY-method} \alias{[,ShapeFrame,ANY,missing,ANY-method} \alias{[,ShapeFrame,missing,missing,ANY-method} @@ -22,10 +23,12 @@ ShapeFrame(data = data.frame(), meta = Zattrs(), metadata = list(), ...) \S4method{names}{ShapeFrame}(x) -\method{.DollarNames}{ShapeFrame}(x) +\method{.DollarNames}{ShapeFrame}(x, pattern = "") \S4method{$}{ShapeFrame}(x, name) +\S4method{geom_type}{ShapeFrame}(x) + \S4method{[}{ShapeFrame,missing,ANY,ANY}(x, i, j, ..., drop = TRUE) \S4method{[}{ShapeFrame,ANY,missing,ANY}(x, i, j, ..., drop = TRUE) @@ -51,7 +54,7 @@ content describing the overall object.} \item{i, j}{indices specifying elements to extract.} -\item{drop}{ignored.} +\item{drop}{pattern ignored.} } \value{ \code{ShapeFrame} From 79a8dcb20ae6f1dea26ae42c2077a33aee33b6c3 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:09:22 +0200 Subject: [PATCH 139/151] fix biccheck warnings --- R/misc.R | 7 +++++-- man/misc.Rd | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/R/misc.R b/R/misc.R index ad128704..ee912ffd 100644 --- a/R/misc.R +++ b/R/misc.R @@ -1,7 +1,10 @@ #' @name misc #' @title Miscellaneous `SpatialData` methods -#' @description ... -#' +#' @aliases show,SpatialData-method +#' +#' @description Miscellaneous methods (e.g., \code{show}) +#' for the \link{\code{SpatialData}} class and its elements. +#' #' @param object \code{\link{SpatialData}} object or one of its #' elements, i.e., an Image/LabelArray or Point/ShapeFrame. #' diff --git a/man/misc.Rd b/man/misc.Rd index a67f7ec8..5e2d5483 100644 --- a/man/misc.Rd +++ b/man/misc.Rd @@ -24,7 +24,8 @@ elements, i.e., an Image/LabelArray or Point/ShapeFrame.} \code{NULL} } \description{ -... +Miscellaneous methods (e.g., \code{show}) +for the \link{\code{SpatialData}} class and its elements. } \examples{ zs <- file.path("extdata", "blobs.zarr") From 46e2a30119e395d5c9f276af4b5d0248ca3abf34 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:10:17 +0200 Subject: [PATCH 140/151] prettify doc --- R/misc.R | 10 ++++++---- man/misc.Rd | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/R/misc.R b/R/misc.R index ee912ffd..ad358288 100644 --- a/R/misc.R +++ b/R/misc.R @@ -2,11 +2,13 @@ #' @title Miscellaneous `SpatialData` methods #' @aliases show,SpatialData-method #' -#' @description Miscellaneous methods (e.g., \code{show}) -#' for the \link{\code{SpatialData}} class and its elements. +#' @description +#' Miscellaneous methods (e.g., \code{show}) for the +#' \link{\code{SpatialData}} class and its elements. #' -#' @param object \code{\link{SpatialData}} object or one of its -#' elements, i.e., an Image/LabelArray or Point/ShapeFrame. +#' @param object +#' \code{\link{SpatialData}} object or one of its elements, +#' i.e., an \code{Image/LabelArray} or \code{Point/ShapeFrame}. #' #' @return \code{NULL} #' diff --git a/man/misc.Rd b/man/misc.Rd index 5e2d5483..fbf18a6c 100644 --- a/man/misc.Rd +++ b/man/misc.Rd @@ -17,15 +17,15 @@ \S4method{show}{ShapeFrame}(object) } \arguments{ -\item{object}{\code{\link{SpatialData}} object or one of its -elements, i.e., an Image/LabelArray or Point/ShapeFrame.} +\item{object}{\code{\link{SpatialData}} object or one of its elements, +i.e., an \code{Image/LabelArray} or \code{Point/ShapeFrame}.} } \value{ \code{NULL} } \description{ -Miscellaneous methods (e.g., \code{show}) -for the \link{\code{SpatialData}} class and its elements. +Miscellaneous methods (e.g., \code{show}) for the +\link{\code{SpatialData}} class and its elements. } \examples{ zs <- file.path("extdata", "blobs.zarr") From dc98bf4876550bcc8f35f5822de2e14f338caacf Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:23:19 +0200 Subject: [PATCH 141/151] fix remaining warnings --- R/LabelArray.R | 8 +++++++- R/ShapeFrame.R | 2 +- R/data.R | 2 +- R/misc.R | 2 +- R/utils.R | 5 +++++ man/LabelArray.Rd | 8 +++++++- man/ShapeFrame.Rd | 2 +- man/blobs.Rd | 3 +++ man/misc.Rd | 2 +- man/utils.Rd | 5 +++++ 10 files changed, 32 insertions(+), 7 deletions(-) diff --git a/R/LabelArray.R b/R/LabelArray.R index 403f8b3f..0f544ba9 100644 --- a/R/LabelArray.R +++ b/R/LabelArray.R @@ -25,7 +25,13 @@ #' @return \code{LabelArray} #' #' @examples -#' # TODO +#' x <- file.path("extdata", "blobs.zarr") +#' x <- system.file(x, package="SpatialData") +#' x <- file.path(x, "labels", "blobs_labels") +#' +#' (y <- readLabel(x)) +#' y[1:10, 1:10] +#' meta(y) #' #' @importFrom S4Vectors metadata<- #' @importFrom methods new diff --git a/R/ShapeFrame.R b/R/ShapeFrame.R index 9f776b4c..86ad2b91 100644 --- a/R/ShapeFrame.R +++ b/R/ShapeFrame.R @@ -10,7 +10,7 @@ #' content describing the overall object. #' @param name character string for extraction (see \code{?base::`$`}). #' @param i,j indices specifying elements to extract. -#' @param drop pattern ignored. +#' @param drop,pattern ignored. #' @param ... optional arguments passed to and from other methods. #' #' @return \code{ShapeFrame} diff --git a/R/data.R b/R/data.R index 9c6daa62..97aa5bf2 100644 --- a/R/data.R +++ b/R/data.R @@ -4,7 +4,7 @@ #' #' @description data were retrieved on Nov. 11th, 2024, from \href{https://github.com/scverse/spatialdata-notebooks/tree/main/notebooks/developers_resources/storage_format/multiple_elements.zarr}{here}. #' -#' @return NULL +#' @returns zarr store. #' #' @examples #' x <- file.path("extdata", "blobs.zarr") diff --git a/R/misc.R b/R/misc.R index ad358288..713a5b82 100644 --- a/R/misc.R +++ b/R/misc.R @@ -4,7 +4,7 @@ #' #' @description #' Miscellaneous methods (e.g., \code{show}) for the -#' \link{\code{SpatialData}} class and its elements. +#' \code{\link{SpatialData}} class and its elements. #' #' @param object #' \code{\link{SpatialData}} object or one of its elements, diff --git a/R/utils.R b/R/utils.R index 91fac0b8..8618e2ad 100644 --- a/R/utils.R +++ b/R/utils.R @@ -7,6 +7,11 @@ #' @param as character string; how results should be returned. #' @param ... optional arguments passed to and from other methods. #' +#' @returns +#' For \code{centroids}, a table (\code{data.frame} or \code{matrix}) +#' of spatial coordinates (if \code{as="list"}, split by instance); +#' for extend, a length-2 numeric list of x- and y-ranges. +#' #' @examples #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") diff --git a/man/LabelArray.Rd b/man/LabelArray.Rd index 0c3dea1b..338f60fd 100644 --- a/man/LabelArray.Rd +++ b/man/LabelArray.Rd @@ -41,6 +41,12 @@ Currently defined methods (here, \code{x} is a \code{LabelArray}): } } \examples{ -# TODO +x <- file.path("extdata", "blobs.zarr") +x <- system.file(x, package="SpatialData") +x <- file.path(x, "labels", "blobs_labels") + +(y <- readLabel(x)) +y[1:10, 1:10] +meta(y) } diff --git a/man/ShapeFrame.Rd b/man/ShapeFrame.Rd index 0aef9cbb..e69d3efc 100644 --- a/man/ShapeFrame.Rd +++ b/man/ShapeFrame.Rd @@ -54,7 +54,7 @@ content describing the overall object.} \item{i, j}{indices specifying elements to extract.} -\item{drop}{pattern ignored.} +\item{drop, pattern}{ignored.} } \value{ \code{ShapeFrame} diff --git a/man/blobs.Rd b/man/blobs.Rd index 1aad2796..9cd4b024 100644 --- a/man/blobs.Rd +++ b/man/blobs.Rd @@ -3,6 +3,9 @@ \name{blobs} \alias{blobs} \title{`SpatialData` .zarr toy datasets} +\value{ +zarr store. +} \description{ data were retrieved on Nov. 11th, 2024, from \href{https://github.com/scverse/spatialdata-notebooks/tree/main/notebooks/developers_resources/storage_format/multiple_elements.zarr}{here}. } diff --git a/man/misc.Rd b/man/misc.Rd index fbf18a6c..4c5cade5 100644 --- a/man/misc.Rd +++ b/man/misc.Rd @@ -25,7 +25,7 @@ i.e., an \code{Image/LabelArray} or \code{Point/ShapeFrame}.} } \description{ Miscellaneous methods (e.g., \code{show}) for the -\link{\code{SpatialData}} class and its elements. +\code{\link{SpatialData}} class and its elements. } \examples{ zs <- file.path("extdata", "blobs.zarr") diff --git a/man/utils.Rd b/man/utils.Rd index b17c24cf..245bc2c4 100644 --- a/man/utils.Rd +++ b/man/utils.Rd @@ -31,6 +31,11 @@ \item{as}{character string; how results should be returned.} } +\value{ +For \code{centroids}, a table (\code{data.frame} or \code{matrix}) +of spatial coordinates (if \code{as="list"}, split by instance); +for extend, a length-2 numeric list of x- and y-ranges. +} \description{ Utilities } From ebd62a979018f039fea8bc53b2f0a066135785ee Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:41:38 +0200 Subject: [PATCH 142/151] address remaining notes/warnings --- R/CTgraph.R | 13 +++++++++++++ man/CTgraph.Rd | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/R/CTgraph.R b/R/CTgraph.R index ee47ebd3..a1a2dd40 100644 --- a/R/CTgraph.R +++ b/R/CTgraph.R @@ -10,6 +10,19 @@ #' @param fac,max scalar numeric; node labels with \code{nchar>max} #' are split and hyphenated at position \code{floor(nchar/fac)} #' +#' @returns +#' \itemize{ +#' \item \code{CTgraph}: +#' \code{graph::graphAM} object with nodes for each element and +#' coordinate space, and edges for each transformation (if specified) +#' \item \code{CTpath}: +#' list of transformations from \code{i} to \code{j}; +#' length > 1 if \code{type} is \code{"sequential"}, length-1 otherwise; +#' each element specifies \code{type} and \code{data} of the transformation +#' \item \code{CTplot}: +#' visualizes the element-coordinate space graph with \code{Rgraphviz} +#' } +#' #' @examples #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") diff --git a/man/CTgraph.Rd b/man/CTgraph.Rd index 89336782..5892ad88 100644 --- a/man/CTgraph.Rd +++ b/man/CTgraph.Rd @@ -40,6 +40,19 @@ CTplot(g, cex = 0.5, fac = 2, max = 10) \item{fac, max}{scalar numeric; node labels with \code{nchar>max} are split and hyphenated at position \code{floor(nchar/fac)}} } +\value{ +\itemize{ +\item \code{CTgraph}: + \code{graph::graphAM} object with nodes for each element and + coordinate space, and edges for each transformation (if specified) +\item \code{CTpath}: + list of transformations from \code{i} to \code{j}; + length > 1 if \code{type} is \code{"sequential"}, length-1 otherwise; + each element specifies \code{type} and \code{data} of the transformation +\item \code{CTplot}: + visualizes the element-coordinate space graph with \code{Rgraphviz} +} +} \description{ Coord. trans. graph } From 3165891c6e48772939fbacd9ce85f3a864322a74 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 19:56:00 +0200 Subject: [PATCH 143/151] add missing return val sections --- R/CTutils.R | 18 ++++++++++++++++++ R/trans.R | 2 ++ man/CTutils.Rd | 18 ++++++++++++++++++ man/trans.Rd | 3 +++ 4 files changed, 41 insertions(+) diff --git a/R/CTutils.R b/R/CTutils.R index c1e06576..124c0929 100644 --- a/R/CTutils.R +++ b/R/CTutils.R @@ -11,6 +11,24 @@ #' element type (e.g., numeric(1) for rotation, numeric(2) for scaling in 2D) #' @param ... option arguments passed to and from other methods. #' +#' @returns +#' \itemize{ +#' \item \code{CTname}: character string; +#' transformation name (e.g., "global") +#' \item \code{CTtype}: character string; +#' transformation type (e.g., "affine") +#' \item \code{CTdata}: list; +#' transformation data (e.g., scalar numeric for rotation) +#' \item \code{CTlist}: list; +#' list of transformation specifications per OME-NGFF spec +#' \item \code{add/rmvCT}: +#' \code{SpatialDataElement} or \code{Zattrs} +#' with transformation(s) added/removed +#' \item \code{axes}: list; +#' each element is a character string (name), or list +#' with axis name and type (e.g., "space" or "channel") +#' } +#' #' @examples #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") diff --git a/R/trans.R b/R/trans.R index 138bd9a4..af9ec51f 100644 --- a/R/trans.R +++ b/R/trans.R @@ -12,6 +12,8 @@ #' only applies to \code{sdArray}s (images, labels). #' @param ... option arguments passed to and from other methods. #' +#' @returns \code{SpatialData} element with transformation(s) applied. +#' #' @examples #' x <- file.path("extdata", "blobs.zarr") #' x <- system.file(x, package="SpatialData") diff --git a/man/CTutils.Rd b/man/CTutils.Rd index ebb83d79..a04efc4b 100644 --- a/man/CTutils.Rd +++ b/man/CTutils.Rd @@ -71,6 +71,24 @@ scalar integer giving the name or index of a coordinate space.} \item{data}{transformation data; size and shape depend on transformation and element type (e.g., numeric(1) for rotation, numeric(2) for scaling in 2D)} } +\value{ +\itemize{ +\item \code{CTname}: character string; + transformation name (e.g., "global") +\item \code{CTtype}: character string; + transformation type (e.g., "affine") +\item \code{CTdata}: list; + transformation data (e.g., scalar numeric for rotation) +\item \code{CTlist}: list; + list of transformation specifications per OME-NGFF spec +\item \code{add/rmvCT}: + \code{SpatialDataElement} or \code{Zattrs} + with transformation(s) added/removed +\item \code{axes}: list; + each element is a character string (name), or list + with axis name and type (e.g., "space" or "channel") +} +} \description{ Coord. trans. utilities } diff --git a/man/trans.Rd b/man/trans.Rd index 659543bc..b4f9c87d 100644 --- a/man/trans.Rd +++ b/man/trans.Rd @@ -59,6 +59,9 @@ only applies to \code{sdArray}s (images, labels).} \item{...}{option arguments passed to and from other methods.} } +\value{ +\code{SpatialData} element with transformation(s) applied. +} \description{ Transformations } From 0873b552dfcfcfc915cba9165b4c992031f60c64 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 20:04:05 +0200 Subject: [PATCH 144/151] add more missing value secs --- R/table_utils.R | 13 +++++++++++++ R/tx_to_ext.R | 2 ++ man/do_tx_to_ext.Rd | 3 +++ man/table-utils.Rd | 13 +++++++++++++ 4 files changed, 31 insertions(+) diff --git a/R/table_utils.R b/R/table_utils.R index 784c7c47..7e2e6e59 100644 --- a/R/table_utils.R +++ b/R/table_utils.R @@ -18,6 +18,19 @@ #' @param ... \code{data.frame} or list of data generation function(s) #' that accept an argument for the number of observations; see examples. #' +#' @returns +#' \itemize{ +#' \item \code{hasTable}: +#' logical scalar (or character string, if \code{name=TRUE}); +#' whether or not a \code{table} annotating \code{i} exists in \code{x} +#' \item \code{getTable}: +#' \code{SingleCellExperiment}; the \code{table} annotating +#' \code{i} with optional filtering of matching observations +#' \item \code{valTable}: +#' vector of values (according to \code{j}) +#' from the \code{table} annotating \code{i} +#' } +#' #' @examples #' library(SingleCellExperiment) #' x <- file.path("extdata", "blobs.zarr") diff --git a/R/tx_to_ext.R b/R/tx_to_ext.R index f0149435..e4421a28 100644 --- a/R/tx_to_ext.R +++ b/R/tx_to_ext.R @@ -21,6 +21,8 @@ #' can include "maintain_positioning" (logical (1)) or numerics for #' target_unit_to_pixels, target_width, target_height, target_depth. #' +#' @return \code{SpatialData} object. +#' #' @examples #' src <- system.file("extdata", "blobs.zarr", package="SpatialData") #' td <- tempfile() diff --git a/man/do_tx_to_ext.Rd b/man/do_tx_to_ext.Rd index da7c529f..47c8656c 100644 --- a/man/do_tx_to_ext.Rd +++ b/man/do_tx_to_ext.Rd @@ -17,6 +17,9 @@ do_tx_to_ext(srcdir, dest, coordinate_system, ...) can include "maintain_positioning" (logical (1)) or numerics for target_unit_to_pixels, target_width, target_height, target_depth.} } +\value{ +\code{SpatialData} object. +} \description{ Use Python's 'spatialdata' 'transform_to_data_extent' on a spatialdata zarr store } diff --git a/man/table-utils.Rd b/man/table-utils.Rd index b83b2f8c..c9f700ee 100644 --- a/man/table-utils.Rd +++ b/man/table-utils.Rd @@ -56,6 +56,19 @@ or row name to retrieve \code{assay} data.} \item{assay}{character string or scalar integer; specifies which \code{assay} to use when \code{j} is a row name.} } +\value{ +\itemize{ +\item \code{hasTable}: + logical scalar (or character string, if \code{name=TRUE}); + whether or not a \code{table} annotating \code{i} exists in \code{x} +\item \code{getTable}: + \code{SingleCellExperiment}; the \code{table} annotating + \code{i} with optional filtering of matching observations +\item \code{valTable}: + vector of values (according to \code{j}) + from the \code{table} annotating \code{i} +} +} \description{ \code{SpatialData} annotations } From 192aac8ebd8acd81f009d6715f51af3013009ae8 Mon Sep 17 00:00:00 2001 From: HelenaLC Date: Sat, 11 Apr 2026 20:12:45 +0200 Subject: [PATCH 145/151] use <- instead of = --- R/read.R | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/R/read.R b/R/read.R index 7d3dce5a..78217254 100644 --- a/R/read.R +++ b/R/read.R @@ -1,4 +1,3 @@ -# # allp = c("session_info==1.0.0", "spatialdata==0.3.0", "spatialdata_io==0.1.7", # "pillow==11.1.0", "anndata==0.11.3", "annotated_types==0.7.0", "asciitree==0.3.3", # "attr==0.3.2", "certifi==2025.01.31", "charset_normalizer==3.4.1", @@ -25,8 +24,14 @@ # "typing_extensions==4.12.2", "urllib3==2.3.0", "wrapt==1.17.2", # "xarray==2024.11.0", "xarray_dataclasses==1.9.1", "xarray_schema==0.0.3", # "zarr==2.18.4", "zict==3.0.0") -allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", - "spatialdata_plot==0.2.14", "setuptools==75.8.0") + +allp <- c( + "zarr==3.1.5", + "spatialdata==0.7.0", + "spatialdata_io==0.6.0", + "spatialdata_plot==0.2.14", + "setuptools==75.8.0") + # notes from VJC/AM -- readSpatialData was modified below so # that if anndataR = FALSE, anndata.read_zarr is used # to get the whole zarr store, and then the tables are @@ -34,7 +39,6 @@ allp = c("zarr==3.1.5", "spatialdata==0.7.0", "spatialdata_io==0.6.0", # for ingesting the visium_hd_3.0.0 example but fails on # the blobs dataset in example("table-utils") because # of matters related to metadata/hasTable behavior -# #' @name readSpatialData #' @title Reading `SpatialData` From 9a1fa9521da5bc9e2900a3f74c3a2aa9716aa078 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Wed, 15 Apr 2026 09:31:21 +0200 Subject: [PATCH 146/151] add read/write support for zarr v3 --- R/zarr_utils.R | 81 ++++++++++++++++++++++----------- tests/testthat/test-zarrutils.R | 80 +++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 33 deletions(-) diff --git a/R/zarr_utils.R b/R/zarr_utils.R index 15f7e5b4..12ebbbc5 100644 --- a/R/zarr_utils.R +++ b/R/zarr_utils.R @@ -9,23 +9,25 @@ create_zarr_group <- function(store, name, version = "v2"){ split.name <- strsplit(name, split = "\\/")[[1]] if(length(split.name) > 1){ - split.name <- vapply(seq_len(length(split.name)), - function(x) paste(split.name[seq_len(x)], collapse = "/"), - FUN.VALUE = character(1)) + split.name <- vapply(seq_len(length(split.name)), + function(x) paste(split.name[seq_len(x)], collapse = "/"), + FUN.VALUE = character(1)) split.name <- rev(tail(split.name,2)) if(!dir.exists(file.path(store,split.name[2]))) - create_zarr_group(store = store, name = split.name[2]) + create_zarr_group(store = store, name = split.name[2], version = version) } dir.create(file.path(store, split.name[1]), showWarnings = FALSE) - switch(version, + switch(version, v2 = { write("{\"zarr_format\":2}", file = file.path(store, split.name[1], ".zgroup"))}, v3 = { - stop("Currently only zarr v2 is supported!") + write( + "{\"zarr_format\":3,\"node_type\":\"group\",\"attributes\":{}}", + file = file.path(store, split.name[1], "zarr.json")) }, - stop("only zarr v2 is supported. Use version = 'v2'") + stop("version must be 'v2' or 'v3'") ) - + } #' create_zarr @@ -63,16 +65,25 @@ create_zarr <- function(name, dir, version = "v2"){ #' @export read_zattrs <- function(path, s3_client = NULL) { path <- .normalize_array_path(path) - zattrs_path <- paste0(path, ".zattrs") - - if(!file.exists(zattrs_path)) - stop("The group or array does not contain attributes (.zattrs)") - - if (!is.null(s3_client)) { + + if (!is.null(s3_client)) stop("no s3 support!") - } else { + + zarr_json_path <- paste0(path, "zarr.json") + zattrs_path <- paste0(path, ".zattrs") + + if (file.exists(zarr_json_path)) { + # zarr v3: attributes are nested under the "attributes" key in zarr.json + parsed <- read_json(zarr_json_path, simplifyVector = TRUE) + zattrs <- parsed[["attributes"]] + if (is.null(zattrs)) zattrs <- list() + } else if (file.exists(zattrs_path)) { + # zarr v2: standalone .zattrs file zattrs <- read_json(zattrs_path, simplifyVector = TRUE) + } else { + stop("The group or array does not contain attributes (.zattrs or zarr.json)") } + return(zattrs) } @@ -88,28 +99,44 @@ read_zattrs <- function(path, s3_client = NULL) { #' @export write_zattrs <- function(path, new.zattrs = list(), overwrite = TRUE){ path <- .normalize_array_path(path) - zattrs_path <- paste0(path, ".zattrs") - + if(is.null(names(new.zattrs))) stop("list elements should be named") - + if("" %in% names(new.zattrs)){ message("Ignoring unnamed list elements") new.zattrs <- new.zattrs[which(names(new.zattrs == ""))] } - - if(file.exists(zattrs_path)){ - old.zattrs <- read_json(zattrs_path) - if(overwrite){ + + zarr_json_path <- paste0(path, "zarr.json") + zattrs_path <- paste0(path, ".zattrs") + + if (file.exists(zarr_json_path)) { + # zarr v3: merge new.zattrs into the "attributes" key of zarr.json + parsed <- read_json(zarr_json_path) + old.zattrs <- if (!is.null(parsed[["attributes"]])) parsed[["attributes"]] else list() + if (overwrite) { old.zattrs <- old.zattrs[setdiff(names(old.zattrs), names(new.zattrs))] } else { - new.zattrs <- new.zattrs[setdiff(names(new.zattrs), names(old.zattrs))] + new.zattrs <- new.zattrs[setdiff(names(new.zattrs), names(old.zattrs))] } - new.zattrs <- c(old.zattrs, new.zattrs) + parsed[["attributes"]] <- c(old.zattrs, new.zattrs) + json <- toJSON(parsed, auto_unbox = TRUE, pretty = TRUE, null = "null") + write(x = json, file = zarr_json_path) + } else { + # zarr v2: standalone .zattrs file + if (file.exists(zattrs_path)) { + old.zattrs <- read_json(zattrs_path) + if (overwrite) { + old.zattrs <- old.zattrs[setdiff(names(old.zattrs), names(new.zattrs))] + } else { + new.zattrs <- new.zattrs[setdiff(names(new.zattrs), names(old.zattrs))] + } + new.zattrs <- c(old.zattrs, new.zattrs) + } + json <- .format_json(toJSON(new.zattrs, auto_unbox = TRUE, pretty = TRUE, null = "null")) + write(x = json, file = zattrs_path) } - - json <- .format_json(toJSON(new.zattrs, auto_unbox = TRUE, pretty = TRUE, null = "null")) - write(x = json, file = zattrs_path) } .replace_zarr <- function(name, path, replace, version = "v2") diff --git a/tests/testthat/test-zarrutils.R b/tests/testthat/test-zarrutils.R index 1d086572..133eac2b 100644 --- a/tests/testthat/test-zarrutils.R +++ b/tests/testthat/test-zarrutils.R @@ -32,21 +32,49 @@ test_that("create zarr/group", { expect_true(dir.exists(file.path(output_zarr, "group3/subgroup1/subsubgroup1"))) expect_true(file.exists(file.path(output_zarr, "group3/subgroup1/subsubgroup1", ".zgroup"))) - # version 3 and other entries + # invalid version string dir.create(td <- tempfile()) name <- "test" - output_zarr <- file.path(td, paste0(name, ".zarr")) - expect_error(create_zarr(dir = td, name = name, version = "v4"), pattern = "only zarr v2 is supported") + expect_error(create_zarr(dir = td, name = name, version = "v4"), pattern = "version must be 'v2' or 'v3'") }) +test_that("create zarr/group v3", { -# create zarr array + dir.create(td <- tempfile()) + name <- "test.zarr" + output_zarr <- file.path(td, name) + + # open v3 zarr store + create_zarr(name = name, dir = td, version = "v3") + expect_true(dir.exists(output_zarr)) + expect_true(file.exists(file.path(output_zarr, "zarr.json"))) + expect_false(file.exists(file.path(output_zarr, ".zgroup"))) + + # check zarr.json content + meta <- jsonlite::read_json(file.path(output_zarr, "zarr.json")) + expect_equal(meta$zarr_format, 3) + expect_equal(meta$node_type, "group") + expect_true(is.list(meta$attributes) && length(meta$attributes) == 0) + + # create a sub-group + create_zarr_group(store = output_zarr, name = "images", version = "v3") + expect_true(file.exists(file.path(output_zarr, "images", "zarr.json"))) + expect_false(file.exists(file.path(output_zarr, "images", ".zgroup"))) + + # create nested groups — parent group should also be v3 + create_zarr_group(store = output_zarr, name = "points/blobs_points", version = "v3") + expect_true(file.exists(file.path(output_zarr, "points", "zarr.json"))) + expect_true(file.exists(file.path(output_zarr, "points/blobs_points", "zarr.json"))) +}) + + +# create a v2 zarr array for the v2 zattrs tests dir.create(td <- tempfile()) path <- file.path(td, "test.zarr") x <- array(runif(n = 10), dim = c(2, 5)) Rarr::write_zarr_array( x = x, zarr_array_path = path, - chunk_dim = c(2, 5) + chunk_dim = c(2, 5), zarr_version = 2L ) test_that("read/write zattrs", { @@ -79,5 +107,45 @@ test_that("read/write zattrs", { read.zattrs <- read_zattrs(path) zattrs[names(zattrs.new.elem)] <- "foo2" expect_equal(read.zattrs, c(zattrs)) - + +}) + +test_that("read/write zattrs v3", { + + # create a v3 zarr group to use as the target path + dir.create(td <- tempfile()) + grp <- file.path(td, "elem") + create_zarr_group(store = td, name = "elem", version = "v3") + + # write attributes into zarr.json + zattrs <- list(foo = "foo", bar = "bar") + write_zattrs(path = grp, new.zattrs = zattrs) + expect_true(file.exists(file.path(grp, "zarr.json"))) + expect_false(file.exists(file.path(grp, ".zattrs"))) + + # read back attributes from zarr.json + read.zattrs <- read_zattrs(grp) + expect_equal(read.zattrs, zattrs) + + # zarr_format / node_type keys in zarr.json must be preserved + meta <- jsonlite::read_json(file.path(grp, "zarr.json")) + expect_equal(meta$zarr_format, 3) + expect_equal(meta$node_type, "group") + + # add new element + write_zattrs(path = grp, new.zattrs = list(baz = "baz")) + read.zattrs <- read_zattrs(grp) + expect_equal(read.zattrs, c(zattrs, list(baz = "baz"))) + + # overwrite existing key + write_zattrs(path = grp, new.zattrs = list(foo = "FOO")) + read.zattrs <- read_zattrs(grp) + expect_equal(read.zattrs$foo, "FOO") + expect_equal(read.zattrs$bar, "bar") # untouched key preserved + + # overwrite = FALSE should not overwrite existing key + write_zattrs(path = grp, new.zattrs = list(foo = "original"), overwrite = FALSE) + read.zattrs <- read_zattrs(grp) + expect_equal(read.zattrs$foo, "FOO") # unchanged + }) \ No newline at end of file From 1acbe7f44214b58fea024ece239dff8412a422f1 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Wed, 15 Apr 2026 16:05:37 +0200 Subject: [PATCH 147/151] continue zarr v3 write, use Rarr read/write functionality when possible --- R/metadata.R | 76 +++++++++--------- R/write.R | 42 ++++++---- R/zarr_utils.R | 132 -------------------------------- tests/testthat/test-zarrutils.R | 54 ++++++------- 4 files changed, 90 insertions(+), 214 deletions(-) diff --git a/R/metadata.R b/R/metadata.R index 8470978a..b338c502 100644 --- a/R/metadata.R +++ b/R/metadata.R @@ -41,7 +41,17 @@ } # metadata constructors ---- - +#' @title Make point/shape metadata +#' @description Make point/shape metadata +#' @param x A points or shapes object +#' @param axes A character vector of axes names +#' @param encoding_type A string specifying the encoding type +#' @param feature_key A string specifying the feature key +#' @param instance_key A string specifying the instance key +#' @param version A string specifying the version +#' @return A list of metadata for the point/shape object +#' @importFrom jsonlite fromJSON toJSON +#' @noRd .make_pointshape_meta <- function(x, axes = NULL, encoding_type = "ngff:points", @@ -51,7 +61,7 @@ meta <- list() ax <- "axes" ct <- "coordinateTransformations" - sa <- "spatial_attrs" + sa <- "spatialdata_attrs" # axis # NOTE: rev dimensions since points and shapes want x, y @@ -72,10 +82,20 @@ meta[[ct]] <- .make_empty_ct(meta[[ax]]) # update json list - meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = TRUE) + meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = FALSE) Zattrs(meta) } +# TODO: make it the functions take a global option e.g. sd_zarr_version +# as an argument for the default zarr version +#' @title Make image metadata +#' @description Make image metadata +#' @param x An image object +#' @param axes A character vector of axes names +#' @param version A string specifying the version +#' @return A list of metadata for the image object +#' @importFrom jsonlite fromJSON toJSON +#' @noRd .make_image_meta <- function(x, axes = NULL, version = 0.4){ @@ -113,18 +133,26 @@ meta[[v]] <- list(version = version) # multiscales - meta <- list(multiscales = list(meta), + meta <- list(multiscales = list(meta), omero = list( - channels = lapply(seq_len(length(axes))-1, \(.) + channels = lapply(seq_len(length(axes))-1, \(.) list(label = .)) - ), + ), spatialdata_attrs = list(version = "0.1")) - + # update json list - meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = TRUE) + meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = FALSE) Zattrs(meta) } +#' @title Make label metadata +#' @description Make label metadata +#' @param x A label object +#' @param axes A character vector of axes names +#' @param version A string specifying the version +#' @return A list of metadata for the label object +#' @importFrom jsonlite fromJSON toJSON +#' @noRd .make_label_meta <- function(x, axes = NULL, version = 0.4){ @@ -164,13 +192,12 @@ spatialdata_attrs = list(version = "0.1")) # update json list - meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = TRUE) + meta <- fromJSON(toJSON(meta, auto_unbox = TRUE), simplifyVector = FALSE) Zattrs(meta) } -#' .get_valid_axes -#' -#' Get validated axes +#' @title Get valid axes +#' @description Get validated axes #' #' @inheritParams write_image #' @@ -254,28 +281,5 @@ return(c(nrow(x), n_col)) } -#' #' @importFrom sf st_as_sf st_geometry -#' .get_geoarrow_dim <- function(x){ -#' meta <- .get_geoarrow_metadata(x) -#' if(length(meta) > 1){ -#' if("geometry" %in% colnames(meta)){ -#' bbox <- meta$geo$columns$geometry$bbox -#' n_col <- if(length(bbox) == 4) 2 else 3 -#' } else { -#' n_col <- ncol(x) -#' } -#' } else { -#' if("geometry" %in% colnames(x)){ -#' geo <- st_geometry(st_as_sf(df)) -#' } else{ -#' stop("No geometry object is detected!") -#' } -#' } -#' return(c(nrow(x), n_col)) -#' } -#' -#' #' @importFrom jsonlite fromJSON -#' .get_geoarrow_metadata <- function(x){ -#' lapply(x$metadata, fromJSON) -#' } + diff --git a/R/write.R b/R/write.R index 8217abfe..3181696e 100644 --- a/R/write.R +++ b/R/write.R @@ -39,22 +39,22 @@ writeSpatialData <- function(x, name, path, replace = TRUE, version = "v2", # write points . <- lapply(pointNames(x), \(.){ - writePoint(point(x, .),., path = zarr.path, replace = replace) + writePoint(point(x, .),., path = zarr.path, replace = replace, version = version) }) - + # write shapes . <- lapply(shapeNames(x), \(.){ - writeShape(shape(x, .),., path = zarr.path, replace = replace) + writeShape(shape(x, .),., path = zarr.path, replace = replace, version = version) }) - + # write images . <- lapply(imageNames(x), \(.){ - writeImage(image(x, .),., path = zarr.path, replace = replace) + writeImage(image(x, .),., path = zarr.path, replace = replace, version = version) }) - + # write labels . <- lapply(labelNames(x), \(.){ - writeLabel(label(x, .),., path = zarr.path, replace = replace) + writeLabel(label(x, .),., path = zarr.path, replace = replace, version = version) }) } @@ -64,7 +64,7 @@ writePoint <- function(x, name, path, replace = TRUE, version = "v2") { # if no PointFrames were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "points"), replace, version) # write meta - write_zattrs(path = zarr.group, meta(x)) + Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) # write data arrow::write_dataset(data(x), file.path(zarr.group, "points.parquet")) } @@ -75,45 +75,53 @@ writeShape <- function(x, name, path, replace = TRUE, version = "v2") { # if no ShapeFrames were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "shapes"), replace, version) # write meta - write_zattrs(path = zarr.group, meta(x)) + Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) # write data arrow::write_dataset(data(x), file.path(zarr.group, "shapes.parquet")) } #' @rdname writeSpatialData +#' @importFrom Rarr write_zarr_array +#' @importFrom DelayedArray realize #' @export writeImage <- function(x, name, path, replace = TRUE, version = "v2") { # if no ImageArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "images"), replace, version) + zarr_version <- if (version == "v3") 3L else 2L # write meta - write_zattrs(path = zarr.group, meta(x)) + Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) # write data lapply( .get_multiscales_dataset_paths(meta(x)), \(.){ da <- data(x, . + 1) - Rarr::write_zarr_array(realize(da), - zarr_array_path = file.path(zarr.group, .), - chunk_dim = dim(da)) + Rarr::write_zarr_array(realize(da), + zarr_array_path = file.path(zarr.group, .), + chunk_dim = dim(da), + zarr_version = zarr_version) } ) } #' @rdname writeSpatialData +#' @importFrom Rarr write_zarr_array +#' @importFrom DelayedArray realize #' @export writeLabel <- function(x, name, path, replace = TRUE, version = "v2") { # if no LabelArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "labels"), replace, version) + zarr_version <- if (version == "v3") 3L else 2L # write meta - write_zattrs(path = zarr.group, meta(x)) + Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) # write data lapply( .get_multiscales_dataset_paths(meta(x)), \(.){ da <- data(x, . + 1) - Rarr::write_zarr_array(realize(da), - zarr_array_path = file.path(zarr.group, .), - chunk_dim = dim(da)) + Rarr::write_zarr_array(realize(da), + zarr_array_path = file.path(zarr.group, .), + chunk_dim = dim(da), + zarr_version = zarr_version) } ) } \ No newline at end of file diff --git a/R/zarr_utils.R b/R/zarr_utils.R index 12ebbbc5..816d546f 100644 --- a/R/zarr_utils.R +++ b/R/zarr_utils.R @@ -49,95 +49,6 @@ create_zarr <- function(name, dir, version = "v2"){ create_zarr_group(store = dir, name = name, version = version) } -#' Read the .zattrs file associated with a Zarr array or group -#' -#' @param path A character vector of length 1. This provides the -#' path to a Zarr array or group. This can either be on a local file -#' system or on S3 storage. -#' @param s3_client A list representing an S3 client. This should be produced -#' by [paws.storage::s3()]. -#' -#' @returns A list containing the .zattrs elements -#' -#' @importFrom jsonlite read_json fromJSON -#' @importFrom stringr str_extract str_remove -#' -#' @export -read_zattrs <- function(path, s3_client = NULL) { - path <- .normalize_array_path(path) - - if (!is.null(s3_client)) - stop("no s3 support!") - - zarr_json_path <- paste0(path, "zarr.json") - zattrs_path <- paste0(path, ".zattrs") - - if (file.exists(zarr_json_path)) { - # zarr v3: attributes are nested under the "attributes" key in zarr.json - parsed <- read_json(zarr_json_path, simplifyVector = TRUE) - zattrs <- parsed[["attributes"]] - if (is.null(zattrs)) zattrs <- list() - } else if (file.exists(zattrs_path)) { - # zarr v2: standalone .zattrs file - zattrs <- read_json(zattrs_path, simplifyVector = TRUE) - } else { - stop("The group or array does not contain attributes (.zattrs or zarr.json)") - } - - return(zattrs) -} - -#' Read the .zattrs file associated with a Zarr array or group -#' -#' @param path A character vector of length 1. This provides the -#' path to a Zarr array or group. -#' @param new.zattrs a list inserted to .zattrs at the \code{path}. -#' @param overwrite if TRUE, existing .zattrs elements will be overwritten by \code{new.zattrs}. -#' -#' @importFrom jsonlite toJSON -#' -#' @export -write_zattrs <- function(path, new.zattrs = list(), overwrite = TRUE){ - path <- .normalize_array_path(path) - - if(is.null(names(new.zattrs))) - stop("list elements should be named") - - if("" %in% names(new.zattrs)){ - message("Ignoring unnamed list elements") - new.zattrs <- new.zattrs[which(names(new.zattrs == ""))] - } - - zarr_json_path <- paste0(path, "zarr.json") - zattrs_path <- paste0(path, ".zattrs") - - if (file.exists(zarr_json_path)) { - # zarr v3: merge new.zattrs into the "attributes" key of zarr.json - parsed <- read_json(zarr_json_path) - old.zattrs <- if (!is.null(parsed[["attributes"]])) parsed[["attributes"]] else list() - if (overwrite) { - old.zattrs <- old.zattrs[setdiff(names(old.zattrs), names(new.zattrs))] - } else { - new.zattrs <- new.zattrs[setdiff(names(new.zattrs), names(old.zattrs))] - } - parsed[["attributes"]] <- c(old.zattrs, new.zattrs) - json <- toJSON(parsed, auto_unbox = TRUE, pretty = TRUE, null = "null") - write(x = json, file = zarr_json_path) - } else { - # zarr v2: standalone .zattrs file - if (file.exists(zattrs_path)) { - old.zattrs <- read_json(zattrs_path) - if (overwrite) { - old.zattrs <- old.zattrs[setdiff(names(old.zattrs), names(new.zattrs))] - } else { - new.zattrs <- new.zattrs[setdiff(names(new.zattrs), names(old.zattrs))] - } - new.zattrs <- c(old.zattrs, new.zattrs) - } - json <- .format_json(toJSON(new.zattrs, auto_unbox = TRUE, pretty = TRUE, null = "null")) - write(x = json, file = zattrs_path) - } -} .replace_zarr <- function(name, path, replace, version = "v2") { @@ -172,46 +83,3 @@ write_zattrs <- function(path, new.zattrs = list(), overwrite = TRUE){ return(ng) } -#' Normalize a Zarr array path -#' -#' Taken from https://zarr.readthedocs.io/en/stable/spec/v2.html#logical-storage-paths -#' -#' @param path Character vector of length 1 giving the path to be normalised. -#' -#' @returns A character vector of length 1 containing the normalised path. -#' -#' @keywords Internal -.normalize_array_path <- function(path) { - ## we strip the protocol because it gets messed up by the slash removal later - if (grepl(x = path, pattern = "^((https?://)|(s3://)).*$")) { - root <- gsub(x = path, pattern = "^((https?://)|(s3://)).*$", - replacement = "\\1") - path <- gsub(x = path, pattern = "^((https?://)|(s3://))(.*$)", - replacement = "\\4") - } else { - ## Replace all backward slash ("\\") with forward slash ("/") - path <- gsub(x = path, pattern = "\\", replacement = "/", fixed = TRUE) - path <- normalizePath(path, winslash = "/", mustWork = FALSE) - root <- gsub(x = path, "(^[[:alnum:]:.]*/)(.*)", replacement = "\\1") - path <- gsub(x = path, "(^[[:alnum:]:.]*/)(.*)", replacement = "\\2") - } - - ## Strip any leading "/" characters - path <- gsub(x = path, pattern = "^/", replacement = "", fixed = FALSE) - ## Strip any trailing "/" characters - path <- gsub(x = path, pattern = "/$", replacement = "", fixed = FALSE) - ## Collapse any sequence of more than one "/" character into a single "/" - path <- gsub(x = path, pattern = "//*", replacement = "/", fixed = FALSE) - ## The key prefix is then obtained by appending a single "/" character to - ## the normalized logical path. - path <- paste0(root, path, "/") - - return(path) -} - -.format_json <- function(json) { - json <- gsub(x = json, pattern = "[", replacement = "[\n ", fixed = TRUE) - json <- gsub(x = json, pattern = "],", replacement = "\n ],", fixed = TRUE) - json <- gsub(x = json, pattern = ", ", replacement = ",\n ", fixed = TRUE) - return(json) -} \ No newline at end of file diff --git a/tests/testthat/test-zarrutils.R b/tests/testthat/test-zarrutils.R index 133eac2b..6b0708dd 100644 --- a/tests/testthat/test-zarrutils.R +++ b/tests/testthat/test-zarrutils.R @@ -50,11 +50,9 @@ test_that("create zarr/group v3", { expect_true(file.exists(file.path(output_zarr, "zarr.json"))) expect_false(file.exists(file.path(output_zarr, ".zgroup"))) - # check zarr.json content - meta <- jsonlite::read_json(file.path(output_zarr, "zarr.json")) - expect_equal(meta$zarr_format, 3) - expect_equal(meta$node_type, "group") - expect_true(is.list(meta$attributes) && length(meta$attributes) == 0) + # check zarr.json exists and attributes are empty + expect_true(file.exists(file.path(output_zarr, "zarr.json"))) + expect_equal(Rarr::read_zarr_attributes(output_zarr), list()) # create a sub-group create_zarr_group(store = output_zarr, name = "images", version = "v3") @@ -81,30 +79,30 @@ test_that("read/write zattrs", { # add .zattrs to / zattrs <- list(foo = "foo", bar = "bar") - write_zattrs(path = path, new.zattrs = zattrs) + Rarr::write_zarr_attributes(path, new.zattrs = zattrs) expect_true(file.exists(file.path(path, ".zattrs"))) - + # check .zattrs - read.zattrs <- read_zattrs(path) + read.zattrs <- Rarr::read_zarr_attributes(path) expect_equal(read.zattrs, zattrs) - + # add new elements to .zattrs zattrs.new.elem <- list(foo2 = "foo") - write_zattrs(path = path, new.zattrs = zattrs.new.elem) - read.zattrs <- read_zattrs(path) + Rarr::write_zarr_attributes(path, new.zattrs = zattrs.new.elem) + read.zattrs <- Rarr::read_zarr_attributes(path) expect_equal(read.zattrs, c(zattrs,zattrs.new.elem)) - + # overwrite zattrs.new.elem <- list(foo2 = "foo2") - write_zattrs(path = path, new.zattrs = zattrs.new.elem) - read.zattrs <- read_zattrs(path) + Rarr::write_zarr_attributes(path, new.zattrs = zattrs.new.elem) + read.zattrs <- Rarr::read_zarr_attributes(path) zattrs[names(zattrs.new.elem)] <- zattrs.new.elem expect_equal(read.zattrs, c(zattrs)) - + # overwrite = FALSE zattrs.new.elem <- list(foo2 = "foo") - write_zattrs(path = path, new.zattrs = zattrs.new.elem, overwrite = FALSE) - read.zattrs <- read_zattrs(path) + Rarr::write_zarr_attributes(path, new.zattrs = zattrs.new.elem, overwrite = FALSE) + read.zattrs <- Rarr::read_zarr_attributes(path) zattrs[names(zattrs.new.elem)] <- "foo2" expect_equal(read.zattrs, c(zattrs)) @@ -119,33 +117,31 @@ test_that("read/write zattrs v3", { # write attributes into zarr.json zattrs <- list(foo = "foo", bar = "bar") - write_zattrs(path = grp, new.zattrs = zattrs) + Rarr::write_zarr_attributes(grp, new.zattrs = zattrs) expect_true(file.exists(file.path(grp, "zarr.json"))) expect_false(file.exists(file.path(grp, ".zattrs"))) # read back attributes from zarr.json - read.zattrs <- read_zattrs(grp) + read.zattrs <- Rarr::read_zarr_attributes(grp) expect_equal(read.zattrs, zattrs) - # zarr_format / node_type keys in zarr.json must be preserved - meta <- jsonlite::read_json(file.path(grp, "zarr.json")) - expect_equal(meta$zarr_format, 3) - expect_equal(meta$node_type, "group") + # zarr.json must still exist (zarr_format / node_type preserved by Rarr internally) + expect_true(file.exists(file.path(grp, "zarr.json"))) # add new element - write_zattrs(path = grp, new.zattrs = list(baz = "baz")) - read.zattrs <- read_zattrs(grp) + Rarr::write_zarr_attributes(grp, new.zattrs = list(baz = "baz")) + read.zattrs <- Rarr::read_zarr_attributes(grp) expect_equal(read.zattrs, c(zattrs, list(baz = "baz"))) # overwrite existing key - write_zattrs(path = grp, new.zattrs = list(foo = "FOO")) - read.zattrs <- read_zattrs(grp) + Rarr::write_zarr_attributes(grp, new.zattrs = list(foo = "FOO")) + read.zattrs <- Rarr::read_zarr_attributes(grp) expect_equal(read.zattrs$foo, "FOO") expect_equal(read.zattrs$bar, "bar") # untouched key preserved # overwrite = FALSE should not overwrite existing key - write_zattrs(path = grp, new.zattrs = list(foo = "original"), overwrite = FALSE) - read.zattrs <- read_zattrs(grp) + Rarr::write_zarr_attributes(grp, new.zattrs = list(foo = "original"), overwrite = FALSE) + read.zattrs <- Rarr::read_zarr_attributes(grp) expect_equal(read.zattrs$foo, "FOO") # unchanged }) \ No newline at end of file From 3ed81d5d1ce497b132102d529ba1f1dff10bb041 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Wed, 15 Apr 2026 16:06:04 +0200 Subject: [PATCH 148/151] minor clean up --- R/ImageArray.R | 12 +++++++----- R/LabelArray.R | 12 +++++++----- R/sdArray.R | 6 ++++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/R/ImageArray.R b/R/ImageArray.R index 10a0a477..a0e9f812 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -26,8 +26,10 @@ #' @importFrom methods new #' @importFrom DelayedArray DelayedArray #' @export -ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), +ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), multiscale=FALSE, axes = NULL, ...) { + if (!missing(data) && is.list(data) && length(data) == 0) + stop("'data' must not be an empty list; use ImageArray() for an empty placeholder") if(!is.list(data)){ if(multiscale){ data <- .generate_multiscale(data, axes = axes, method = "image") @@ -35,11 +37,11 @@ ImageArray <- function(data=list(), meta=Zattrs(), metadata=list(), data <- list(DelayedArray::DelayedArray(data)) } } - if(length(meta) < 1){ - meta <- .make_image_meta(data, - version = 0.4, + if(length(meta) < 1 && length(data) > 0){ + meta <- .make_image_meta(data, + version = 0.4, axes = axes) - } + } x <- .ImageArray(data=data, meta=meta, ...) metadata(x) <- metadata return(x) diff --git a/R/LabelArray.R b/R/LabelArray.R index 08285ff0..1da4cee2 100644 --- a/R/LabelArray.R +++ b/R/LabelArray.R @@ -39,8 +39,10 @@ #' @importFrom S4Vectors metadata<- #' @importFrom methods new #' @export -LabelArray <- function(data=list(), meta=Zattrs(), metadata=list(), +LabelArray <- function(data=list(), meta=Zattrs(), metadata=list(), multiscale = FALSE, axes = NULL, ...) { + if (!missing(data) && is.list(data) && length(data) == 0) + stop("'data' must not be an empty list; use LabelArray() for an empty placeholder") if(!is.list(data)){ if(multiscale){ data <- .generate_multiscale(data, axes = axes, method = "label") @@ -48,11 +50,11 @@ LabelArray <- function(data=list(), meta=Zattrs(), metadata=list(), data <- list(DelayedArray::DelayedArray(data)) } } - if(length(meta) < 1){ - meta <- .make_label_meta(data, - version = 0.4, + if(length(meta) < 1 && length(data) > 0){ + meta <- .make_label_meta(data, + version = 0.4, axes = axes) - } + } x <- .LabelArray(data=data, meta=meta, ...) metadata(x) <- metadata return(x) diff --git a/R/sdArray.R b/R/sdArray.R index b613a065..47d22cce 100644 --- a/R/sdArray.R +++ b/R/sdArray.R @@ -111,5 +111,11 @@ setMethod("data_type", "DelayedArray", \(x) zarr_overview(path(x), as_data_frame perm = rev(seq_len(length(axes)))) } } + if (method == "label") { + image_list <- lapply(image_list, function(x) { + storage.mode(x) <- "integer" + x + }) + } image_list } \ No newline at end of file From da7314bdaa85c99257d0954547594d01da5f8419 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Wed, 15 Apr 2026 16:06:27 +0200 Subject: [PATCH 149/151] add jsonlite again, update documentation --- DESCRIPTION | 3 ++- NAMESPACE | 9 ++------- man/SpatialData.Rd | 6 ++---- man/dot-normalize_array_path.Rd | 18 ------------------ man/read_zattrs.Rd | 22 ---------------------- man/write_zattrs.Rd | 19 ------------------- 6 files changed, 6 insertions(+), 71 deletions(-) delete mode 100644 man/dot-normalize_array_path.Rd delete mode 100644 man/read_zattrs.Rd delete mode 100644 man/write_zattrs.Rd diff --git a/DESCRIPTION b/DESCRIPTION index ad926741..57838bf6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -54,7 +54,8 @@ Imports: SingleCellExperiment, SummarizedExperiment, EBImage, - stringr + stringr, + jsonlite Suggests: BiocStyle, ggnewscale, diff --git a/NAMESPACE b/NAMESPACE index 5336b278..661f76de 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,13 +24,11 @@ export(readPoint) export(readShape) export(readSpatialData) export(readTable) -export(read_zattrs) export(writeImage) export(writeLabel) export(writePoint) export(writeShape) export(writeSpatialData) -export(write_zattrs) exportClasses(SpatialData) exportMethods("$") exportMethods("[") @@ -109,8 +107,7 @@ importFrom(BiocGenerics,as.data.frame) importFrom(BiocGenerics,colnames) importFrom(BiocGenerics,rownames) importFrom(DelayedArray,DelayedArray) -importFrom(DelayedArray,DelayedArray) -importFrom(EBImage,resize) +importFrom(DelayedArray,realize) importFrom(EBImage,resize) importFrom(EBImage,rotate) importFrom(EBImage,translate) @@ -164,7 +161,7 @@ importFrom(graph,graph.par) importFrom(graph,graphAM) importFrom(graph,nodeData) importFrom(graph,nodes) -importFrom(jsonlite,read_json) +importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) importFrom(methods,as) importFrom(methods,callNextMethod) @@ -184,8 +181,6 @@ importFrom(sf,st_geometry_type) importFrom(sf,st_intersects) importFrom(sf,st_polygon) importFrom(stats,setNames) -importFrom(stringr,str_extract) -importFrom(stringr,str_remove) importFrom(utils,.DollarNames) importFrom(utils,head) importFrom(utils,tail) diff --git a/man/SpatialData.Rd b/man/SpatialData.Rd index 913adda8..671a894f 100644 --- a/man/SpatialData.Rd +++ b/man/SpatialData.Rd @@ -52,8 +52,6 @@ \alias{element,SpatialData,ANY,numeric-method} \alias{element,SpatialData,ANY,missing-method} \alias{element,SpatialData,ANY,ANY-method} -\alias{[[<-,SpatialData,numeric,ANY,ANY-method} -\alias{[[<-,SpatialData,character,ANY,ANY-method} \title{The `SpatialData` class} \usage{ SpatialData(images, labels, points, shapes, tables) @@ -90,9 +88,9 @@ SpatialData(images, labels, points, shapes, tables) \S4method{element}{SpatialData,ANY,ANY}(x, i, j) -\S4method{[[}{SpatialData,numeric,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,numeric,ANY}(x, i) <- value -\S4method{[[}{SpatialData,character,ANY,ANY}(x, i) <- value +\S4method{[[}{SpatialData,character,ANY}(x, i) <- value } \arguments{ \item{images}{list of \code{\link{ImageArray}}s} diff --git a/man/dot-normalize_array_path.Rd b/man/dot-normalize_array_path.Rd deleted file mode 100644 index 8a491cab..00000000 --- a/man/dot-normalize_array_path.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/zarr_utils.R -\name{.normalize_array_path} -\alias{.normalize_array_path} -\title{Normalize a Zarr array path} -\usage{ -.normalize_array_path(path) -} -\arguments{ -\item{path}{Character vector of length 1 giving the path to be normalised.} -} -\value{ -A character vector of length 1 containing the normalised path. -} -\description{ -Taken from https://zarr.readthedocs.io/en/stable/spec/v2.html#logical-storage-paths -} -\keyword{Internal} diff --git a/man/read_zattrs.Rd b/man/read_zattrs.Rd deleted file mode 100644 index ccd83f36..00000000 --- a/man/read_zattrs.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/zarr_utils.R -\name{read_zattrs} -\alias{read_zattrs} -\title{Read the .zattrs file associated with a Zarr array or group} -\usage{ -read_zattrs(path, s3_client = NULL) -} -\arguments{ -\item{path}{A character vector of length 1. This provides the -path to a Zarr array or group. This can either be on a local file -system or on S3 storage.} - -\item{s3_client}{A list representing an S3 client. This should be produced -by [paws.storage::s3()].} -} -\value{ -A list containing the .zattrs elements -} -\description{ -Read the .zattrs file associated with a Zarr array or group -} diff --git a/man/write_zattrs.Rd b/man/write_zattrs.Rd deleted file mode 100644 index 944db4ea..00000000 --- a/man/write_zattrs.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/zarr_utils.R -\name{write_zattrs} -\alias{write_zattrs} -\title{Read the .zattrs file associated with a Zarr array or group} -\usage{ -write_zattrs(path, new.zattrs = list(), overwrite = TRUE) -} -\arguments{ -\item{path}{A character vector of length 1. This provides the -path to a Zarr array or group.} - -\item{new.zattrs}{a list inserted to .zattrs at the \code{path}.} - -\item{overwrite}{if TRUE, existing .zattrs elements will be overwritten by \code{new.zattrs}.} -} -\description{ -Read the .zattrs file associated with a Zarr array or group -} From 42e472ca66db26a7dc703f146113a3502296d2b4 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Thu, 16 Apr 2026 14:01:10 +0200 Subject: [PATCH 150/151] correct metadata writing in zarr.json files --- R/write.R | 70 ++++++++++++++++++++++++++++++++++++++++++-------- R/zarr_utils.R | 8 +++++- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/R/write.R b/R/write.R index 3181696e..9cfae7e6 100644 --- a/R/write.R +++ b/R/write.R @@ -31,12 +31,36 @@ #' NULL +# For zarr v3, OME-NGFF content (multiscales, omero, image-label) must be +# nested under an "ome" key inside "attributes"; spatialdata_attrs stays at top. +# If the metadata was read from a v3 store it already has "ome", so skip wrapping. +.wrap_ome_for_v3 <- function(zattrs, version) { + if (version != "v3" || "ome" %in% names(zattrs)) return(as.list(zattrs)) + ome_keys <- setdiff(names(zattrs), "spatialdata_attrs") + ome_content <- as.list(zattrs)[ome_keys] + # Strip v2-only fields from each multiscales entry + if (!is.null(ome_content$multiscales)) { + ome_content$multiscales <- lapply(ome_content$multiscales, function(ms) { + ms[setdiff(names(ms), c("version", "metadata"))] + }) + } + list( + ome = c(list(version = "0.5-dev-spatialdata"), ome_content), + spatialdata_attrs = zattrs[["spatialdata_attrs"]] + ) +} + #' @rdname writeSpatialData #' @export -writeSpatialData <- function(x, name, path, replace = TRUE, version = "v2", +writeSpatialData <- function(x, name, path, replace = TRUE, version = "v2", ...) { zarr.path <- .replace_zarr(name, path, replace, version) - + + # write root-level spatialdata_attrs for v3 (Python uses this to pick the read path) + if (version == "v3") + Rarr::write_zarr_attributes(zarr.path, + new.zattrs = list(spatialdata_attrs = list(version = "0.2"))) + # write points . <- lapply(pointNames(x), \(.){ writePoint(point(x, .),., path = zarr.path, replace = replace, version = version) @@ -56,6 +80,17 @@ writeSpatialData <- function(x, name, path, replace = TRUE, version = "v2", . <- lapply(labelNames(x), \(.){ writeLabel(label(x, .),., path = zarr.path, replace = replace, version = version) }) + + # write labels group metadata listing all label names (required by spatialdata spec) + # v2: {"labels": [...]}, v3: {"ome": {"labels": [...]}} + lnames <- labelNames(x) + if (length(lnames) > 0L) { + labels.dir <- file.path(zarr.path, "labels") + lnames_zattrs <- if (version == "v3") + list(ome = list(labels = as.list(lnames))) else + list(labels = as.list(lnames)) + Rarr::write_zarr_attributes(labels.dir, new.zattrs = lnames_zattrs) + } } #' @rdname writeSpatialData @@ -64,9 +99,12 @@ writePoint <- function(x, name, path, replace = TRUE, version = "v2") { # if no PointFrames were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "points"), replace, version) # write meta - Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) + zattrs <- as.list(meta(x)) + if (version == "v3") zattrs$spatialdata_attrs$version <- "0.2" + Rarr::write_zarr_attributes(zarr.group, new.zattrs = zattrs) # write data - arrow::write_dataset(data(x), file.path(zarr.group, "points.parquet")) + arrow::write_dataset(data(x), file.path(zarr.group, "points.parquet"), + basename_template = "part.{i}.parquet") } #' @rdname writeSpatialData @@ -75,9 +113,11 @@ writeShape <- function(x, name, path, replace = TRUE, version = "v2") { # if no ShapeFrames were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "shapes"), replace, version) # write meta - Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) - # write data - arrow::write_dataset(data(x), file.path(zarr.group, "shapes.parquet")) + zattrs <- as.list(meta(x)) + if (version == "v3") zattrs$spatialdata_attrs$version <- "0.3" + Rarr::write_zarr_attributes(zarr.group, new.zattrs = zattrs) + # write data as a single parquet file (matches Python spatialdata convention) + arrow::write_parquet(data(x), file.path(zarr.group, "shapes.parquet")) } #' @rdname writeSpatialData @@ -88,8 +128,10 @@ writeImage <- function(x, name, path, replace = TRUE, version = "v2") { # if no ImageArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "images"), replace, version) zarr_version <- if (version == "v3") 3L else 2L - # write meta - Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) + # write meta: for v3, OME-NGFF content goes under "ome" key in attributes + zattrs <- .wrap_ome_for_v3(meta(x), version) + if (version == "v3") zattrs$spatialdata_attrs$version <- "0.3" + Rarr::write_zarr_attributes(zarr.group, new.zattrs = zattrs) # write data lapply( .get_multiscales_dataset_paths(meta(x)), @@ -98,6 +140,8 @@ writeImage <- function(x, name, path, replace = TRUE, version = "v2") { Rarr::write_zarr_array(realize(da), zarr_array_path = file.path(zarr.group, .), chunk_dim = dim(da), + order = "C", + dimension_separator = "/", zarr_version = zarr_version) } ) @@ -111,8 +155,10 @@ writeLabel <- function(x, name, path, replace = TRUE, version = "v2") { # if no LabelArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "labels"), replace, version) zarr_version <- if (version == "v3") 3L else 2L - # write meta - Rarr::write_zarr_attributes(zarr.group, new.zattrs = meta(x)) + # write meta: for v3, OME-NGFF content goes under "ome" key in attributes + zattrs <- .wrap_ome_for_v3(meta(x), version) + if (version == "v3") zattrs$spatialdata_attrs$version <- "0.3" + Rarr::write_zarr_attributes(zarr.group, new.zattrs = zattrs) # write data lapply( .get_multiscales_dataset_paths(meta(x)), @@ -121,6 +167,8 @@ writeLabel <- function(x, name, path, replace = TRUE, version = "v2") { Rarr::write_zarr_array(realize(da), zarr_array_path = file.path(zarr.group, .), chunk_dim = dim(da), + order = "C", + dimension_separator = "/", zarr_version = zarr_version) } ) diff --git a/R/zarr_utils.R b/R/zarr_utils.R index 816d546f..e9a824aa 100644 --- a/R/zarr_utils.R +++ b/R/zarr_utils.R @@ -67,8 +67,14 @@ create_zarr <- function(name, dir, version = "v2"){ .make_zarr_group <- function(x, name, path, replace, version){ # gd <- file.path(path, "points") - if(!dir.exists(path)) + if(!dir.exists(path)) { dir.create(path) + switch(version, + v2 = write('{"zarr_format":2}', file = file.path(path, ".zgroup")), + v3 = write('{"zarr_format":3,"node_type":"group","attributes":{}}', + file = file.path(path, "zarr.json")) + ) + } ng <- file.path(path, name) if(replace){ unlink(ng, recursive = TRUE) From dd369ec216f77de165dcd70ad10d6c5b1dcbdf77 Mon Sep 17 00:00:00 2001 From: kollo97 Date: Fri, 24 Apr 2026 16:21:10 +0200 Subject: [PATCH 151/151] move helper to zarr_utils --- R/write.R | 44 +++++++++------------- R/zarr_utils.R | 63 ++++++++++++++++++++++++++++++++ tests/testthat/test-imagearray.R | 27 +++++++++++++- 3 files changed, 107 insertions(+), 27 deletions(-) diff --git a/R/write.R b/R/write.R index 9cfae7e6..ccd29f41 100644 --- a/R/write.R +++ b/R/write.R @@ -31,25 +31,6 @@ #' NULL -# For zarr v3, OME-NGFF content (multiscales, omero, image-label) must be -# nested under an "ome" key inside "attributes"; spatialdata_attrs stays at top. -# If the metadata was read from a v3 store it already has "ome", so skip wrapping. -.wrap_ome_for_v3 <- function(zattrs, version) { - if (version != "v3" || "ome" %in% names(zattrs)) return(as.list(zattrs)) - ome_keys <- setdiff(names(zattrs), "spatialdata_attrs") - ome_content <- as.list(zattrs)[ome_keys] - # Strip v2-only fields from each multiscales entry - if (!is.null(ome_content$multiscales)) { - ome_content$multiscales <- lapply(ome_content$multiscales, function(ms) { - ms[setdiff(names(ms), c("version", "metadata"))] - }) - } - list( - ome = c(list(version = "0.5-dev-spatialdata"), ome_content), - spatialdata_attrs = zattrs[["spatialdata_attrs"]] - ) -} - #' @rdname writeSpatialData #' @export writeSpatialData <- function(x, name, path, replace = TRUE, version = "v2", @@ -128,6 +109,7 @@ writeImage <- function(x, name, path, replace = TRUE, version = "v2") { # if no ImageArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "images"), replace, version) zarr_version <- if (version == "v3") 3L else 2L + dimension_names <- .get_multiscale_axes(meta(x)) # write meta: for v3, OME-NGFF content goes under "ome" key in attributes zattrs <- .wrap_ome_for_v3(meta(x), version) if (version == "v3") zattrs$spatialdata_attrs$version <- "0.3" @@ -136,13 +118,18 @@ writeImage <- function(x, name, path, replace = TRUE, version = "v2") { lapply( .get_multiscales_dataset_paths(meta(x)), \(.){ - da <- data(x, . + 1) - Rarr::write_zarr_array(realize(da), + arr <- realize(data(x, . + 1)) + # Rarr reads names(dimnames(x)) to write dimension_names in v3 zarr.json + if (!is.null(dimension_names)) + dimnames(arr) <- setNames(vector("list", length(dim(arr))), dimension_names) + Rarr::write_zarr_array(arr, zarr_array_path = file.path(zarr.group, .), - chunk_dim = dim(da), + chunk_dim = dim(arr), order = "C", dimension_separator = "/", zarr_version = zarr_version) + if (version == "v3") + .normalize_v3_array_metadata(file.path(zarr.group, .)) } ) } @@ -155,6 +142,7 @@ writeLabel <- function(x, name, path, replace = TRUE, version = "v2") { # if no LabelArray were written before, update zarr store zarr.group <- .make_zarr_group(x, name, file.path(path, "labels"), replace, version) zarr_version <- if (version == "v3") 3L else 2L + dimension_names <- .get_multiscale_axes(meta(x)) # write meta: for v3, OME-NGFF content goes under "ome" key in attributes zattrs <- .wrap_ome_for_v3(meta(x), version) if (version == "v3") zattrs$spatialdata_attrs$version <- "0.3" @@ -163,13 +151,17 @@ writeLabel <- function(x, name, path, replace = TRUE, version = "v2") { lapply( .get_multiscales_dataset_paths(meta(x)), \(.){ - da <- data(x, . + 1) - Rarr::write_zarr_array(realize(da), + arr <- realize(data(x, . + 1)) + if (!is.null(dimension_names)) + dimnames(arr) <- setNames(vector("list", length(dim(arr))), dimension_names) + Rarr::write_zarr_array(arr, zarr_array_path = file.path(zarr.group, .), - chunk_dim = dim(da), + chunk_dim = dim(arr), order = "C", dimension_separator = "/", zarr_version = zarr_version) + if (version == "v3") + .normalize_v3_array_metadata(file.path(zarr.group, .)) } ) -} \ No newline at end of file +} diff --git a/R/zarr_utils.R b/R/zarr_utils.R index e9a824aa..970dd3b1 100644 --- a/R/zarr_utils.R +++ b/R/zarr_utils.R @@ -89,3 +89,66 @@ create_zarr <- function(name, dir, version = "v2"){ return(ng) } + +# For zarr v3, OME-NGFF content (multiscales, omero, image-label) must be +# nested under an "ome" key inside "attributes"; spatialdata_attrs stays at top. +# If the metadata was read from a v3 store it already has "ome", so skip wrapping. +.wrap_ome_for_v3 <- function(zattrs, version) { + if (version != "v3" || "ome" %in% names(zattrs)) return(as.list(zattrs)) + ome_keys <- setdiff(names(zattrs), "spatialdata_attrs") + ome_content <- as.list(zattrs)[ome_keys] + # Strip v2-only fields from each multiscales entry + if (!is.null(ome_content$multiscales)) { + ome_content$multiscales <- lapply(ome_content$multiscales, function(ms) { + ms[setdiff(names(ms), c("version", "metadata"))] + }) + } + list( + ome = c(list(version = "0.5-dev-spatialdata"), ome_content), + spatialdata_attrs = zattrs[["spatialdata_attrs"]] + ) +} + +.get_multiscale_axes <- function(zattrs) { + multiscales <- zattrs[["multiscales"]] + if (is.null(multiscales) && !is.null(zattrs[["ome"]])) + multiscales <- zattrs[["ome"]][["multiscales"]] + if (is.null(multiscales) || length(multiscales) == 0L) return(NULL) + axes <- multiscales[[1]][["axes"]] + if (is.null(axes) || length(axes) == 0L) return(NULL) + vapply(axes, `[[`, character(1), "name") +} + +# Post-processes Rarr-written v3 array zarr.json: +# 1. Sorts codecs to required order [array-array → array-bytes → bytes-bytes]. +# Rarr currently serialises them as [transpose, zstd, bytes] which Python rejects. +# 2. Adds "attributes": {} and "storage_transformers": [] which Python zarr expects +# but Rarr does not emit. +# dimension_names are handled upstream by setting names(dimnames()) before write_zarr_array. +.normalize_v3_array_metadata <- function(zarr_array_path) { + metadata_path <- file.path(zarr_array_path, "zarr.json") + if (!file.exists(metadata_path)) return(invisible(FALSE)) + + metadata <- jsonlite::read_json(metadata_path, simplifyVector = FALSE) + codecs <- metadata[["codecs"]] + if (!is.null(codecs) && length(codecs) > 1L) { + codec_names <- vapply(codecs, `[[`, character(1), "name") + codec_stage <- ifelse( + codec_names %in% "transpose", 1L, + ifelse(codec_names %in% c("bytes", "vlen-utf8", "vlen_utf8"), 2L, 3L) + ) + metadata[["codecs"]] <- codecs[order(codec_stage)] + } + + if (is.null(metadata[["attributes"]])) metadata[["attributes"]] <- list() + if (is.null(metadata[["storage_transformers"]])) metadata[["storage_transformers"]] <- list() + + jsonlite::write_json( + metadata, + path = metadata_path, + auto_unbox = TRUE, + pretty = 4, + null = "null" + ) + invisible(TRUE) +} diff --git a/tests/testthat/test-imagearray.R b/tests/testthat/test-imagearray.R index e9f70408..3acb046f 100644 --- a/tests/testthat/test-imagearray.R +++ b/tests/testthat/test-imagearray.R @@ -149,4 +149,29 @@ test_that("write multiscale", { expect_identical(realize(data(imgarray, 3)), realize(data(imgarray2, 3))) expect_identical(meta(imgarray),meta(imgarray2)) -}) \ No newline at end of file +}) + +test_that("write v3 uses Python-readable codec ordering", { + td <- tempdir() + zarr.path <- file.path(td, "test_v3.zarr") + unlink(zarr.path, recursive = TRUE) + + set.seed(1) + img <- array(sample(1:255, size = 20 * 20 * 3, replace = TRUE), + dim = c(3, 20, 20)) + imgarray <- ImageArray(img, axes = c("c", "y", "x")) + sd <- SpatialData(images = list(test_image = imgarray)) + + writeSpatialData(sd, "test_v3.zarr", path = td, version = "v3") + + metadata <- jsonlite::read_json( + file.path(zarr.path, "images", "test_image", "0", "zarr.json"), + simplifyVector = FALSE + ) + codec_names <- vapply(metadata$codecs, `[[`, character(1), "name") + + expect_identical(codec_names, c("transpose", "bytes", "zstd")) + expect_equal(unname(unlist(metadata$dimension_names)), c("c", "y", "x")) + expect_equal(metadata$attributes, list()) + expect_equal(metadata$storage_transformers, list()) +})