diff --git a/apis/python/src/tiledbsoma/_scene.py b/apis/python/src/tiledbsoma/_scene.py index f7432c3f73..71aabdda27 100644 --- a/apis/python/src/tiledbsoma/_scene.py +++ b/apis/python/src/tiledbsoma/_scene.py @@ -6,6 +6,7 @@ Implementation of a SOMA Scene """ +import warnings from typing import Any, List, Optional, Sequence, Tuple, Type, TypeVar, Union import somacore @@ -17,9 +18,13 @@ from typing_extensions import Self from . import _funcs, _tdb_handles +from . import pytiledbsoma as clib from ._collection import CollectionBase -from ._constants import SOMA_COORDINATE_SPACE_METADATA_KEY -from ._exception import SOMAError +from ._constants import ( + SOMA_COORDINATE_SPACE_METADATA_KEY, + SPATIAL_DISCLAIMER, +) +from ._exception import SOMAError, map_exception_for_create from ._geometry_dataframe import GeometryDataFrame from ._multiscale_image import MultiscaleImage from ._point_cloud_dataframe import PointCloudDataFrame @@ -32,6 +37,7 @@ ) from ._types import OpenTimestamp from .options import SOMATileDBContext +from .options._soma_tiledb_context import _validate_soma_tiledb_context _spatial_element = Union[GeometryDataFrame, MultiscaleImage, PointCloudDataFrame] @@ -97,16 +103,29 @@ def create( Lifecycle: Experimental. """ - if coordinate_space is not None: - raise NotImplementedError( - "Setting the coordinate space on create is not yet implemented." + warnings.warn(SPATIAL_DISCLAIMER) + + context = _validate_soma_tiledb_context(context) + try: + timestamp_ms = context._open_timestamp_ms(tiledb_timestamp) + clib.SOMAScene.create( + ctx=context.native_context, + uri=uri, + timestamp=(0, timestamp_ms), ) - return super().create( - uri=uri, - platform_config=platform_config, - context=context, - tiledb_timestamp=tiledb_timestamp, - ) + handle = cls._wrapper_type.open(uri, "w", context, tiledb_timestamp) + if coordinate_space is not None: + if not isinstance(coordinate_space, CoordinateSpace): + coordinate_space = CoordinateSpace.from_axis_names(coordinate_space) + handle.meta[SOMA_COORDINATE_SPACE_METADATA_KEY] = ( + coordinate_space_to_json(coordinate_space) + ) + return cls( + handle, + _dont_call_this_use_create_or_open_instead="tiledbsoma-internal-code", + ) + except SOMAError as e: + raise map_exception_for_create(e, uri) from None def __init__( self, diff --git a/apis/python/src/tiledbsoma/soma_collection.cc b/apis/python/src/tiledbsoma/soma_collection.cc index 6c3dd54e8e..5836ca54bb 100644 --- a/apis/python/src/tiledbsoma/soma_collection.cc +++ b/apis/python/src/tiledbsoma/soma_collection.cc @@ -76,8 +76,22 @@ void load_soma_collection(py::module& m) { py::class_( m, "SOMAMeasurement"); - py::class_( - m, "SOMAScene"); + py::class_(m, "SOMAScene") + .def_static( + "create", + [](std::shared_ptr ctx, + std::string_view uri, + std::optional timestamp) { + try { + SOMAScene::create(uri, ctx, timestamp); + } catch (const std::exception& e) { + TPY_ERROR_LOC(e.what()); + } + }, + py::kw_only(), + "ctx"_a, + "uri"_a, + "timestamp"_a = py::none()); py::class_( m, "SOMAMultiscaleImage"); diff --git a/apis/python/tests/test_scene.py b/apis/python/tests/test_scene.py index 625ac7d984..f8e6ad4b1c 100644 --- a/apis/python/tests/test_scene.py +++ b/apis/python/tests/test_scene.py @@ -111,7 +111,39 @@ def test_measurement_with_var_scene(tmp_path): assert soma.DataFrame.exists(obs_scene_uri) -def test_scene_coord_space(tmp_path): +def test_scene_coord_space_at_create(tmp_path): + uri = tmp_path.as_uri() + + coord_space = soma.CoordinateSpace( + [ + soma.Axis(name="x"), + soma.Axis(name="y"), + ] + ) + coord_space_json = """ + [ + {"name": "x", "unit": null}, + {"name": "y", "unit": null} + ] + """ + + with soma.Scene.create(uri, coordinate_space=("x", "y")) as scene: + + # Reserved metadata key should not be settable? + # with pytest.raises(soma.SOMAError): + # scene.metadata["soma_coordinate_space"] = coord_space_json + + scene.coordinate_space = coord_space + assert scene.coordinate_space == coord_space + assert json.loads(scene.metadata["soma_coordinate_space"]) == json.loads( + coord_space_json + ) + + with soma.Scene.open(uri) as scene: + assert scene.coordinate_space == coord_space + + +def test_scene_coord_space_after_create(tmp_path): uri = tmp_path.as_uri() coord_space = soma.CoordinateSpace( diff --git a/libtiledbsoma/src/soma/soma_scene.cc b/libtiledbsoma/src/soma/soma_scene.cc index a0cd5b77ac..680d21a186 100644 --- a/libtiledbsoma/src/soma/soma_scene.cc +++ b/libtiledbsoma/src/soma/soma_scene.cc @@ -46,30 +46,10 @@ void SOMAScene::create( std::optional timestamp) { try { std::filesystem::path scene_uri(uri); - - SOMAGroup::create(ctx, scene_uri.string(), "SOMAScene", timestamp); - SOMACollection::create((scene_uri / "img").string(), ctx, timestamp); - SOMACollection::create((scene_uri / "obsl").string(), ctx, timestamp); - SOMACollection::create((scene_uri / "varl").string(), ctx, timestamp); - - auto name = std::string(std::filesystem::path(uri).filename()); - auto group = SOMAGroup::open( - OpenMode::write, scene_uri.string(), ctx, name, timestamp); - group->set( - (scene_uri / "img").string(), - URIType::absolute, - "img", - "SOMACollection"); - group->set( - (scene_uri / "obsl").string(), - URIType::absolute, - "obsl", - "SOMACollection"); - group->set( - (scene_uri / "varl").string(), - URIType::absolute, - "varl", - "SOMAColelction"); + auto group = SOMAGroup::create( + ctx, scene_uri.string(), "SOMAScene", timestamp); + // TODO: Set extra metadata. + // (See story: https://app.shortcut.com/tiledb-inc/story/59915) group->close(); } catch (TileDBError& e) { throw TileDBSOMAError(e.what()); diff --git a/libtiledbsoma/src/soma/soma_scene.h b/libtiledbsoma/src/soma/soma_scene.h index 6ac36df3c7..f056961f65 100644 --- a/libtiledbsoma/src/soma/soma_scene.h +++ b/libtiledbsoma/src/soma/soma_scene.h @@ -51,7 +51,7 @@ class SOMAScene : public SOMACollection { * * @param uri URI to create the SOMAScene * @param schema TileDB ArraySchema - * @param platform_config Optional config parameter dictionary + * @param timestamp Optional pair indicating timestamp start and end */ static void create( std::string_view uri,