Skip to content

Commit f0eb248

Browse files
Merge branch 'xan/revert-SOMAContext' of https://github.com/single-cell-data/TileDB-SOMA into xan/revert-SOMAContext
2 parents 1aa0856 + 217b838 commit f0eb248

9 files changed

Lines changed: 222 additions & 7 deletions

File tree

apis/python/HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2727

2828
### Fixed
2929

30+
- `PlatformConfig.dense_nd_array_dim_zstd_level` was incorrectly aliased to `sparse_nd_array_dim_zstd_level` and is now bound to the correct field.
31+
3032
### Security
3133

3234
## [2.3.0]

apis/python/src/tiledbsoma/_soma_array.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def schema(self) -> pa.Schema:
3434

3535
def schema_config_options(self) -> clib.PlatformSchemaConfig:
3636
"""Returns metadata about the array schema that is not encompassed within
37-
the Arrow Schema, in the form of a PlatformConfig.
37+
the Arrow Schema, in the form of a PlatformSchemaConfig.
3838
3939
Available attributes are:
4040
* capacity: int

apis/python/src/tiledbsoma/pytiledbsoma.cc

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <tiledbsoma/tiledbsoma>
22

3+
#include <sstream>
4+
35
#include <pybind11/numpy.h>
46
#include <pybind11/pybind11.h>
57
#include <pybind11/pytypes.h>
@@ -154,7 +156,7 @@ PYBIND11_MODULE(pytiledbsoma, m) {
154156
.def(py::init<>())
155157
.def_readwrite("dataframe_dim_zstd_level", &PlatformConfig::dataframe_dim_zstd_level)
156158
.def_readwrite("sparse_nd_array_dim_zstd_level", &PlatformConfig::sparse_nd_array_dim_zstd_level)
157-
.def_readwrite("dense_nd_array_dim_zstd_level", &PlatformConfig::sparse_nd_array_dim_zstd_level)
159+
.def_readwrite("dense_nd_array_dim_zstd_level", &PlatformConfig::dense_nd_array_dim_zstd_level)
158160
.def_readwrite("write_X_chunked", &PlatformConfig::write_X_chunked)
159161
.def_readwrite("goal_chunk_nnz", &PlatformConfig::goal_chunk_nnz)
160162
.def_readwrite("remote_cap_nbytes", &PlatformConfig::remote_cap_nbytes)
@@ -166,7 +168,45 @@ PYBIND11_MODULE(pytiledbsoma, m) {
166168
.def_readwrite("allows_duplicates", &PlatformConfig::allows_duplicates)
167169
.def_readwrite("tile_order", &PlatformConfig::tile_order)
168170
.def_readwrite("cell_order", &PlatformConfig::cell_order)
169-
.def_readwrite("consolidate_and_vacuum", &PlatformConfig::consolidate_and_vacuum);
171+
.def_readwrite("consolidate_and_vacuum", &PlatformConfig::consolidate_and_vacuum)
172+
.def(
173+
"to_dict",
174+
[](const PlatformConfig& c) {
175+
return py::dict(
176+
"dataframe_dim_zstd_level"_a = c.dataframe_dim_zstd_level,
177+
"sparse_nd_array_dim_zstd_level"_a = c.sparse_nd_array_dim_zstd_level,
178+
"dense_nd_array_dim_zstd_level"_a = c.dense_nd_array_dim_zstd_level,
179+
"write_X_chunked"_a = c.write_X_chunked,
180+
"goal_chunk_nnz"_a = c.goal_chunk_nnz,
181+
"remote_cap_nbytes"_a = c.remote_cap_nbytes,
182+
"capacity"_a = c.capacity,
183+
"offsets_filters"_a = c.offsets_filters,
184+
"validity_filters"_a = c.validity_filters,
185+
"allows_duplicates"_a = c.allows_duplicates,
186+
"tile_order"_a = c.tile_order,
187+
"cell_order"_a = c.cell_order,
188+
"attrs"_a = c.attrs,
189+
"dims"_a = c.dims,
190+
"consolidate_and_vacuum"_a = c.consolidate_and_vacuum);
191+
})
192+
.def("__repr__", [](const PlatformConfig& c) {
193+
auto q = [](const std::optional<std::string>& v) { return v ? ("'" + *v + "'") : std::string("None"); };
194+
std::ostringstream os;
195+
os << "PlatformConfig("
196+
<< "dataframe_dim_zstd_level=" << c.dataframe_dim_zstd_level
197+
<< ", sparse_nd_array_dim_zstd_level=" << c.sparse_nd_array_dim_zstd_level
198+
<< ", dense_nd_array_dim_zstd_level=" << c.dense_nd_array_dim_zstd_level
199+
<< ", write_X_chunked=" << (c.write_X_chunked ? "True" : "False")
200+
<< ", goal_chunk_nnz=" << c.goal_chunk_nnz << ", remote_cap_nbytes=" << c.remote_cap_nbytes
201+
<< ", capacity=" << c.capacity << ", offsets_filters='" << c.offsets_filters << "'"
202+
<< ", validity_filters='" << c.validity_filters << "'"
203+
<< ", allows_duplicates=" << (c.allows_duplicates ? "True" : "False")
204+
<< ", tile_order=" << q(c.tile_order) << ", cell_order=" << q(c.cell_order) << ", attrs='" << c.attrs
205+
<< "'"
206+
<< ", dims='" << c.dims << "'"
207+
<< ", consolidate_and_vacuum=" << (c.consolidate_and_vacuum ? "True" : "False") << ")";
208+
return os.str();
209+
});
170210

171211
py::class_<PlatformSchemaConfig>(m, "PlatformSchemaConfig")
172212
.def(py::init<>())
@@ -177,7 +217,33 @@ PYBIND11_MODULE(pytiledbsoma, m) {
177217
.def_readwrite("dims", &PlatformSchemaConfig::dims)
178218
.def_readwrite("allows_duplicates", &PlatformSchemaConfig::allows_duplicates)
179219
.def_readwrite("tile_order", &PlatformSchemaConfig::tile_order)
180-
.def_readwrite("cell_order", &PlatformSchemaConfig::cell_order);
220+
.def_readwrite("cell_order", &PlatformSchemaConfig::cell_order)
221+
.def(
222+
"to_dict",
223+
[](const PlatformSchemaConfig& c) {
224+
return py::dict(
225+
"capacity"_a = c.capacity,
226+
"offsets_filters"_a = c.offsets_filters,
227+
"validity_filters"_a = c.validity_filters,
228+
"attrs"_a = c.attrs,
229+
"dims"_a = c.dims,
230+
"allows_duplicates"_a = c.allows_duplicates,
231+
"tile_order"_a = c.tile_order,
232+
"cell_order"_a = c.cell_order);
233+
})
234+
.def("__repr__", [](const PlatformSchemaConfig& c) {
235+
auto q = [](const std::optional<std::string>& v) { return v ? ("'" + *v + "'") : std::string("None"); };
236+
std::ostringstream os;
237+
os << "PlatformSchemaConfig("
238+
<< "capacity=" << c.capacity << ", allows_duplicates=" << (c.allows_duplicates ? "True" : "False")
239+
<< ", tile_order=" << q(c.tile_order) << ", cell_order=" << q(c.cell_order) << ", offsets_filters='"
240+
<< c.offsets_filters << "'"
241+
<< ", validity_filters='" << c.validity_filters << "'"
242+
<< ", attrs='" << c.attrs << "'"
243+
<< ", dims='" << c.dims << "'"
244+
<< ")";
245+
return os.str();
246+
});
181247

182248
m.def("_update_dataframe_schema", &SOMADataFrame::update_dataframe_schema);
183249

apis/python/tests/test_platform_config.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import tiledbsoma
77
import tiledbsoma.io
88
import tiledbsoma.options._tiledb_create_write_options as tco
9+
from tiledbsoma import pytiledbsoma as clib
910

1011
from ._util import assert_adata_equal
1112

@@ -134,3 +135,97 @@ def test_dig_platform_config():
134135
# Unrecognized type (at tip)
135136
with pytest.raises(TypeError):
136137
tco._dig_platform_config({"a": {"b": "invalid"}}, int, ("a", "b"))
138+
139+
140+
def test_platform_schema_config_to_dict_keys():
141+
cfg = clib.PlatformSchemaConfig()
142+
d = cfg.to_dict()
143+
assert type(d) is dict
144+
assert set(d) == {
145+
"capacity",
146+
"offsets_filters",
147+
"validity_filters",
148+
"attrs",
149+
"dims",
150+
"allows_duplicates",
151+
"tile_order",
152+
"cell_order",
153+
}
154+
155+
156+
def test_platform_schema_config_to_dict_values_match_attrs():
157+
cfg = clib.PlatformSchemaConfig()
158+
cfg.capacity = 1234
159+
cfg.tile_order = "row-major"
160+
d = cfg.to_dict()
161+
assert d["capacity"] == 1234
162+
assert d["tile_order"] == "row-major"
163+
assert d["cell_order"] is None
164+
assert isinstance(d["allows_duplicates"], bool)
165+
166+
167+
def test_platform_schema_config_to_dict_json_fields_parse(conftest_pbmc_small, tmp_path):
168+
output_path = tmp_path.as_posix()
169+
create_cfg = {
170+
"capacity": 8888,
171+
"offsets_filters": [
172+
"RleFilter",
173+
{"_type": "GzipFilter", "level": 7},
174+
"NoOpFilter",
175+
],
176+
"dims": {
177+
"soma_dim_0": {"tile": 6, "filters": ["RleFilter"]},
178+
"soma_dim_1": {"filters": []},
179+
},
180+
"attrs": {"soma_data": {"filters": ["NoOpFilter"]}},
181+
"cell_order": "row-major",
182+
"tile_order": "column-major",
183+
}
184+
tiledbsoma.io.from_anndata(
185+
output_path,
186+
conftest_pbmc_small,
187+
"RNA",
188+
platform_config={"tiledb": {"create": create_cfg}},
189+
)
190+
x_arr_uri = str(Path(output_path) / "ms" / "RNA" / "X" / "data")
191+
with tiledbsoma.SparseNDArray.open(x_arr_uri) as x_arr:
192+
cfg = x_arr.schema_config_options()
193+
d = cfg.to_dict()
194+
assert d["capacity"] == create_cfg["capacity"]
195+
assert d["tile_order"] == create_cfg["tile_order"]
196+
assert d["cell_order"] == create_cfg["cell_order"]
197+
# JSON-string fields should parse cleanly
198+
assert isinstance(json.loads(d["offsets_filters"]), list)
199+
assert isinstance(json.loads(d["attrs"]), dict)
200+
assert isinstance(json.loads(d["dims"]), dict)
201+
202+
203+
def test_platform_schema_config_repr():
204+
cfg = clib.PlatformSchemaConfig()
205+
r = repr(cfg)
206+
assert r.startswith("PlatformSchemaConfig(")
207+
assert r.endswith(")")
208+
assert "capacity=" in r
209+
assert "tile_order=None" in r
210+
211+
212+
def test_platform_config_to_dict_keys():
213+
cfg = clib.PlatformConfig()
214+
d = cfg.to_dict()
215+
assert type(d) is dict
216+
assert {"capacity", "tile_order", "consolidate_and_vacuum"} <= set(d)
217+
218+
219+
def test_platform_config_repr():
220+
cfg = clib.PlatformConfig()
221+
assert repr(cfg).startswith("PlatformConfig(")
222+
223+
224+
def test_platform_config_dense_zstd_level_not_aliased():
225+
"""Regression: pytiledbsoma.cc:157 used to bind dense_nd_array_dim_zstd_level
226+
to &PlatformConfig::sparse_nd_array_dim_zstd_level."""
227+
cfg = clib.PlatformConfig()
228+
cfg.sparse_nd_array_dim_zstd_level = 1
229+
cfg.dense_nd_array_dim_zstd_level = 9
230+
assert cfg.sparse_nd_array_dim_zstd_level == 1
231+
assert cfg.dense_nd_array_dim_zstd_level == 9

apis/r/DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Description: Interface for working with 'TileDB'-based Stack of Matrices,
66
like those commonly used for single cell data analysis. It is documented at
77
<https://github.com/single-cell-data>; a formal specification available is at
88
<https://github.com/single-cell-data/SOMA/blob/main/abstract_specification.md>.
9-
Version: 2.3.99.4
9+
Version: 2.3.99.5
1010
Authors@R: c(
1111
person(given = "Paul", family = "Hoffman",
1212
role = c("cre", "aut"), email = "tiledb-r@tiledb.com",

apis/r/NEWS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
- Metadata values retrieved from collection-based classes (`SOMACollection`, `SOMAExperiment`, `SOMAMeasurement`) are no longer wrapped in a named list, consistent with array-based classes ([#4429](https://github.com/single-cell-data/TileDB-SOMA/pull/4429)).
3030
- [BREAKING] Use stored handle to access `SOMAArrayBase` properties rather than re-opening the `SOMAArrayBase`. Array properties can no longer be accessed on an unopened array. ([#4414](https://github.com/single-cell-data/TileDB-SOMA/pull/4414))
3131
- Use stored handle to read and write to `SOMASparseNDArray`, `SOMADenseNDArray`, and `SOMADataFrame` rather than re-opening the SOMA oobjects for each read/write. ([#4414](https://github.com/single-cell-data/TileDB-SOMA/pull/4414))
32-
- Properly close `ms` collection handle in `write_soma.SummarizedExperiment()`, ensuring that new measurements added to the ms collection are persisted to disk. ([#4452](https://github.com/single-cell-data/TileDB-SOMA/pull/4452))
32+
- Properly close `ms` collection handle in `write_soma.SummarizedExperiment()`, ensuring that new measurements added to the `ms` collection are persisted to disk. ([#4452](https://github.com/single-cell-data/TileDB-SOMA/pull/4452))
33+
- Fix handling of empty arrays when calling `write_soma()` with `ingest_mode = "resume"`. ([#4443](https://github.com/single-cell-data/TileDB-SOMA/pull/4453))
3334

3435
## Security
3536

apis/r/R/write_soma.R

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,13 @@ write_soma.TsparseMatrix <- function(
716716
j = bit64::as.integer64(methods::slot(x, "j")),
717717
x = methods::slot(x, "x")
718718
)
719-
tbl <- tbl[-which(tbl$i %in% row_ids & tbl$j %in% col_ids), , drop = FALSE]
719+
720+
# Only subset the table if there are existing joinid matches
721+
idx <- which(tbl$i %in% row_ids & tbl$j %in% col_ids)
722+
if (length(idx) > 0) {
723+
tbl <- tbl[-idx, , drop = FALSE]
724+
}
725+
720726
x <- if (nrow(tbl)) {
721727
Matrix::sparseMatrix(
722728
i = as.integer(tbl$i),

apis/r/tests/testthat/test-14-SummarizedExperimentIngest.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ test_that("Write SummarizedExperiment mechanics", {
5858
names(SummarizedExperiment::rowData(se))
5959
)
6060

61+
# Verify X data values round-trip correctly
62+
for (assay in SummarizedExperiment::assayNames(se)) {
63+
original <- SummarizedExperiment::assay(se, assay)
64+
stored <- ms$X$get(assay)$read()$sparse_matrix()$concat()
65+
expect_equal(
66+
sum(stored != 0),
67+
sum(original != 0),
68+
label = sprintf("non-zero count for assay '%s'", assay)
69+
)
70+
}
71+
6172
# Test ms_name assertions
6273
expect_error(write_soma(se, uri))
6374
expect_error(write_soma(se, uri, ""))
@@ -101,4 +112,8 @@ test_that("Resume-mode adds a second measurement to an existing experiment", {
101112
on.exit(exp$close(), add = TRUE, after = FALSE)
102113

103114
expect_setequal(exp$ms$names(), c("ms1", "ms2"))
115+
116+
mat2 <- exp$ms$get("ms2")$X$get("counts")$read()$sparse_matrix()$concat()
117+
# Verify that the second measurement's X data is not all zeros (CX-279)
118+
expect_true(sum(mat2 != 0) > 0)
104119
})

apis/r/tests/testthat/test-16-write-soma-resume.R

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,36 @@ test_that("Resume-mode sparse arrays", {
314314
gc()
315315
})
316316

317+
test_that("Resume-mode sparse array to new URI preserves data", {
318+
collection <- SOMACollectionCreate(tempfile(pattern = "sparse-new-resume"))
319+
on.exit(collection$close(), add = TRUE, after = FALSE)
320+
321+
# Create a sparse matrix with known non-zero values
322+
mat <- Matrix::rsparsematrix(10L, 8L, 0.5, repr = "T")
323+
324+
# Writing a new sparse array in "resume" mode should still write all data even
325+
# if the array doesn't exist yet.
326+
expect_s3_class(
327+
ndarray <- write_soma(
328+
mat,
329+
uri = "new-array",
330+
soma_parent = collection,
331+
ingest_mode = "resume"
332+
),
333+
"SOMASparseNDArray"
334+
)
335+
on.exit(ndarray$close(), add = TRUE, after = FALSE)
336+
337+
ndarray$reopen("READ")
338+
result <- ndarray$read()$sparse_matrix()$concat()
339+
expect_equal(
340+
as.matrix(result),
341+
as.matrix(mat),
342+
label = "result",
343+
expected.label = "mat"
344+
)
345+
})
346+
317347
test_that("Resume-mode dense arrays", {
318348
skip_if(!extended_tests())
319349
skip_if_not_installed("datasets")

0 commit comments

Comments
 (0)