From 95786d170e12f9eb0d7ff31c034d432a8c4d5e2e Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 14:57:02 -0400 Subject: [PATCH 01/19] Initial spatialdata interface --- src/segger/cli/export.py | 68 +++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 7d0b6a8..6092c37 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -2,7 +2,9 @@ One command writes the chosen SpatialData elements: ``anndata`` the cell by gene table (``adata.h5ad``), ``transcripts`` the assigned transcripts/points (``transcripts.parquet``), -and ``boundaries`` one polygon per cell/shapes (``cell_boundaries.parquet``). Default: anndata + boundaries. +``boundaries`` one polygon per cell/shapes (``cell_boundaries.parquet``), and ``spatialdata`` +which copies an existing SpatialData store (``--sdata``) into the output directory and adds +the transcripts, cell boundaries, and table elements to the copy. Default: anndata + boundaries. """ from __future__ import annotations @@ -15,12 +17,21 @@ _group_io = Group(name="I/O", sort_key=0) _group_opts = Group(name="Options", sort_key=1) -_Element = Literal["anndata", "transcripts", "boundaries"] +_Element = Literal["anndata", "transcripts", "boundaries", "spatialdata"] _DEFAULT_ELEMENTS = ("anndata", "boundaries") _Seg = Annotated[Path, Parameter(alias="-s", group=_group_io, validator=validators.Path(exists=True, dir_okay=False))] _Source = Annotated[Path, Parameter(alias="-i", group=_group_io, validator=validators.Path(exists=True, dir_okay=True))] _Out = Annotated[Path, Parameter(alias="-o", group=_group_io)] +_Sdata = Annotated[ + Optional[Path], + Parameter( + alias="--sdata", + group=_group_io, + validator=validators.Path(exists=True, dir_okay=True), + help="Existing SpatialData Zarr store to copy into the output directory and add elements to (required for 'spatialdata').", + ), +] _IncludeAll = Annotated[ bool, @@ -94,11 +105,33 @@ def _load_assigned( return assigned +def _write_to_sdata(sdata_path: Path, output_directory: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData") -> Path: + """Copy the source SpatialData store into the output directory, then add segger's elements to the copy.""" + import shutil + import spatialdata + from spatialdata.models import PointsModel, ShapesModel, TableModel + + dest = output_directory / sdata_path.name + if dest.exists(): + raise FileExistsError(f"{dest} already exists; aborting to avoid overwriting an existing SpatialData store.") + shutil.copytree(sdata_path, dest) + + sdata = spatialdata.read_zarr(dest) + sdata["transcripts"] = PointsModel.parse( + assigned.to_pandas(), coordinates={"x": "x", "y": "y"}, feature_key="feature_name", instance_key="segger_cell_id" + ) + sdata["cell_boundaries"] = ShapesModel.parse(gdf) + sdata["table"] = TableModel.parse(adata) + sdata.write_element(["transcripts", "cell_boundaries", "table"], overwrite=True) + return dest + + def export( *elements: Annotated[_Element, Parameter(help="Elements to write (default: anndata boundaries).")], segmentation_path: _Seg, source_path: _Source, output_directory: _Out, + sdata_path: _Sdata = None, method: Annotated[ Literal["delaunay", "convex_hull"], Parameter(group=_group_opts, help="Cell-polygon method for boundaries."), @@ -110,27 +143,38 @@ def export( min_similarity: _MinSim = None, min_transcripts: _MinTx = 10, ): - """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries).""" + """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS + if "spatialdata" in selected and sdata_path is None: + raise ValueError("--sdata is required when exporting 'spatialdata'.") + assigned = _load_assigned(segmentation_path, source_path, include_all_transcripts, min_similarity, min_transcripts) output_directory.mkdir(parents=True, exist_ok=True) + # compute outputs gdf = None - if "boundaries" in selected: + if "boundaries" in selected or "spatialdata" in selected: from ..export import generate_boundaries - gdf = generate_boundaries(assigned, cell_id="segger_cell_id", method=method, smoothing=chaikin_iterations) - gdf.to_parquet(output_directory / "cell_boundaries.parquet") - print(f"Wrote {len(gdf)} {method} cell boundaries: {output_directory / 'cell_boundaries.parquet'}") - if "anndata" in selected: + adata = None + if "anndata" in selected or "spatialdata" in selected: from ..export import build_anndata - - # Use the exported polygon areas so obs["area"] matches the boundaries; omitted otherwise. adata = build_anndata(assigned, cell_id="segger_cell_id", area=gdf.geometry.area if gdf is not None else None) - adata.write_h5ad(output_directory / "adata.h5ad") - print(f"Wrote AnnData ({adata.n_obs} cells x {adata.n_vars} genes): {output_directory / 'adata.h5ad'}") + # save outputs if "transcripts" in selected: assigned.write_parquet(output_directory / "transcripts.parquet") print(f"Wrote {assigned.height} assigned transcripts: {output_directory / 'transcripts.parquet'}") + + if "boundaries" in selected: + gdf.to_parquet(output_directory / "cell_boundaries.parquet") + print(f"Wrote {len(gdf)} {method} cell boundaries: {output_directory / 'cell_boundaries.parquet'}") + + if "anndata" in selected: + adata.write_h5ad(output_directory / "adata.h5ad") + print(f"Wrote AnnData ({adata.n_obs} cells x {adata.n_vars} genes): {output_directory / 'adata.h5ad'}") + + if "spatialdata" in selected: + dest = _write_to_sdata(sdata_path, output_directory, assigned, gdf, adata) + print(f"Added transcripts, cell_boundaries and table to {dest}") From 3fb3775f6cd37b7d21ae880117c5783e5dd6f946 Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 15:00:19 -0400 Subject: [PATCH 02/19] Use new 'filtered' column and remove faulty min_similarity validators --- src/segger/cli/export.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 6092c37..751e76b 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -41,8 +41,7 @@ Optional[float], Parameter( group=_group_opts, - validator=validators.Number(gte=0, lte=1), - help="Fixed similarity threshold (0-1), overriding the per-gene threshold from segmentation.", + help="Custom required similarity threshold, overriding the per-gene threshold from segmentation.", ), ] _MinTx = Annotated[ @@ -79,18 +78,13 @@ def _load_assigned( pred_cols = [c for c in (std.row_index, "segger_cell_id", "segger_similarity", "similarity_threshold") if c in seg.columns] merged = tx.join(seg.select(pred_cols), on=std.row_index, how="left") - has_assignment = pl.col("segger_cell_id").is_not_null() if include_all_transcripts: - keep = has_assignment + keep = pl.col("segger_cell_id").is_not_null() elif min_similarity is not None: - if "segger_similarity" not in merged.columns: - raise ValueError("--min-similarity needs a 'segger_similarity' column in the segmentation file.") - keep = has_assignment & (pl.col("segger_similarity") >= min_similarity) - elif {"segger_similarity", "similarity_threshold"} <= set(merged.columns): - keep = has_assignment & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) + keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= min_similarity) else: - keep = has_assignment - + keep = pl.col("filtered") + assigned = merged.filter(keep).select( pl.col(std.row_index), pl.col("segger_cell_id").cast(pl.String), From 29c2011250d17003650983df0c92b1426a9edec4 Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 15:12:14 -0400 Subject: [PATCH 03/19] Make -i/source_path optional; fall back to joining source transcripts for legacy segmentation outputs --- src/segger/cli/export.py | 45 ++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 751e76b..f8c9aab 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -21,7 +21,15 @@ _DEFAULT_ELEMENTS = ("anndata", "boundaries") _Seg = Annotated[Path, Parameter(alias="-s", group=_group_io, validator=validators.Path(exists=True, dir_okay=False))] -_Source = Annotated[Path, Parameter(alias="-i", group=_group_io, validator=validators.Path(exists=True, dir_okay=True))] +_Source = Annotated[ + Optional[Path], + Parameter( + alias="-i", + group=_group_io, + validator=validators.Path(exists=True, dir_okay=True), + help="Source transcripts directory; only needed for segmentation outputs written before x/y/feature_name were included inline.", + ), +] _Out = Annotated[Path, Parameter(alias="-o", group=_group_io)] _Sdata = Annotated[ Optional[Path], @@ -54,37 +62,48 @@ ] +def _legacy_join(seg: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.DataFrame": + """Join x/y/feature_name onto a segmentation output written before they were included inline.""" + if source_path is None: + raise ValueError("This segmentation output predates inline x/y/feature_name; pass -i/--source-path to join them from the source transcripts.") + import polars as pl + + from ..io import get_preprocessor + + tx = get_preprocessor(source_path).transcripts + tx = tx.collect() if isinstance(tx, pl.LazyFrame) else tx + pred_cols = [c for c in (std.row_index, "segger_cell_id", "segger_similarity", "similarity_threshold") if c in seg.columns] + return tx.join(seg.select(pred_cols), on=std.row_index, how="left") + + def _load_assigned( segmentation_path: Path, - source_path: Path, + source_path: Optional[Path], include_all_transcripts: bool, min_similarity: Optional[float], min_transcripts: int = 10, ) -> "pl.DataFrame": - """Join predictions onto source transcripts; return the kept tx (row_index/segger_cell_id/feature_name/x/y).""" + """Return the kept assigned tx (row_index/segger_cell_id/feature_name/x/y).""" import polars as pl - from ..io import StandardTranscriptFields, get_preprocessor + from ..io import StandardTranscriptFields std = StandardTranscriptFields() seg = pl.read_parquet(segmentation_path) if "segger_cell_id" not in seg.columns: raise ValueError(f"No 'segger_cell_id' column in {segmentation_path}.") - tx = get_preprocessor(source_path).transcripts - if isinstance(tx, pl.LazyFrame): - tx = tx.collect() - - pred_cols = [c for c in (std.row_index, "segger_cell_id", "segger_similarity", "similarity_threshold") if c in seg.columns] - merged = tx.join(seg.select(pred_cols), on=std.row_index, how="left") + merged = seg if {std.x, std.y, std.feature} <= set(seg.columns) else _legacy_join(seg, source_path, std) if include_all_transcripts: keep = pl.col("segger_cell_id").is_not_null() elif min_similarity is not None: keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= min_similarity) - else: + elif "filtered" in merged.columns: keep = pl.col("filtered") - + else: + keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) + assigned = merged.filter(keep).select( pl.col(std.row_index), pl.col("segger_cell_id").cast(pl.String), @@ -123,8 +142,8 @@ def _write_to_sdata(sdata_path: Path, output_directory: Path, assigned: "pl.Data def export( *elements: Annotated[_Element, Parameter(help="Elements to write (default: anndata boundaries).")], segmentation_path: _Seg, - source_path: _Source, output_directory: _Out, + source_path: _Source = None, sdata_path: _Sdata = None, method: Annotated[ Literal["delaunay", "convex_hull"], From 4ea4f502c7ba19a8cb0e86739a1eaad71806d23b Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 15:16:41 -0400 Subject: [PATCH 04/19] Add example usage to export docstring --- src/segger/cli/export.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index f8c9aab..f11a3f0 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -1,10 +1,16 @@ """``segger export``: write a segger segmentation as scverse-compatible files. -One command writes the chosen SpatialData elements: ``anndata`` the cell by gene table -(``adata.h5ad``), ``transcripts`` the assigned transcripts/points (``transcripts.parquet``), -``boundaries`` one polygon per cell/shapes (``cell_boundaries.parquet``), and ``spatialdata`` -which copies an existing SpatialData store (``--sdata``) into the output directory and adds -the transcripts, cell boundaries, and table elements to the copy. Default: anndata + boundaries. +Example usage: + # save anndata and boundaries + segger export anndata \ + -s $PATH_OUTPUT/segger_segmentation.parquet \ + -o $PATH_OUTPUT/adata_export + + # save spatialdata - sdata object must exist already, this will copy it + segger export spatialdata \ + -s $PATH_OUTPUT/segger_segmentation.parquet \ + -o $PATH_OUTPUT/sdata_export \ + --sdata $PATH_INPUT/sdata.zarr """ from __future__ import annotations @@ -27,7 +33,7 @@ alias="-i", group=_group_io, validator=validators.Path(exists=True, dir_okay=True), - help="Source transcripts directory; only needed for segmentation outputs written before x/y/feature_name were included inline.", + help="Source transcripts directory. Only needed for segger v0.2.0 (before x/y/feature_name were included in outputs).", ), ] _Out = Annotated[Path, Parameter(alias="-o", group=_group_io)] From 5e71342111e9be7508f6c7ab1031efa143f9f6ab Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 15:20:07 -0400 Subject: [PATCH 05/19] Log copy/write start for spatialdata export; check destination existence upfront --- src/segger/cli/export.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index f11a3f0..9ba47f0 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -124,15 +124,13 @@ def _load_assigned( return assigned -def _write_to_sdata(sdata_path: Path, output_directory: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData") -> Path: - """Copy the source SpatialData store into the output directory, then add segger's elements to the copy.""" +def _write_to_sdata(sdata_path: Path, dest: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData") -> Path: + """Copy the source SpatialData store to ``dest``, then add segger's elements to the copy.""" import shutil import spatialdata from spatialdata.models import PointsModel, ShapesModel, TableModel - dest = output_directory / sdata_path.name - if dest.exists(): - raise FileExistsError(f"{dest} already exists; aborting to avoid overwriting an existing SpatialData store.") + print(f"Copying {sdata_path} to {dest}...") shutil.copytree(sdata_path, dest) sdata = spatialdata.read_zarr(dest) @@ -141,6 +139,8 @@ def _write_to_sdata(sdata_path: Path, output_directory: Path, assigned: "pl.Data ) sdata["cell_boundaries"] = ShapesModel.parse(gdf) sdata["table"] = TableModel.parse(adata) + + print(f"Writing transcripts, cell_boundaries and table to {dest}...") sdata.write_element(["transcripts", "cell_boundaries", "table"], overwrite=True) return dest @@ -164,8 +164,14 @@ def export( ): """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS - if "spatialdata" in selected and sdata_path is None: - raise ValueError("--sdata is required when exporting 'spatialdata'.") + + sdata_dest = None + if "spatialdata" in selected: + if sdata_path is None: + raise ValueError("--sdata is required when exporting 'spatialdata'.") + sdata_dest = output_directory / sdata_path.name + if sdata_dest.exists(): + raise FileExistsError(f"{sdata_dest} already exists; aborting to avoid overwriting an existing SpatialData store.") assigned = _load_assigned(segmentation_path, source_path, include_all_transcripts, min_similarity, min_transcripts) output_directory.mkdir(parents=True, exist_ok=True) @@ -195,5 +201,5 @@ def export( print(f"Wrote AnnData ({adata.n_obs} cells x {adata.n_vars} genes): {output_directory / 'adata.h5ad'}") if "spatialdata" in selected: - dest = _write_to_sdata(sdata_path, output_directory, assigned, gdf, adata) + dest = _write_to_sdata(sdata_path, sdata_dest, assigned, gdf, adata) print(f"Added transcripts, cell_boundaries and table to {dest}") From 785fd35cd99eb0310915ff5bbfb213a592d0ff22 Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Tue, 25 Aug 2026 17:06:36 -0400 Subject: [PATCH 06/19] Check spatialdata package is importable before exporting the 'spatialdata' element --- src/segger/cli/export.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 9ba47f0..19c27d7 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -167,6 +167,10 @@ def export( sdata_dest = None if "spatialdata" in selected: + import importlib.util + + if importlib.util.find_spec("spatialdata") is None: + raise ImportError("The 'spatialdata' element needs the spatialdata package: pip install segger[spatialdata]") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") sdata_dest = output_directory / sdata_path.name From 35b7acf36b5bd64c535db7dc4b5ff5d96be6c6e5 Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Wed, 26 Aug 2026 14:18:29 -0400 Subject: [PATCH 07/19] add spatialdata dependency --- pixi.lock | 622 +++++++++++++++++++++++++++++++++++++-- pixi.toml | 3 +- src/segger/cli/export.py | 2 +- 3 files changed, 603 insertions(+), 24 deletions(-) diff --git a/pixi.lock b/pixi.lock index 6900a66..cddf7d0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -40,17 +40,22 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/6d/f4/5a7d76dc844d3ff8ed1f1a043158aa393794aebb787d3e2f8c0fe87f674f/aiobotocore-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/f4/4d0193dc5bab3af74e9560a8b45830d88ac707467d15ceff7e3df17adc41/anndata-0.12.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/d3/54cd560804a8c2b898824778e86c13c2a14600bc83532a9c4f69f2f469c3/array_api_compat-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/f2/4bd8f2f419088feb3ce55f0ca91040ff902f402edfd197450b20a2e1d533/botocore-1.43.46-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/24/e95471ae93c08d3606c9c7343cf65d490f154daa88b50581957a0aa780f4/colorcet-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/66/d5/bd4c03e9516d3cf788a270debe28d687e5c48b13a9931599bbddf01de302/cuda_bindings-12.9.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/92/de/8ca2b613042550dcf9ef50c596c8b1f602afda92cf9032ac28a73f6ee410/cuda_pathfinder-1.4.2-py3-none-any.whl @@ -67,23 +72,26 @@ environments: - pypi: https://pypi.nvidia.com/dask-cuda/dask_cuda-24.10.0-py3-none-any.whl - pypi: https://pypi.nvidia.com/dask-cudf-cu12/dask_cudf_cu12-24.10.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/11/d758ed9f1e9d6917402c475a8512b0a0204873226479d47cc440e50404ec/dask_expr-1.1.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/5b/15d6d6ff8697b188787609be059fe4f07f99fc00f43f68e9e1540fa8733e/dask_image-2026.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/41/247627c8b9fef5c605d00546b85771a8fe42975b9616a557cead5468789b/datashader-0.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/15/3746074e6a20e87de78c61fe8eb63a70ad01cf37f0ef1a8611fd7f9e5701/distributed-2024.9.0-py3-none-any.whl - pypi: https://pypi.nvidia.com/distributed-ucxx-cu12/distributed_ucxx_cu12-0.40.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/e2/5e5515562b2e9a56d84659377176aef7345da2c3c22909a1897fe27e14dd/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/d7/8e4845993ee233c2023d11babe9b3dae7d30333da1d792eeccebcb77baab/fonttools-4.62.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/78/6a04792ace63a93e162f1305392d500ae8ddcb620e7eb88a22fd622b35bb/geopandas-1.1.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - pypi: https://download.pytorch.org/whl/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl @@ -97,22 +105,24 @@ environments: - pypi: https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://download.pytorch.org/whl/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/02/eff2e0a291201b70f5c342a8dac0ec67c75aa912af50a999054b99755832/multiscale_spatial_image-2.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/16/aa6e3ba3cd45435c117d1101b278b646444ed05b7c712af631b91353f573/numba-0.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/a8/908a226632ffabf19caf8c99f1b2898f2f22aac02795a6fe9d018fd6d9dd/numcodecs-0.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cublas-cu12/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cuda-cupti-cu12/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cuda-nvrtc-cu12/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cuda-runtime-cu12/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl - - pypi: https://download.pytorch.org/whl/cu121/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl + - pypi: https://download.pytorch.org/whl/cu124/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cufft-cu12/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-curand-cu12/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-cusolver-cu12/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl @@ -121,17 +131,23 @@ environments: - pypi: https://pypi.nvidia.com/nvidia-nvjitlink-cu12/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl - pypi: https://pypi.nvidia.com/nvidia-nvtx-cu12/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl - pypi: https://pypi.nvidia.com/nvtx/nvtx-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/24/c8/35ac60b20eccccf84638a872c3b4ebefad46b20f87540ca17a37908b1db6/ome_zarr-0.11.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a5/4d82be566f069d7a9a702dcdf6f9106df0e0b042e738043c0cc7ddd7e3f6/pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ac/37/1351bbfabbacbe92cd38774f8779ff680fab48c36b5fed9bcfe0c160009c/param-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz + - pypi: https://files.pythonhosted.org/packages/28/be/0ff05fcd2938fb58ad9219bd54135968342d214737e012d62d43f06a2dd6/platformdirs-4.11.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/f8/fad8470d9701c1b208cc24919a661efdf565373e77e7d06400642a759285/polars-1.39.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/60/c0d0b6720437685223457242a79f6bba443485ca85928645786479ebed86/polars_runtime_32-1.39.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/21/9ca93b84b92ef927814cb7ba37f0774a484c849d58f0b692b16af8eebcfb/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://pypi.nvidia.com/pylibcudf-cu12/pylibcudf_cu12-24.10.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://pypi.nvidia.com/pylibcugraph-cu12/pylibcugraph_cu12-24.10.0-cp311-cp311-manylinux_2_28_x86_64.whl @@ -152,40 +168,50 @@ environments: - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl - pypi: https://pypi.nvidia.com/rmm-cu12/rmm_cu12-24.10.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/e9/c1d43543da87cd27e8e2a74db85cf0b6c5cff2d5f04a86bd584d2fbc2bb0/scanpy-1.11.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/7c/64d18f2374e19ba9bee52dee885ec81f808a1863ed0995495b83319b88bc/session_info2-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/c1/16eb09d03eb7096b901e816dc93f53819f83b867c7d5dbb075f837d0120b/spatial_image-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/96/015832d04de478b5ccf089fd259bff58adcff195cf27fec29b2bed307a46/spatialdata-0.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://download.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp311-cp311-linux_x86_64.whl + - pypi: https://download-r2.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp311-cp311-linux_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/4dffd7300500465e0b4a2ae917dcb2ce771de0b9a772670365799a27c024/torch_geometric-2.7.0-py3-none-any.whl - pypi: https://data.pyg.org/whl/torch-2.5.0%2Bcu121/torch_scatter-2.1.2%2Bpt25cu121-cp311-cp311-linux_x86_64.whl - pypi: https://data.pyg.org/whl/torch-2.5.0%2Bcu121/torch_sparse-0.6.18%2Bpt25cu121-cp311-cp311-linux_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c3/a2/c7f6ebf546f8f644edf0f999aa98ece106986a77a7b922316bf6414ff825/torchmetrics-1.9.0-py3-none-any.whl - - pypi: https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp311-cp311-linux_x86_64.whl + - pypi: https://download-r2.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp311-cp311-linux_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/0e/c9aecfed527ef6bff91ea51036cfcf4b6f6218bc6f507cdf4dff0bb2c81f/treelite-4.3.0-py3-none-manylinux2014_x86_64.whl - - pypi: https://download.pytorch.org/whl/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://download.pytorch.org/whl/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://download-r2.pytorch.org/whl/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://pypi.nvidia.com/ucx-py-cu12/ucx_py_cu12-0.40.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl - pypi: https://pypi.nvidia.com/ucxx-cu12/ucxx_cu12-0.40.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/43/d2/fcf7192dd1cd8c090b6cfd53fa223c4fb2887a17c47e06bc356d44f40dfb/umap_learn-0.5.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/40/ed/1c4631ad5909487ea8907cd326d9855c2207d790e3936e77bda48173b8be/xarray-2024.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/7b/9adbd82e2b7b8571da23856a890324b42cfbc38ae0ceac2177e5256f7216/xarray_dataclasses-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/6d/f585a27b380ee987619b5617c0ca672a71a4345b67cfedbb6299750ce845/xarray_schema-0.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/ee/9d0bb9f1bbaf347efe2ca6ad7bae259d6ed9e7a2b4441bd16b88723145a1/xarray_spatial-0.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - pypi: ./ @@ -303,6 +329,21 @@ packages: purls: [] size: 28948 timestamp: 1770939786096 +- pypi: https://files.pythonhosted.org/packages/6d/f4/5a7d76dc844d3ff8ed1f1a043158aa393794aebb787d3e2f8c0fe87f674f/aiobotocore-3.8.0-py3-none-any.whl + name: aiobotocore + version: 3.8.0 + sha256: 8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + requires_dist: + - aiohttp>=3.12.0,<4.0.0 + - aioitertools>=0.5.1,<1.0.0 + - botocore>=1.43.3,<1.43.47 + - python-dateutil>=2.1,<3.0.0 + - jmespath>=0.7.1,<2.0.0 + - multidict>=6.0.0,<7.0.0 + - typing-extensions>=4.14.0,<5.0.0 ; python_full_version < '3.11' + - wrapt>=1.10.10,<3.0.0 + - httpx>=0.25.1,<0.29 ; extra == 'httpx' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl name: aiohappyeyeballs version: 2.6.1 @@ -326,6 +367,13 @@ packages: - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + name: aioitertools + version: 0.13.0 + sha256: 0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.10' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl name: aiosignal version: 1.4.0 @@ -442,11 +490,25 @@ packages: - torch ; extra == 'dev' - sparse>=0.15.1 ; extra == 'dev' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + name: asciitree + version: 0.3.3 + sha256: 4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl name: attrs version: 25.4.0 sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0e/f2/4bd8f2f419088feb3ce55f0ca91040ff902f402edfd197450b20a2e1d533/botocore-1.43.46-py3-none-any.whl + name: botocore + version: 1.43.46 + sha256: cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 + requires_dist: + - jmespath>=0.7.1,<2.0.0 + - python-dateutil>=2.1,<3.0.0 + - urllib3>=1.25.4,!=2.2.0,<3 + - awscrt==0.32.2 ; extra == 'crt' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 md5: d2ffd7602c02f2b316fd921d39876885 @@ -494,6 +556,31 @@ packages: version: 3.1.2 sha256: 9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/1b/24/e95471ae93c08d3606c9c7343cf65d490f154daa88b50581957a0aa780f4/colorcet-3.2.1-py3-none-any.whl + name: colorcet + version: 3.2.1 + sha256: 3f6fde13cef2169222dd5fe2a2bf847c02d644470fdf167ed566f6421df470f7 + requires_dist: + - pre-commit ; extra == 'tests' + - pytest>=2.8.5 ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - packaging ; extra == 'tests' + - colorcet[tests] ; extra == 'tests-extra' + - pytest-mpl ; extra == 'tests-extra' + - numpy ; extra == 'examples' + - holoviews ; extra == 'examples' + - matplotlib ; extra == 'examples' + - bokeh ; extra == 'examples' + - colorcet[examples] ; extra == 'tests-examples' + - nbval ; extra == 'tests-examples' + - colorcet[examples] ; extra == 'doc' + - nbsite>=0.8.4 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - colorcet[tests] ; extra == 'all' + - colorcet[tests-extra] ; extra == 'all' + - colorcet[examples] ; extra == 'all' + - colorcet[doc] ; extra == 'all' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: contourpy version: 1.3.3 @@ -886,6 +973,60 @@ packages: - crick ; extra == 'analyze' - distributed ; extra == 'analyze' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3f/5b/15d6d6ff8697b188787609be059fe4f07f99fc00f43f68e9e1540fa8733e/dask_image-2026.5.0-py3-none-any.whl + name: dask-image + version: 2026.5.0 + sha256: acf86cd7f0f1e97804198d30b7cc931efd29f9dec86c65ec004b50405c3f5227 + requires_dist: + - dask[array]>=2024.4.1 + - numpy>=1.18 + - scipy>=1.7.0 + - pims>=0.4.1 + - tifffile>=2020.10.1 + - dask[dataframe]>=2024.4.1 ; extra == 'dataframe' + - pandas>=2.0.0 ; extra == 'dataframe' + - build>=1.2.1 ; extra == 'test' + - coverage>=7.2.1 ; extra == 'test' + - flake8>=6.0.0 ; extra == 'test' + - flake8-pyproject ; extra == 'test' + - pytest>=7.2.2 ; extra == 'test' + - pytest-cov>=4.0.0 ; extra == 'test' + - pytest-flake8>=1.1.1 ; extra == 'test' + - pytest-timeout>=2.3.1 ; extra == 'test' + - twine>=3.1.1 ; extra == 'test' + - cupy>=9.0.0 ; extra == 'gpu' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cb/41/247627c8b9fef5c605d00546b85771a8fe42975b9616a557cead5468789b/datashader-0.19.1-py3-none-any.whl + name: datashader + version: 0.19.1 + sha256: 7ce7154ff3ed070607f429355f57002fee5a17964e47c0b6447eeafe8cef9c82 + requires_dist: + - colorcet + - multipledispatch + - numba + - numpy + - packaging + - pandas + - param + - pyct + - requests + - scipy + - toolz + - xarray + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + name: deprecated + version: 1.3.1 + sha256: 597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f + requires_dist: + - wrapt>=1.10,<3 + - inspect2 ; python_full_version < '3' + - tox ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - bump2version<1 ; extra == 'dev' + - setuptools ; python_full_version >= '3.12' and extra == 'dev' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - pypi: https://files.pythonhosted.org/packages/a0/15/3746074e6a20e87de78c61fe8eb63a70ad01cf37f0ef1a8611fd7f9e5701/distributed-2024.9.0-py3-none-any.whl name: distributed version: 2024.9.0 @@ -953,6 +1094,11 @@ packages: - pytest ; extra == 'test' - cloudpickle ; extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl + name: fasteners + version: '0.20' + sha256: 9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7 + requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/01/e2/5e5515562b2e9a56d84659377176aef7345da2c3c22909a1897fe27e14dd/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl name: fastrlock version: 0.8.3 @@ -1398,6 +1544,11 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + name: jmespath + version: 1.1.0 + sha256: a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl name: joblib version: 1.5.3 @@ -1906,10 +2057,9 @@ packages: - pytest-regressions ; extra == 'testing' - requests ; extra == 'testing' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://download.pytorch.org/whl/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: markupsafe version: 3.0.3 - sha256: 0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: matplotlib @@ -1960,6 +2110,38 @@ packages: requires_dist: - typing-extensions>=4.1.0 ; python_full_version < '3.11' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl + name: multipledispatch + version: 1.0.0 + sha256: 0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4 +- pypi: https://files.pythonhosted.org/packages/4f/02/eff2e0a291201b70f5c342a8dac0ec67c75aa912af50a999054b99755832/multiscale_spatial_image-2.0.2-py3-none-any.whl + name: multiscale-spatial-image + version: 2.0.2 + sha256: 0c25fe7d743293bec06811ccd19842c0998dcdb99b58a0102116340d97d7dfcc + requires_dist: + - dask + - numpy + - python-dateutil + - spatial-image>=0.2.1 + - xarray>=2024.10.0 + - zarr + - dask-image ; extra == 'dask-image' + - pyimagej ; extra == 'imagej' + - itk-filtering>=5.3.0 ; extra == 'itk' + - matplotlib>=3.9.2,<4 ; extra == 'notebooks' + - ome-types>=0.5.1.post1,<0.6 ; extra == 'notebooks' + - tqdm>=4.66.4,<5 ; extra == 'notebooks' + - dask-image ; extra == 'test' + - fsspec ; extra == 'test' + - ipfsspec ; extra == 'test' + - itk-filtering>=5.3.0 ; extra == 'test' + - jsonschema ; extra == 'test' + - nbmake ; extra == 'test' + - pooch ; extra == 'test' + - pytest ; extra == 'test' + - pytest-mypy ; extra == 'test' + - urllib3 ; extra == 'test' + requires_python: '>=3.10,<3.13' - pypi: https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl name: natsort version: 8.4.0 @@ -2029,6 +2211,26 @@ packages: - llvmlite>=0.46.0.dev0,<0.47 - numpy>=1.22,<2.5 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a6/a8/908a226632ffabf19caf8c99f1b2898f2f22aac02795a6fe9d018fd6d9dd/numcodecs-0.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: numcodecs + version: 0.15.1 + sha256: cdfaef9f5f2ed8f65858db801f1953f1007c9613ee490a1c56233cd78b505ed5 + requires_dist: + - numpy>=1.24 + - deprecated + - sphinx ; extra == 'docs' + - sphinx-issues ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - numpydoc ; extra == 'docs' + - coverage ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - importlib-metadata ; extra == 'test-extras' + - msgpack ; extra == 'msgpack' + - zfpy>=1.0.0 ; extra == 'zfpy' + - pcodec>=0.3,<0.4 ; extra == 'pcodec' + - crc32c>=2.7 ; extra == 'crc32c' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: numcodecs version: 0.16.5 @@ -2082,7 +2284,7 @@ packages: version: 12.1.105 sha256: 6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40 requires_python: '>=3' -- pypi: https://download.pytorch.org/whl/cu121/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl +- pypi: https://download.pytorch.org/whl/cu124/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl name: nvidia-cudnn-cu12 version: 9.1.0.70 sha256: 165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f @@ -2140,6 +2342,21 @@ packages: - setuptools ; extra == 'test' - sphinx ; extra == 'docs' - nvidia-sphinx-theme ; extra == 'docs' +- pypi: https://files.pythonhosted.org/packages/24/c8/35ac60b20eccccf84638a872c3b4ebefad46b20f87540ca17a37908b1db6/ome_zarr-0.11.1-py3-none-any.whl + name: ome-zarr + version: 0.11.1 + sha256: e8ffc93de78558be990ed3b1b2b186b97ca0c7eb2cb460a7cffb861f4f13a8f1 + requires_dist: + - numpy + - dask + - zarr>=2.8.1,<3 + - fsspec[s3]>=0.8,!=2021.7.0,!=2023.9.0 + - aiohttp<4 + - requests + - scikit-image + - toolz + - pytest ; extra == 'tests' + requires_python: '>3.10' - pypi: https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: opencv-python version: 4.11.0.86 @@ -2363,6 +2580,73 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ac/37/1351bbfabbacbe92cd38774f8779ff680fab48c36b5fed9bcfe0c160009c/param-2.4.1-py3-none-any.whl + name: param + version: 2.4.1 + sha256: 6cdc0869b3ade232fc6d01691223cf86c2bdb53a6d5ba67a33dba838d8b8cf4c + requires_dist: + - aiohttp ; extra == 'all' + - cloudpickle ; extra == 'all' + - gmpy2 ; extra == 'all' + - ipython ; extra == 'all' + - jsonschema ; extra == 'all' + - nbval ; extra == 'all' + - nest-asyncio ; extra == 'all' + - numpy ; extra == 'all' + - odfpy ; extra == 'all' + - openpyxl ; extra == 'all' + - pandas ; extra == 'all' + - panel ; extra == 'all' + - pyarrow ; extra == 'all' + - pytest ; extra == 'all' + - pytest-asyncio ; extra == 'all' + - pytest-cov ; extra == 'all' + - pytest-xdist ; extra == 'all' + - tables ; extra == 'all' + - xlrd ; extra == 'all' + - aiohttp ; extra == 'examples' + - pandas ; extra == 'examples' + - panel ; extra == 'examples' + - pytest ; extra == 'tests' + - pytest-asyncio ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - odfpy ; extra == 'tests-deser' + - openpyxl ; extra == 'tests-deser' + - pyarrow ; extra == 'tests-deser' + - tables ; extra == 'tests-deser' + - xlrd ; extra == 'tests-deser' + - aiohttp ; extra == 'tests-examples' + - nbval ; extra == 'tests-examples' + - pandas ; extra == 'tests-examples' + - panel ; extra == 'tests-examples' + - pytest ; extra == 'tests-examples' + - pytest-asyncio ; extra == 'tests-examples' + - pytest-xdist ; extra == 'tests-examples' + - aiohttp ; extra == 'tests-full' + - cloudpickle ; extra == 'tests-full' + - gmpy2 ; extra == 'tests-full' + - ipython ; extra == 'tests-full' + - jsonschema ; extra == 'tests-full' + - nbval ; extra == 'tests-full' + - nest-asyncio ; extra == 'tests-full' + - numpy ; extra == 'tests-full' + - odfpy ; extra == 'tests-full' + - openpyxl ; extra == 'tests-full' + - pandas ; extra == 'tests-full' + - panel ; extra == 'tests-full' + - pyarrow ; extra == 'tests-full' + - pytest ; extra == 'tests-full' + - pytest-asyncio ; extra == 'tests-full' + - pytest-cov ; extra == 'tests-full' + - pytest-xdist ; extra == 'tests-full' + - tables ; extra == 'tests-full' + - xlrd ; extra == 'tests-full' + - cloudpickle ; extra == 'tests-pypy' + - ipython ; extra == 'tests-pypy' + - jsonschema ; extra == 'tests-pypy' + - nest-asyncio ; extra == 'tests-pypy' + - numpy ; extra == 'tests-pypy' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl name: partd version: 1.4.2 @@ -2417,6 +2701,22 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz + name: pims + version: '0.7' + sha256: 55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b + requires_dist: + - imageio + - numpy>=1.19 + - packaging + - slicerator>=0.9.8 + - tifffile + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/28/be/0ff05fcd2938fb58ad9219bd54135968342d214737e012d62d43f06a2dd6/platformdirs-4.11.4-py3-none-any.whl + name: platformdirs + version: 4.11.4 + sha256: e34ff91a24bcddc6d939b878bdf3f5c437c9c46fe9e212b1bf455fdf1ee57586 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl name: polars version: 1.38.1 @@ -2501,6 +2801,20 @@ packages: version: 1.39.0 sha256: b82c872b25ef6628462f90f1b6b3950779aee36889e83b3693d0a69684d3d86a requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + name: pooch + version: 1.9.0 + sha256: f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b + requires_dist: + - platformdirs>=2.5.0 + - packaging>=20.0 + - requests>=2.19.0 + - tqdm>=4.41.0,<5.0.0 ; extra == 'progress' + - paramiko>=2.7.0 ; extra == 'sftp' + - xxhash>=1.4.3 ; extra == 'xxhash' + - pytest-httpserver ; extra == 'test' + - pytest-localftpserver ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: propcache version: 0.4.1 @@ -2567,6 +2881,16 @@ packages: version: 23.0.1 sha256: 26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl + name: pyct + version: 0.6.0 + sha256: cfaded7289fca72ddf6579b81459e3ec8db323a508e61c49aa318ee3cd6ff160 + requires_dist: + - param>=1.7.0 + - pyyaml ; extra == 'cmd' + - requests ; extra == 'cmd' + - pytest ; extra == 'tests' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments version: 2.19.2 @@ -2903,6 +3227,15 @@ packages: - pytest ; extra == 'test' - pytest-cov ; extra == 'test' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl + name: s3fs + version: 2026.2.0 + sha256: 65198835b86b1d5771112b0085d1da52a6ede36508b1aaa6cae2aedc765dfe10 + requires_dist: + - aiobotocore>=2.19.0,<4.0.0 + - fsspec==2026.2.0 + - aiohttp!=4.0.0a0,!=4.0.0a1 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/32/e9/c1d43543da87cd27e8e2a74db85cf0b6c5cff2d5f04a86bd584d2fbc2bb0/scanpy-1.11.5-py3-none-any.whl name: scanpy version: 1.11.5 @@ -3196,8 +3529,8 @@ packages: requires_python: '>=3.8' - pypi: ./ name: segger - version: 0.1.0 - sha256: 800a085c1d8e3c13b06804ff039a14bc4f9789fe51414139e12b0dfc7505ad3d + version: 0.2.0 + sha256: 90ad8c33bbab808b877deb569839bcb2b1f7bbc6ef01866f717b08989ee9bea3 requires_dist: - anndata - cyclopts @@ -3214,6 +3547,7 @@ packages: - scikit-image - scikit-learn - tifffile + - tqdm requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/44/7c/64d18f2374e19ba9bee52dee885ec81f808a1863ed0995495b83319b88bc/session_info2-0.4-py3-none-any.whl name: session-info2 @@ -3243,6 +3577,62 @@ packages: - pytest-subprocess ; extra == 'test' - testing-common-database ; extra == 'test' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl + name: setuptools + version: 84.0.0 + sha256: 51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.13.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + - mypy==1.18.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: shapely version: 2.1.2 @@ -3263,10 +3653,75 @@ packages: version: 1.17.0 sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl + name: slicerator + version: 1.1.0 + sha256: 167668d48c6d3a5ba0bd3d54b2688e81ee267dc20aef299e547d711e6f3c441a - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl name: sortedcontainers version: 2.4.0 sha256: a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 +- pypi: https://files.pythonhosted.org/packages/b9/c1/16eb09d03eb7096b901e816dc93f53819f83b867c7d5dbb075f837d0120b/spatial_image-1.2.1-py3-none-any.whl + name: spatial-image + version: 1.2.1 + sha256: 312c7f289284ed7f72eae50ca1b29b25b74db73f6daff745666af5ec27ad5436 + requires_dist: + - numpy + - xarray-dataclasses + - xarray>=2024.10.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/14/96/015832d04de478b5ccf089fd259bff58adcff195cf27fec29b2bed307a46/spatialdata-0.4.0-py3-none-any.whl + name: spatialdata + version: 0.4.0 + sha256: f0a3f76efda57a221a775686a4d376093bfea560492697f40820eb3ec6d6ea89 + requires_dist: + - anndata>=0.9.1 + - click + - dask-image + - dask>=2024.4.1,<=2024.11.2 + - fsspec + - geopandas>=0.14 + - multiscale-spatial-image>=2.0.2 + - networkx + - numba>=0.55.0 + - numpy + - ome-zarr>=0.8.4 + - pandas + - pooch + - pyarrow + - rich + - scikit-image + - scipy + - setuptools + - shapely>=2.0.1 + - spatial-image>=1.1.0 + - typing-extensions>=4.8.0 + - xarray-dataclasses>=1.8.0 + - xarray-schema + - xarray-spatial>=0.3.5 + - xarray>=2024.10.0 + - zarr<3 + - asv ; extra == 'benchmark' + - bump2version ; extra == 'dev' + - ipython>=8.6.0 ; extra == 'docs' + - myst-nb ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-book-theme>=1.0.0 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinx-pytest ; extra == 'docs' + - sphinx>=4.5 ; extra == 'docs' + - sphinxcontrib-bibtex>=1.0.0 ; extra == 'docs' + - napari-spatialdata[all] ; extra == 'extra' + - spatialdata-io ; extra == 'extra' + - spatialdata-plot ; extra == 'extra' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - torch ; extra == 'test' + - torch ; extra == 'torch' + requires_python: '>=3.10,<3.13' - pypi: https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: statsmodels version: 0.14.6 @@ -3377,7 +3832,7 @@ packages: version: 1.1.0 sha256: 15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 requires_python: '>=3.9' -- pypi: https://download.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp311-cp311-linux_x86_64.whl +- pypi: https://download-r2.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp311-cp311-linux_x86_64.whl name: torch version: 2.5.1+cu121 sha256: c8ab8c92eab928a93c483f83ca8c63f13dafc10fc93ad90ed2dcb7c82ea50410 @@ -3633,7 +4088,7 @@ packages: - kornia>=0.6.7 ; extra == 'dev' - statsmodels>0.13.5 ; extra == 'dev' requires_python: '>=3.10' -- pypi: https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp311-cp311-linux_x86_64.whl +- pypi: https://download-r2.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp311-cp311-linux_x86_64.whl name: torchvision version: 0.20.1+cu121 sha256: 237609a3551c2b68f32bcd3f3875dca93836010d83922ef2f3bd3a7540caee58 @@ -3680,7 +4135,7 @@ packages: - pytest ; extra == 'testing' - scikit-learn ; extra == 'testing' requires_python: '>=3.8' -- pypi: https://download.pytorch.org/whl/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://download-r2.pytorch.org/whl/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: triton version: 3.1.0 sha256: 0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c @@ -3698,10 +4153,6 @@ packages: - matplotlib ; extra == 'tutorials' - pandas ; extra == 'tutorials' - tabulate ; extra == 'tutorials' -- pypi: https://download.pytorch.org/whl/typing_extensions-4.15.0-py3-none-any.whl - name: typing-extensions - version: 4.15.0 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl name: typing-extensions version: 4.15.0 @@ -3790,6 +4241,111 @@ packages: - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: wrapt + version: 2.3.0 + sha256: fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/40/ed/1c4631ad5909487ea8907cd326d9855c2207d790e3936e77bda48173b8be/xarray-2024.11.0-py3-none-any.whl + name: xarray + version: 2024.11.0 + sha256: 6ee94f63ddcbdd0cf3909d1177f78cdac756640279c0e32ae36819a89cdaba37 + requires_dist: + - numpy>=1.24 + - packaging>=23.2 + - pandas>=2.1 + - scipy ; extra == 'accel' + - bottleneck ; extra == 'accel' + - numbagg ; extra == 'accel' + - numba>=0.54 ; extra == 'accel' + - flox ; extra == 'accel' + - opt-einsum ; extra == 'accel' + - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' + - hypothesis ; extra == 'dev' + - jinja2 ; extra == 'dev' + - mypy ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-env ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-autosummary-accessors ; extra == 'dev' + - xarray[complete] ; extra == 'dev' + - netcdf4 ; extra == 'io' + - h5netcdf ; extra == 'io' + - scipy ; extra == 'io' + - pydap ; python_full_version < '3.10' and extra == 'io' + - zarr ; extra == 'io' + - fsspec ; extra == 'io' + - cftime ; extra == 'io' + - pooch ; extra == 'io' + - sparse ; extra == 'etc' + - dask[complete] ; extra == 'parallel' + - cartopy ; extra == 'viz' + - matplotlib ; extra == 'viz' + - nc-time-axis ; extra == 'viz' + - seaborn ; extra == 'viz' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/aa/7b/9adbd82e2b7b8571da23856a890324b42cfbc38ae0ceac2177e5256f7216/xarray_dataclasses-1.9.1-py3-none-any.whl + name: xarray-dataclasses + version: 1.9.1 + sha256: 3590556ef795ebbb33908b484a58311192d880cbb511666fe4c354569c3c468f + requires_dist: + - numpy>=1.22,<1.25 ; python_full_version == '3.8.*' + - numpy>=1.22,<3.0 ; python_full_version >= '3.9' and python_full_version < '3.14' + - typing-extensions>=4.0,<5.0 + - xarray>=2022.3,<2023.2 ; python_full_version == '3.8.*' + - xarray>=2022.3,<2025.0 ; python_full_version >= '3.9' and python_full_version < '3.14' + requires_python: '>=3.8,<3.14' +- pypi: https://files.pythonhosted.org/packages/a9/6d/f585a27b380ee987619b5617c0ca672a71a4345b67cfedbb6299750ce845/xarray_schema-0.0.3-py3-none-any.whl + name: xarray-schema + version: 0.0.3 + sha256: aa6f856626b2e100213ba290407797464608b2555bb8e0b26093a97fe1ba38ce + requires_dist: + - xarray>=0.16 + - numpy>=1.21 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/61/ee/9d0bb9f1bbaf347efe2ca6ad7bae259d6ed9e7a2b4441bd16b88723145a1/xarray_spatial-0.5.3-py3-none-any.whl + name: xarray-spatial + version: 0.5.3 + sha256: 84f5a3e07a1ac3212bcfff3c4677daa3c529a533f1493eb555f8e498b7d9746e + requires_dist: + - datashader>=0.15.0 + - numba + - xarray + - numpy + - dask[dataframe] ; extra == 'doc' + - geopandas ; extra == 'doc' + - jinja2>=2.11 ; extra == 'doc' + - ipykernel ; extra == 'doc' + - matplotlib ; extra == 'doc' + - nbsphinx ; extra == 'doc' + - numpydoc ; extra == 'doc' + - pandoc ; extra == 'doc' + - pydata-sphinx-theme ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-panels ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - awkward>=1.4 ; extra == 'optional' + - geopandas ; extra == 'optional' + - shapely ; extra == 'optional' + - spatialpandas ; extra == 'optional' + - rtxpy ; extra == 'optional' + - flake8 ; extra == 'tests' + - isort ; extra == 'tests' + - noise>=1.2.2 ; extra == 'tests' + - dask ; extra == 'tests' + - pyarrow ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - scipy ; extra == 'tests' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: xxhash version: 3.6.0 @@ -3804,6 +4360,28 @@ packages: - multidict>=4.0 - propcache>=0.2.1 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl + name: zarr + version: 2.18.7 + sha256: ac3dc4033e9ae4e9d7b5e27c97ea3eaf1003cc0a07f010bd83d5134bf8c4b223 + requires_dist: + - asciitree + - numpy>=1.24 + - fasteners ; sys_platform != 'emscripten' + - numcodecs>=0.10.0,!=0.14.0,!=0.14.1,<0.16 + - notebook ; extra == 'jupyter' + - ipytree>=0.2.2 ; extra == 'jupyter' + - ipywidgets>=8.0.0 ; extra == 'jupyter' + - sphinx ; extra == 'docs' + - sphinx-automodapi ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinx-issues ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - numpydoc ; extra == 'docs' + - numcodecs[msgpack]!=0.14.0,!=0.14.1,<0.16 ; extra == 'docs' + - pytest-doctestplus ; extra == 'docs' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl name: zarr version: 3.1.5 diff --git a/pixi.toml b/pixi.toml index b27ccad..73fc6f5 100644 --- a/pixi.toml +++ b/pixi.toml @@ -37,9 +37,10 @@ cudf-cu12 = "==24.10.*" cuml-cu12 = "==24.10.*" cugraph-cu12 = "==24.10.*" cupy-cuda12x = ">=12.2,<13.0" +spatialdata = ">=0.4.0, <0.5" [tool.uv.extra-build-dependencies] torch-scatter = ["torch"] [environments] -cuda121 = ["cuda121"] \ No newline at end of file +cuda121 = ["cuda121"] diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 19c27d7..3ec26e2 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -170,7 +170,7 @@ def export( import importlib.util if importlib.util.find_spec("spatialdata") is None: - raise ImportError("The 'spatialdata' element needs the spatialdata package: pip install segger[spatialdata]") + raise ImportError("The 'spatialdata' element needs the spatialdata package. Make sure spatialdata is installed in your environment, for example with `pip install spatialdata`.") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") sdata_dest = output_directory / sdata_path.name From 18bcc509fb673662d404830a46055cb4f8074754 Mon Sep 17 00:00:00 2001 From: tobiaspk Date: Wed, 26 Aug 2026 14:41:49 -0400 Subject: [PATCH 08/19] Parallelize cell boundary generation across CPUs Runs cell_boundary for each cell in a ProcessPoolExecutor sized to the SLURM-allocated CPU count instead of a single-threaded loop. --- src/segger/export/boundary.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/segger/export/boundary.py b/src/segger/export/boundary.py index 65ea72b..2a30466 100644 --- a/src/segger/export/boundary.py +++ b/src/segger/export/boundary.py @@ -7,6 +7,8 @@ from __future__ import annotations +import os +from concurrent.futures import ProcessPoolExecutor from typing import Literal, Optional, Union import geopandas as gpd @@ -184,6 +186,13 @@ def cell_boundary( return poly +def _build_one_boundary(inputs: tuple) -> tuple: + """Run ``cell_boundary`` for one cell; returns (cell_id, n_transcripts, geometry).""" + cid, pts, method, smoothing, connectivity = inputs + geom = cell_boundary(pts, method=method, smoothing=smoothing, connectivity=connectivity) + return str(cid), len(pts), geom + + def generate_boundaries( transcripts: Union[pl.DataFrame, pd.DataFrame], cell_id: str = "cell_id", @@ -203,11 +212,18 @@ def generate_boundaries( n_groups = grouped.ngroups groups = ((cid, g[[x, y]].to_numpy()) for cid, g in grouped) - ids, n_tx, geoms = [], [], [] - for cid, pts in tqdm(groups, total=n_groups, desc="Building cell boundaries"): - ids.append(str(cid)) - n_tx.append(len(pts)) - geoms.append(cell_boundary(pts, method=method, smoothing=smoothing, connectivity=connectivity)) + inputs = [(cid, pts, method, smoothing, connectivity) for cid, pts in groups] + + n_workers = len(os.sched_getaffinity(0)) + with ProcessPoolExecutor(max_workers=n_workers) as pool: + results = list( + tqdm( + pool.map(_build_one_boundary, inputs, chunksize=10), + total=n_groups, + desc="Building cell boundaries", + ) + ) + ids, n_tx, geoms = zip(*results) if results else ((), (), ()) # Output the SpatialData instance key as "cell_id" regardless of the input column name. Keep it as # a column too: geoparquet drops a named index, and it must match the table instance key to join. From dc9c7d5334002dc1a0cc8c9cd44e5887d6131947 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Thu, 27 Aug 2026 11:38:29 -0400 Subject: [PATCH 09/19] Add setuptools dependency to cuda121 env spatialdata's xarray_schema dependency imports pkg_resources, which comes from setuptools; not otherwise pulled in by the resolved env. --- pixi.lock | 17 +++++++++-------- pixi.toml | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pixi.lock b/pixi.lock index cddf7d0..d84af50 100644 --- a/pixi.lock +++ b/pixi.lock @@ -175,7 +175,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/7c/64d18f2374e19ba9bee52dee885ec81f808a1863ed0995495b83319b88bc/session_info2-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl @@ -3577,10 +3577,10 @@ packages: - pytest-subprocess ; extra == 'test' - testing-common-database ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl name: setuptools - version: 84.0.0 - sha256: 51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 + version: 81.0.0 + sha256: fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6 requires_dist: - pytest>=6,!=8.1.* ; extra == 'test' - virtualenv>=13.0.0 ; extra == 'test' @@ -3621,18 +3621,19 @@ packages: - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' - wheel>=0.43.0 ; extra == 'core' + - platformdirs>=4.2.2 ; extra == 'core' - jaraco-functools>=4 ; extra == 'core' - more-itertools ; extra == 'core' - - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-checkdocs>=2.4 ; extra == 'check' - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - ruff>=0.13.0 ; sys_platform != 'cygwin' and extra == 'check' - pytest-cov ; extra == 'cover' - - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' - mypy==1.18.* ; extra == 'type' - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' - requires_python: '>=3.10' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: shapely version: 2.1.2 diff --git a/pixi.toml b/pixi.toml index 73fc6f5..7d048d2 100644 --- a/pixi.toml +++ b/pixi.toml @@ -38,6 +38,7 @@ cuml-cu12 = "==24.10.*" cugraph-cu12 = "==24.10.*" cupy-cuda12x = ">=12.2,<13.0" spatialdata = ">=0.4.0, <0.5" +setuptools = ">=81.0.0, <82" [tool.uv.extra-build-dependencies] torch-scatter = ["torch"] From d48625a5e49ae2134b287e2c01d8d2858bd7c1ce Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Thu, 27 Aug 2026 11:38:34 -0400 Subject: [PATCH 10/19] Fix parallel boundary generation: list unpacking and worker count zip(*results) produced tuples, which geopandas' set_geometry mistakes for a column-name lookup instead of geometry values, dumping every polygon into the error message. Convert to lists. Also leave one CPU free instead of using every allocated core. --- src/segger/export/boundary.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/segger/export/boundary.py b/src/segger/export/boundary.py index 2a30466..3307d90 100644 --- a/src/segger/export/boundary.py +++ b/src/segger/export/boundary.py @@ -214,7 +214,7 @@ def generate_boundaries( inputs = [(cid, pts, method, smoothing, connectivity) for cid, pts in groups] - n_workers = len(os.sched_getaffinity(0)) + n_workers = max(len(os.sched_getaffinity(0)) - 1, 1) with ProcessPoolExecutor(max_workers=n_workers) as pool: results = list( tqdm( @@ -223,10 +223,9 @@ def generate_boundaries( desc="Building cell boundaries", ) ) - ids, n_tx, geoms = zip(*results) if results else ((), (), ()) + ids, n_tx, geoms = map(list, zip(*results)) if results else ([], [], []) - # Output the SpatialData instance key as "cell_id" regardless of the input column name. Keep it as - # a column too: geoparquet drops a named index, and it must match the table instance key to join. + # Output the SpatialData instance key as "cell_id" regardless of the input column name. gdf = gpd.GeoDataFrame( {"cell_id": ids, "n_transcripts": n_tx}, geometry=geoms, index=pd.Index(ids, name="cell_id") ) From d1ec959e045e873c42bf8023cb6a4411685c1776 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Thu, 27 Aug 2026 11:38:40 -0400 Subject: [PATCH 11/19] Write spatialdata export elements in place, avoid dask-expr crash - Disable dask's query-planning backend at the top of segger/__init__.py: cudf is imported unconditionally by every CLI subcommand and pulls in dask.dataframe before export code ever runs, so setting the config lazily inside the export path was too late. - _write_to_sdata now adds elements directly to the given --sdata store instead of copytree-ing it into the output directory first; avoids the 'cannot overwrite' error from the raw Xenium sdata's transcripts/ cell_boundaries/table already occupying those element names. - Fail fast: check the target element names don't already exist before doing any of the (expensive) segmentation/boundary work. - Add --spatialdata-element-prefix (default '_segger') to name segger's elements distinctly from the source sdata's own. - -o/--output-directory is now optional, only required for the anndata/transcripts/boundaries elements. --- src/segger/__init__.py | 5 +++ src/segger/cli/export.py | 79 +++++++++++++++++++++++++++------------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/segger/__init__.py b/src/segger/__init__.py index 67cb121..a4ef99d 100644 --- a/src/segger/__init__.py +++ b/src/segger/__init__.py @@ -1,6 +1,11 @@ import logging import os from pathlib import Path + +import dask + +dask.config.set({"dataframe.query-planning": False}) # spatialdata doesn't yet support dask-expr; must be set before cudf pulls in dask.dataframe + import cupy as cp import torch import rmm diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 3ec26e2..807649b 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -6,10 +6,9 @@ -s $PATH_OUTPUT/segger_segmentation.parquet \ -o $PATH_OUTPUT/adata_export - # save spatialdata - sdata object must exist already, this will copy it + # save spatialdata - adds elements directly to an existing sdata object, no -o needed segger export spatialdata \ -s $PATH_OUTPUT/segger_segmentation.parquet \ - -o $PATH_OUTPUT/sdata_export \ --sdata $PATH_INPUT/sdata.zarr """ @@ -36,14 +35,17 @@ help="Source transcripts directory. Only needed for segger v0.2.0 (before x/y/feature_name were included in outputs).", ), ] -_Out = Annotated[Path, Parameter(alias="-o", group=_group_io)] +_Out = Annotated[ + Optional[Path], + Parameter(alias="-o", group=_group_io, help="Required unless the only element being exported is 'spatialdata'."), +] _Sdata = Annotated[ Optional[Path], Parameter( alias="--sdata", group=_group_io, validator=validators.Path(exists=True, dir_okay=True), - help="Existing SpatialData Zarr store to copy into the output directory and add elements to (required for 'spatialdata').", + help="Existing SpatialData Zarr store to add elements to in place (required for 'spatialdata').", ), ] @@ -66,6 +68,14 @@ help="Minimum number of assigned transcripts a cell must have to be included.", ), ] +_SpatialdataElementPrefix = Annotated[ + str, + Parameter( + group=_group_opts, + help="Appended to spatialdata element names, e.g. '_segger' -> 'transcripts_segger' " + "(avoids colliding with an existing same-named element, e.g. from the raw Xenium sdata).", + ), +] def _legacy_join(seg: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.DataFrame": @@ -124,31 +134,42 @@ def _load_assigned( return assigned -def _write_to_sdata(sdata_path: Path, dest: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData") -> Path: - """Copy the source SpatialData store to ``dest``, then add segger's elements to the copy.""" - import shutil +def _sdata_element_names(spatialdata_element_prefix: str) -> list: + return [ + f"transcripts{spatialdata_element_prefix}", + f"cell_boundaries{spatialdata_element_prefix}", + f"table{spatialdata_element_prefix}" + ] + +def _check_sdata_writable(sdata_path: Path, spatialdata_element_prefix: str) -> None: + """Fail fast if any target element name already exists, before doing any of the actual work.""" + kinds = ("points", "shapes", "tables") + for kind, name in zip(kinds, _sdata_element_names(spatialdata_element_prefix)): + if (sdata_path / kind / name).exists(): + raise FileExistsError(f"{sdata_path / kind / name} already exists; pick a different --spatialdata-element-prefix.") + + +def _write_to_sdata(sdata_path: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData", spatialdata_element_prefix: str = "") -> None: + """Add segger's elements directly to the existing SpatialData store at ``sdata_path``.""" import spatialdata from spatialdata.models import PointsModel, ShapesModel, TableModel - print(f"Copying {sdata_path} to {dest}...") - shutil.copytree(sdata_path, dest) - - sdata = spatialdata.read_zarr(dest) - sdata["transcripts"] = PointsModel.parse( + names = _sdata_element_names(spatialdata_element_prefix) + sdata = spatialdata.read_zarr(sdata_path) + sdata[names[0]] = PointsModel.parse( assigned.to_pandas(), coordinates={"x": "x", "y": "y"}, feature_key="feature_name", instance_key="segger_cell_id" ) - sdata["cell_boundaries"] = ShapesModel.parse(gdf) - sdata["table"] = TableModel.parse(adata) + sdata[names[1]] = ShapesModel.parse(gdf) + sdata[names[2]] = TableModel.parse(adata) - print(f"Writing transcripts, cell_boundaries and table to {dest}...") - sdata.write_element(["transcripts", "cell_boundaries", "table"], overwrite=True) - return dest + print(f"Writing {', '.join(names)} to {sdata_path}...") + sdata.write_element(names, overwrite=True) def export( *elements: Annotated[_Element, Parameter(help="Elements to write (default: anndata boundaries).")], segmentation_path: _Seg, - output_directory: _Out, + output_directory: _Out = None, source_path: _Source = None, sdata_path: _Sdata = None, method: Annotated[ @@ -161,11 +182,11 @@ def export( include_all_transcripts: _IncludeAll = False, min_similarity: _MinSim = None, min_transcripts: _MinTx = 10, + spatialdata_element_prefix: _SpatialdataElementPrefix = "_segger", ): """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS - sdata_dest = None if "spatialdata" in selected: import importlib.util @@ -173,12 +194,13 @@ def export( raise ImportError("The 'spatialdata' element needs the spatialdata package. Make sure spatialdata is installed in your environment, for example with `pip install spatialdata`.") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") - sdata_dest = output_directory / sdata_path.name - if sdata_dest.exists(): - raise FileExistsError(f"{sdata_dest} already exists; aborting to avoid overwriting an existing SpatialData store.") + _check_sdata_writable(sdata_path, spatialdata_element_prefix) + if set(selected) - {"spatialdata"} and output_directory is None: + raise ValueError("-o/--output-directory is required unless the only element being exported is 'spatialdata'.") assigned = _load_assigned(segmentation_path, source_path, include_all_transcripts, min_similarity, min_transcripts) - output_directory.mkdir(parents=True, exist_ok=True) + if output_directory is not None: + output_directory.mkdir(parents=True, exist_ok=True) # compute outputs gdf = None @@ -189,7 +211,12 @@ def export( adata = None if "anndata" in selected or "spatialdata" in selected: from ..export import build_anndata - adata = build_anndata(assigned, cell_id="segger_cell_id", area=gdf.geometry.area if gdf is not None else None) + adata = build_anndata( + assigned, + cell_id="segger_cell_id", + area=gdf.geometry.area if gdf is not None else None, + region=f"cell_boundaries{spatialdata_element_prefix}", + ) # save outputs if "transcripts" in selected: @@ -205,5 +232,5 @@ def export( print(f"Wrote AnnData ({adata.n_obs} cells x {adata.n_vars} genes): {output_directory / 'adata.h5ad'}") if "spatialdata" in selected: - dest = _write_to_sdata(sdata_path, sdata_dest, assigned, gdf, adata) - print(f"Added transcripts, cell_boundaries and table to {dest}") + _write_to_sdata(sdata_path, assigned, gdf, adata, spatialdata_element_prefix=spatialdata_element_prefix) + print(f"Added {', '.join(_sdata_element_names(spatialdata_element_prefix))} to {sdata_path}") From b0e198c767095a115509c619cc4375f2ac3008ce Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Fri, 28 Aug 2026 12:19:43 -0400 Subject: [PATCH 12/19] Thread an optional z coordinate through export and training writers Points and table centroids include z when present; no-op until a reader emits it. Spatialdata writes now fail on a name collision instead of overwriting. Co-authored-by: Elyas Heidari <55977725+EliHei2@users.noreply.github.com> Co-authored-by: enric-bazz --- src/segger/cli/export.py | 17 +++++++++++++---- src/segger/data/writer.py | 12 +++++++----- src/segger/export/anndata_writer.py | 13 +++++++++---- src/segger/io/fields.py | 1 + 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 807649b..7fd4388 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -120,12 +120,15 @@ def _load_assigned( else: keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) + coord_cols = [pl.col(std.x).alias("x"), pl.col(std.y).alias("y")] + if std.z in merged.columns: + coord_cols.append(pl.col(std.z).alias("z")) + assigned = merged.filter(keep).select( pl.col(std.row_index), pl.col("segger_cell_id").cast(pl.String), pl.col(std.feature).alias("feature_name"), - pl.col(std.x).alias("x"), - pl.col(std.y).alias("y"), + *coord_cols, ) if min_transcripts > 0: @@ -144,6 +147,7 @@ def _sdata_element_names(spatialdata_element_prefix: str) -> list: def _check_sdata_writable(sdata_path: Path, spatialdata_element_prefix: str) -> None: """Fail fast if any target element name already exists, before doing any of the actual work.""" kinds = ("points", "shapes", "tables") + # TODO: Load sdata, instead of assuming that files exist for kind, name in zip(kinds, _sdata_element_names(spatialdata_element_prefix)): if (sdata_path / kind / name).exists(): raise FileExistsError(f"{sdata_path / kind / name} already exists; pick a different --spatialdata-element-prefix.") @@ -156,14 +160,18 @@ def _write_to_sdata(sdata_path: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDat names = _sdata_element_names(spatialdata_element_prefix) sdata = spatialdata.read_zarr(sdata_path) + coordinates = {"x": "x", "y": "y"} + if "z" in assigned.columns: + coordinates["z"] = "z" + # TODO: Consider using the transformations from the base elements! sdata[names[0]] = PointsModel.parse( - assigned.to_pandas(), coordinates={"x": "x", "y": "y"}, feature_key="feature_name", instance_key="segger_cell_id" + assigned.to_pandas(), coordinates=coordinates, feature_key="feature_name", instance_key="segger_cell_id" ) sdata[names[1]] = ShapesModel.parse(gdf) sdata[names[2]] = TableModel.parse(adata) print(f"Writing {', '.join(names)} to {sdata_path}...") - sdata.write_element(names, overwrite=True) + sdata.write_element(names, overwrite=False) def export( @@ -214,6 +222,7 @@ def export( adata = build_anndata( assigned, cell_id="segger_cell_id", + z="z", area=gdf.geometry.area if gdf is not None else None, region=f"cell_boundaries{spatialdata_element_prefix}", ) diff --git a/src/segger/data/writer.py b/src/segger/data/writer.py index dfc1139..7f62be5 100644 --- a/src/segger/data/writer.py +++ b/src/segger/data/writer.py @@ -110,6 +110,10 @@ def write_anndata( tx_fields = TrainingTranscriptFields() tx = trainer.datamodule.tx + coordinate_columns = [tx_fields.x, tx_fields.y] + if tx_fields.z in tx.columns: + coordinate_columns.append(tx_fields.z) + transcripts = ( segmentation .filter( @@ -118,8 +122,7 @@ def write_anndata( .join( tx.select([ tx_fields.row_index, - tx_fields.x, - tx_fields.y, + *coordinate_columns, tx_fields.feature, ]), on=tx_fields.row_index, @@ -132,8 +135,7 @@ def write_anndata( "segger_cell_id", "segger_similarity", "similarity_threshold", - tx_fields.x, - tx_fields.y, + *coordinate_columns, ]) ) @@ -142,7 +144,7 @@ def write_anndata( feature_column="segger_gene", cell_id_column="segger_cell_id", score_column="segger_similarity", - coordinate_columns=[tx_fields.x, tx_fields.y], + coordinate_columns=coordinate_columns, ) adata.write_h5ad(self.output_directory / 'segger_anndata.h5ad') diff --git a/src/segger/export/anndata_writer.py b/src/segger/export/anndata_writer.py index b957e56..a8921bc 100644 --- a/src/segger/export/anndata_writer.py +++ b/src/segger/export/anndata_writer.py @@ -15,19 +15,24 @@ def build_anndata( feature: str = "feature_name", x: str = "x", y: str = "y", + z: Optional[str] = None, region: str = "cell_boundaries", area: Optional[pd.Series] = None, ) -> AnnData: """Cell x gene table built on :func:`anndata_from_transcripts`, with the SpatialData link added. - ``obs`` is indexed by the cell id; centroids land in ``obsm["spatial"]`` and the table-to-shapes - link in ``uns["spatialdata_attrs"]``. ``area`` (a per-cell Series, e.g. the exported boundary - polygon areas) is written to ``obs["area"]`` when given. + ``obs`` is indexed by cell id, with centroids in ``obsm["spatial"]`` (3D if ``z`` is given and + present) and the table-to-shapes link in ``uns["spatialdata_attrs"]``. ``area``, when given, is + written to ``obs["area"]``. """ from ..data.utils.anndata import anndata_from_transcripts + coordinate_columns = [x, y] + if z is not None and z in assigned.columns: + coordinate_columns.append(z) + adata = anndata_from_transcripts( - assigned, feature_column=feature, cell_id_column=cell_id, coordinate_columns=[x, y] + assigned, feature_column=feature, cell_id_column=cell_id, coordinate_columns=coordinate_columns ) if "X_spatial" in adata.obsm: adata.obsm["spatial"] = adata.obsm.pop("X_spatial") diff --git a/src/segger/io/fields.py b/src/segger/io/fields.py index 8dc30a9..315965c 100644 --- a/src/segger/io/fields.py +++ b/src/segger/io/fields.py @@ -107,6 +107,7 @@ class StandardTranscriptFields: row_index: str = 'row_index' x: str = 'x' y: str = 'y' + z: str = 'z' feature: str = 'feature_name' cell_id: str = 'cell_id' compartment: str = 'cell_compartment' From 6515b6e5ab63f15409dede3ba1cb1c838e89a2c9 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Mon, 31 Aug 2026 14:59:02 -0400 Subject: [PATCH 13/19] Append segger columns to existing transcripts element instead of a new one Per PR #80 discussion: transcripts_segger duplicated the sdata transcripts table and dropped unassigned/control transcripts. Now left-joins segger's columns (segger_cell_id, segger_similarity, segger_similarity_threshold, segger_converged, segger_filtered) onto the existing points element in place, with unassigned transcripts left null and a new segger_unused flag for rows segger never saw at all. --- src/segger/cli/export.py | 145 +++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 7fd4388..b32f1a0 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -72,10 +72,17 @@ str, Parameter( group=_group_opts, - help="Appended to spatialdata element names, e.g. '_segger' -> 'transcripts_segger' " + help="Appended to the new spatialdata element names, e.g. '_segger' -> 'cell_boundaries_segger' " "(avoids colliding with an existing same-named element, e.g. from the raw Xenium sdata).", ), ] +_TranscriptsElement = Annotated[ + str, + Parameter( + group=_group_opts, + help="Name of the existing points element in --sdata to append segger's columns to.", + ), +] def _legacy_join(seg: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.DataFrame": @@ -92,9 +99,22 @@ def _legacy_join(seg: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.D return tx.join(seg.select(pred_cols), on=std.row_index, how="left") +def _load_merged(segmentation_path: Path, source_path: Optional[Path]) -> "pl.DataFrame": + """Full segmentation output (every transcript segger considered), joined with x/y/feature_name when not already inline.""" + import polars as pl + + from ..io import StandardTranscriptFields + + std = StandardTranscriptFields() + seg = pl.read_parquet(segmentation_path) + if "segger_cell_id" not in seg.columns: + raise ValueError(f"No 'segger_cell_id' column in {segmentation_path}.") + + return seg if {std.x, std.y, std.feature} <= set(seg.columns) else _legacy_join(seg, source_path, std) + + def _load_assigned( - segmentation_path: Path, - source_path: Optional[Path], + merged: "pl.DataFrame", include_all_transcripts: bool, min_similarity: Optional[float], min_transcripts: int = 10, @@ -105,11 +125,6 @@ def _load_assigned( from ..io import StandardTranscriptFields std = StandardTranscriptFields() - seg = pl.read_parquet(segmentation_path) - if "segger_cell_id" not in seg.columns: - raise ValueError(f"No 'segger_cell_id' column in {segmentation_path}.") - - merged = seg if {std.x, std.y, std.feature} <= set(seg.columns) else _legacy_join(seg, source_path, std) if include_all_transcripts: keep = pl.col("segger_cell_id").is_not_null() @@ -139,38 +154,96 @@ def _load_assigned( def _sdata_element_names(spatialdata_element_prefix: str) -> list: return [ - f"transcripts{spatialdata_element_prefix}", f"cell_boundaries{spatialdata_element_prefix}", f"table{spatialdata_element_prefix}" ] -def _check_sdata_writable(sdata_path: Path, spatialdata_element_prefix: str) -> None: - """Fail fast if any target element name already exists, before doing any of the actual work.""" - kinds = ("points", "shapes", "tables") - # TODO: Load sdata, instead of assuming that files exist - for kind, name in zip(kinds, _sdata_element_names(spatialdata_element_prefix)): - if (sdata_path / kind / name).exists(): - raise FileExistsError(f"{sdata_path / kind / name} already exists; pick a different --spatialdata-element-prefix.") +def _check_sdata_writable(sdata, transcripts_element: str, spatialdata_element_prefix: str) -> None: + """Fail fast if the transcripts element is missing, or any target element name already exists.""" + if transcripts_element not in sdata.points: + raise KeyError(f"{transcripts_element!r} not found in sdata.points; pass --transcripts-element to point at the right one.") + cell_boundaries_name, table_name = _sdata_element_names(spatialdata_element_prefix) + if cell_boundaries_name in sdata.shapes: + raise FileExistsError(f"{cell_boundaries_name!r} already exists in sdata.shapes; pick a different --spatialdata-element-prefix.") + if table_name in sdata.tables: + raise FileExistsError(f"{table_name!r} already exists in sdata.tables; pick a different --spatialdata-element-prefix.") -def _write_to_sdata(sdata_path: Path, assigned: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData", spatialdata_element_prefix: str = "") -> None: - """Add segger's elements directly to the existing SpatialData store at ``sdata_path``.""" - import spatialdata +_SEGGER_COLUMN_RENAMES = { + "similarity_threshold": "segger_similarity_threshold", + "converged": "segger_converged", + "filtered": "segger_filtered", +} + + +def _segger_transcript_columns(merged: "pl.DataFrame", row_index: str) -> "pl.DataFrame": + """Segger's per-transcript outputs, renamed for joining onto an existing transcripts table.""" + import polars as pl + + present = [c for c in _SEGGER_COLUMN_RENAMES if c in merged.columns] + return ( + merged.select(row_index, "segger_cell_id", "segger_similarity", *present) + .rename({c: _SEGGER_COLUMN_RENAMES[c] for c in present}) + .with_columns(pl.col("segger_cell_id").cast(pl.String)) + ) + + +def _append_segger_to_transcripts(sdata, merged: "pl.DataFrame", std, transcripts_element: str) -> "pl.DataFrame": + """Left-join segger's outputs onto the sdata store's existing transcripts table. + + Assumes ``std.row_index`` was assigned over the same rows, in the same order, as the existing + transcripts table (true when segger's preprocessor read the same source file spatialdata was + built from). Rows absent from ``merged`` -- e.g. control probes or low-quality transcripts + dropped before segger ever saw them -- are flagged ``segger_unused``; the rest get segger's own + columns, including a null ``segger_cell_id`` for anything segger didn't assign. + """ + import polars as pl + + existing = sdata.points[transcripts_element] + existing = existing.compute() if hasattr(existing, "compute") else existing + tx = pl.from_pandas(existing).with_row_index(name=std.row_index) + + seg_cols = _segger_transcript_columns(merged, std.row_index).with_columns(pl.lit(True).alias("_segger_seen")) + return ( + tx.join(seg_cols, on=std.row_index, how="left") + .with_columns(pl.col("_segger_seen").is_null().alias("segger_unused")) + .drop(std.row_index, "_segger_seen") + ) + + +def _write_to_sdata( + sdata, + merged: "pl.DataFrame", + gdf: "gpd.GeoDataFrame", + adata: "AnnData", + spatialdata_element_prefix: str = "", + transcripts_element: str = "transcripts", +) -> None: + """Append segger's columns to the existing transcripts element, and add its boundaries/table elements to the given SpatialData store.""" from spatialdata.models import PointsModel, ShapesModel, TableModel + from ..io import StandardTranscriptFields + + std = StandardTranscriptFields() names = _sdata_element_names(spatialdata_element_prefix) - sdata = spatialdata.read_zarr(sdata_path) + + attrs = sdata.points[transcripts_element].attrs.get("spatialdata_attrs", {}) + joined = _append_segger_to_transcripts(sdata, merged, std, transcripts_element) coordinates = {"x": "x", "y": "y"} - if "z" in assigned.columns: + if "z" in joined.columns: coordinates["z"] = "z" # TODO: Consider using the transformations from the base elements! - sdata[names[0]] = PointsModel.parse( - assigned.to_pandas(), coordinates=coordinates, feature_key="feature_name", instance_key="segger_cell_id" + sdata[transcripts_element] = PointsModel.parse( + joined.to_pandas(), + coordinates=coordinates, + feature_key=attrs.get("feature_key"), + instance_key=attrs.get("instance_key"), ) - sdata[names[1]] = ShapesModel.parse(gdf) - sdata[names[2]] = TableModel.parse(adata) + sdata[names[0]] = ShapesModel.parse(gdf) + sdata[names[1]] = TableModel.parse(adata) - print(f"Writing {', '.join(names)} to {sdata_path}...") + print(f"Writing {transcripts_element}, {', '.join(names)} to {sdata.path}...") + sdata.write_element([transcripts_element], overwrite=True) sdata.write_element(names, overwrite=False) @@ -191,10 +264,12 @@ def export( min_similarity: _MinSim = None, min_transcripts: _MinTx = 10, spatialdata_element_prefix: _SpatialdataElementPrefix = "_segger", + transcripts_element: _TranscriptsElement = "transcripts", ): """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS + sdata = None if "spatialdata" in selected: import importlib.util @@ -202,11 +277,15 @@ def export( raise ImportError("The 'spatialdata' element needs the spatialdata package. Make sure spatialdata is installed in your environment, for example with `pip install spatialdata`.") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") - _check_sdata_writable(sdata_path, spatialdata_element_prefix) + import spatialdata as _spatialdata + + sdata = _spatialdata.read_zarr(sdata_path) + _check_sdata_writable(sdata, transcripts_element, spatialdata_element_prefix) if set(selected) - {"spatialdata"} and output_directory is None: raise ValueError("-o/--output-directory is required unless the only element being exported is 'spatialdata'.") - assigned = _load_assigned(segmentation_path, source_path, include_all_transcripts, min_similarity, min_transcripts) + merged = _load_merged(segmentation_path, source_path) + assigned = _load_assigned(merged, include_all_transcripts, min_similarity, min_transcripts) if output_directory is not None: output_directory.mkdir(parents=True, exist_ok=True) @@ -241,5 +320,11 @@ def export( print(f"Wrote AnnData ({adata.n_obs} cells x {adata.n_vars} genes): {output_directory / 'adata.h5ad'}") if "spatialdata" in selected: - _write_to_sdata(sdata_path, assigned, gdf, adata, spatialdata_element_prefix=spatialdata_element_prefix) - print(f"Added {', '.join(_sdata_element_names(spatialdata_element_prefix))} to {sdata_path}") + _write_to_sdata( + sdata, + merged, + gdf, + adata, + spatialdata_element_prefix=spatialdata_element_prefix, + transcripts_element=transcripts_element, + ) From a1e223b92d085ae8fac17077500ab8e187b92406 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Mon, 31 Aug 2026 15:57:20 -0400 Subject: [PATCH 14/19] Simplify export.py to rely on the 'filtered' column directly Rework transcripts loading around a single load_transcripts() that trusts segger's own 'filtered' flag instead of recomputing similarity thresholds client-side, and rename the sdata element options to explicit --sdata-transcripts-name/--sdata-cell-boundaries-name/--sdata-table-name. Fix two gaps that would have broken the spatialdata path: load_transcripts now returns the full per-transcript frame alongside the assigned subset (needed to append segger's columns onto the sdata transcripts table), and _merge_sdata_transcripts keeps row_index through its select so the join onto the sdata table doesn't fail. --- src/segger/cli/export.py | 251 +++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 143 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index b32f1a0..1ff3fdc 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -16,9 +16,11 @@ from pathlib import Path from typing import Annotated, Literal, Optional - from cyclopts import Parameter, Group, validators +import polars as pl +from ..io import StandardTranscriptFields + _group_io = Group(name="I/O", sort_key=0) _group_opts = Group(name="Options", sort_key=1) @@ -51,200 +53,159 @@ _IncludeAll = Annotated[ bool, - Parameter(group=_group_opts, help="Keep every cell-assigned transcript, ignoring the similarity threshold."), -] -_MinSim = Annotated[ - Optional[float], - Parameter( - group=_group_opts, - help="Custom required similarity threshold, overriding the per-gene threshold from segmentation.", - ), + Parameter(group=_group_opts, help="Keep every transcript in the segmentation output, not just the ones segger's 'filtered' flag marked as kept."), ] -_MinTx = Annotated[ - int, +_SdataTranscriptsName = Annotated[ + str, Parameter( group=_group_opts, - validator=validators.Number(gte=0), - help="Minimum number of assigned transcripts a cell must have to be included.", + help="Name of the existing points element in --sdata to append segger's columns to.", ), ] -_SpatialdataElementPrefix = Annotated[ +_SdataCellBoundariesName = Annotated[ str, Parameter( group=_group_opts, - help="Appended to the new spatialdata element names, e.g. '_segger' -> 'cell_boundaries_segger' " + help="Name of the shapes element segger's cell boundaries are written to " "(avoids colliding with an existing same-named element, e.g. from the raw Xenium sdata).", ), ] -_TranscriptsElement = Annotated[ +_SdataTableName = Annotated[ str, Parameter( group=_group_opts, - help="Name of the existing points element in --sdata to append segger's columns to.", + help="Name of the table element segger's AnnData is written to.", ), ] -def _legacy_join(seg: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.DataFrame": - """Join x/y/feature_name onto a segmentation output written before they were included inline.""" - if source_path is None: - raise ValueError("This segmentation output predates inline x/y/feature_name; pass -i/--source-path to join them from the source transcripts.") - import polars as pl - - from ..io import get_preprocessor - - tx = get_preprocessor(source_path).transcripts - tx = tx.collect() if isinstance(tx, pl.LazyFrame) else tx - pred_cols = [c for c in (std.row_index, "segger_cell_id", "segger_similarity", "similarity_threshold") if c in seg.columns] - return tx.join(seg.select(pred_cols), on=std.row_index, how="left") - - -def _load_merged(segmentation_path: Path, source_path: Optional[Path]) -> "pl.DataFrame": - """Full segmentation output (every transcript segger considered), joined with x/y/feature_name when not already inline.""" - import polars as pl - - from ..io import StandardTranscriptFields - - std = StandardTranscriptFields() - seg = pl.read_parquet(segmentation_path) - if "segger_cell_id" not in seg.columns: - raise ValueError(f"No 'segger_cell_id' column in {segmentation_path}.") - - return seg if {std.x, std.y, std.feature} <= set(seg.columns) else _legacy_join(seg, source_path, std) - - -def _load_assigned( - merged: "pl.DataFrame", +# -- LOAD TRANSCRIPTS -- +def load_transcripts( + segmentation_path: Path, include_all_transcripts: bool, - min_similarity: Optional[float], - min_transcripts: int = 10, -) -> "pl.DataFrame": - """Return the kept assigned tx (row_index/segger_cell_id/feature_name/x/y).""" - import polars as pl - - from ..io import StandardTranscriptFields + source_path: Path = None, +): + # read transcripts + tx = pl.read_parquet(segmentation_path) + # check if legacy result (before v0.2.0) std = StandardTranscriptFields() + if not {std.x, std.y, std.feature, "filtered"} <= set(tx.columns): + tx = _legacy_join(tx, source_path=source_path, std=std) - if include_all_transcripts: - keep = pl.col("segger_cell_id").is_not_null() - elif min_similarity is not None: - keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= min_similarity) - elif "filtered" in merged.columns: - keep = pl.col("filtered") - else: - keep = pl.col("segger_cell_id").is_not_null() & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) - + # subset coord_cols = [pl.col(std.x).alias("x"), pl.col(std.y).alias("y")] - if std.z in merged.columns: + if std.z in tx.columns: coord_cols.append(pl.col(std.z).alias("z")) - assigned = merged.filter(keep).select( + # filter + kept = tx.filter(pl.col("filtered")) if not include_all_transcripts else tx + + # select + assigned = kept.select( pl.col(std.row_index), pl.col("segger_cell_id").cast(pl.String), pl.col(std.feature).alias("feature_name"), *coord_cols, ) - if min_transcripts > 0: - assigned = assigned.filter(pl.len().over("segger_cell_id") >= min_transcripts) - - return assigned + return assigned, tx -def _sdata_element_names(spatialdata_element_prefix: str) -> list: - return [ - f"cell_boundaries{spatialdata_element_prefix}", - f"table{spatialdata_element_prefix}" - ] +def _legacy_join(tx: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.DataFrame": + """Join x/y/feature_name onto a segmentation output written before they were included inline.""" + if source_path is None: + raise ValueError("This segmentation output predates inline x/y/feature_name; pass -i/--source-path to join them from the source transcripts.") -def _check_sdata_writable(sdata, transcripts_element: str, spatialdata_element_prefix: str) -> None: + from ..io import get_preprocessor + + # load and merge + tx_all = get_preprocessor(source_path).transcripts + pred_cols = [c for c in (std.row_index, "segger_cell_id", "segger_similarity", "similarity_threshold") if c in tx.columns] + tx = tx.select(pred_cols).join(tx_all, on=std.row_index, how="left") + + # add "filtered" + tx = tx.with_columns(( + (pl.col("segger_cell_id").is_not_null()) & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) + ).alias("filtered")) + + return tx + +# -- Spatial Data Support +def _check_sdata_elements( + sdata, + element_names: dict, + ) -> None: """Fail fast if the transcripts element is missing, or any target element name already exists.""" - if transcripts_element not in sdata.points: - raise KeyError(f"{transcripts_element!r} not found in sdata.points; pass --transcripts-element to point at the right one.") - cell_boundaries_name, table_name = _sdata_element_names(spatialdata_element_prefix) - if cell_boundaries_name in sdata.shapes: - raise FileExistsError(f"{cell_boundaries_name!r} already exists in sdata.shapes; pick a different --spatialdata-element-prefix.") - if table_name in sdata.tables: - raise FileExistsError(f"{table_name!r} already exists in sdata.tables; pick a different --spatialdata-element-prefix.") - + if element_names["transcripts"] not in sdata.points: + raise KeyError(f"{element_names['transcripts']!r} not found in sdata.points; pass --sdata-transcripts-name to point at the right one.") + if element_names["cell_boundaries"] in sdata.shapes: + raise FileExistsError(f"{element_names['cell_boundaries']!r} already exists in sdata.shapes; pass --sdata-cell-boundaries-name to specify a different name.") + if element_names["table"] in sdata.tables: + raise FileExistsError(f"{element_names['table']!r} already exists in sdata.tables; pass --sdata-table-name to specify a different name.") -_SEGGER_COLUMN_RENAMES = { - "similarity_threshold": "segger_similarity_threshold", - "converged": "segger_converged", - "filtered": "segger_filtered", -} -def _segger_transcript_columns(merged: "pl.DataFrame", row_index: str) -> "pl.DataFrame": +def _merge_sdata_transcripts(sdata_tx: "pl.DataFrame", tx: "pl.DataFrame", row_index: str) -> "pl.DataFrame": """Segger's per-transcript outputs, renamed for joining onto an existing transcripts table.""" - import polars as pl - - present = [c for c in _SEGGER_COLUMN_RENAMES if c in merged.columns] - return ( - merged.select(row_index, "segger_cell_id", "segger_similarity", *present) - .rename({c: _SEGGER_COLUMN_RENAMES[c] for c in present}) - .with_columns(pl.col("segger_cell_id").cast(pl.String)) - ) - - -def _append_segger_to_transcripts(sdata, merged: "pl.DataFrame", std, transcripts_element: str) -> "pl.DataFrame": - """Left-join segger's outputs onto the sdata store's existing transcripts table. - - Assumes ``std.row_index`` was assigned over the same rows, in the same order, as the existing - transcripts table (true when segger's preprocessor read the same source file spatialdata was - built from). Rows absent from ``merged`` -- e.g. control probes or low-quality transcripts - dropped before segger ever saw them -- are flagged ``segger_unused``; the rest get segger's own - columns, including a null ``segger_cell_id`` for anything segger didn't assign. - """ - import polars as pl - - existing = sdata.points[transcripts_element] - existing = existing.compute() if hasattr(existing, "compute") else existing - tx = pl.from_pandas(existing).with_row_index(name=std.row_index) - - seg_cols = _segger_transcript_columns(merged, std.row_index).with_columns(pl.lit(True).alias("_segger_seen")) - return ( - tx.join(seg_cols, on=std.row_index, how="left") - .with_columns(pl.col("_segger_seen").is_null().alias("segger_unused")) - .drop(std.row_index, "_segger_seen") - ) + # rename; these names are hardcoded. make sure to update this if any name should change going forward + map_columns = { + "segger_cell_id": "segger_cell_id", + "segger_similarity": "segger_similarity", + "similarity_threshold": "segger_similarity_threshold", + "converged": "segger_converged", + "filtered": "segger_filtered", + } + tx = tx \ + .rename(map_columns) \ + .select(row_index, *map_columns.values()) \ + + # sdata_tx is already passed in as a DataFrame + if hasattr(sdata_tx, "compute"): + sdata_tx = sdata_tx.compute() + sdata_tx = pl.from_pandas(sdata_tx).with_row_index(name=row_index) + + # merge + sdata_tx = sdata_tx \ + .join(tx, on=row_index, how="left") \ + .with_columns(pl.col("segger_filtered").is_not_null().alias("segger_seen")) \ + + return sdata_tx def _write_to_sdata( sdata, - merged: "pl.DataFrame", + tx: "pl.DataFrame", gdf: "gpd.GeoDataFrame", adata: "AnnData", - spatialdata_element_prefix: str = "", transcripts_element: str = "transcripts", + cell_boundaries_element: str = "cell_boundaries_segger", + table_element: str = "table_segger", ) -> None: """Append segger's columns to the existing transcripts element, and add its boundaries/table elements to the given SpatialData store.""" from spatialdata.models import PointsModel, ShapesModel, TableModel - from ..io import StandardTranscriptFields - std = StandardTranscriptFields() - names = _sdata_element_names(spatialdata_element_prefix) attrs = sdata.points[transcripts_element].attrs.get("spatialdata_attrs", {}) - joined = _append_segger_to_transcripts(sdata, merged, std, transcripts_element) + tx = _merge_sdata_transcripts(sdata.points[transcripts_element], tx, "row_index") + coordinates = {"x": "x", "y": "y"} - if "z" in joined.columns: + if "z" in tx.columns: coordinates["z"] = "z" # TODO: Consider using the transformations from the base elements! sdata[transcripts_element] = PointsModel.parse( - joined.to_pandas(), + tx.to_pandas(), coordinates=coordinates, feature_key=attrs.get("feature_key"), instance_key=attrs.get("instance_key"), ) - sdata[names[0]] = ShapesModel.parse(gdf) - sdata[names[1]] = TableModel.parse(adata) + sdata[cell_boundaries_element] = ShapesModel.parse(gdf) + sdata[table_element] = TableModel.parse(adata) - print(f"Writing {transcripts_element}, {', '.join(names)} to {sdata.path}...") + print(f"Writing {transcripts_element}, {cell_boundaries_element}, {table_element} to {sdata.path}...") sdata.write_element([transcripts_element], overwrite=True) - sdata.write_element(names, overwrite=False) + sdata.write_element([cell_boundaries_element, table_element], overwrite=False) def export( @@ -261,10 +222,9 @@ def export( int, Parameter(group=_group_opts, help="Chaikin corner-cutting iterations to round boundaries (0 disables).") ] = 0, include_all_transcripts: _IncludeAll = False, - min_similarity: _MinSim = None, - min_transcripts: _MinTx = 10, - spatialdata_element_prefix: _SpatialdataElementPrefix = "_segger", - transcripts_element: _TranscriptsElement = "transcripts", + sdata_transcripts_name: _SdataTranscriptsName = "transcripts", + sdata_cell_boundaries_name: _SdataCellBoundariesName = "cell_boundaries_segger", + sdata_table_name: _SdataTableName = "table_segger", ): """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS @@ -280,12 +240,16 @@ def export( import spatialdata as _spatialdata sdata = _spatialdata.read_zarr(sdata_path) - _check_sdata_writable(sdata, transcripts_element, spatialdata_element_prefix) + _check_sdata_elements(sdata, { + "transcripts": sdata_transcripts_name, + "cell_boundaries": sdata_cell_boundaries_name, + "table": sdata_table_name, + }) + if set(selected) - {"spatialdata"} and output_directory is None: raise ValueError("-o/--output-directory is required unless the only element being exported is 'spatialdata'.") - merged = _load_merged(segmentation_path, source_path) - assigned = _load_assigned(merged, include_all_transcripts, min_similarity, min_transcripts) + assigned, tx = load_transcripts(segmentation_path, include_all_transcripts, source_path) if output_directory is not None: output_directory.mkdir(parents=True, exist_ok=True) @@ -303,7 +267,7 @@ def export( cell_id="segger_cell_id", z="z", area=gdf.geometry.area if gdf is not None else None, - region=f"cell_boundaries{spatialdata_element_prefix}", + region=sdata_cell_boundaries_name, ) # save outputs @@ -322,9 +286,10 @@ def export( if "spatialdata" in selected: _write_to_sdata( sdata, - merged, + tx, gdf, adata, - spatialdata_element_prefix=spatialdata_element_prefix, - transcripts_element=transcripts_element, + transcripts_element=sdata_transcripts_name, + cell_boundaries_element=sdata_cell_boundaries_name, + table_element=sdata_table_name, ) From 7ec59c1af60abab13d9ce7d32845102fdea0e2e6 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Mon, 31 Aug 2026 16:02:02 -0400 Subject: [PATCH 15/19] Take element names as separate args in _check_sdata_elements Matches _write_to_sdata's signature style instead of a dict. --- src/segger/cli/export.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 1ff3fdc..c6bb851 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -134,15 +134,17 @@ def _legacy_join(tx: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.Da # -- Spatial Data Support def _check_sdata_elements( sdata, - element_names: dict, + transcripts_element: str, + cell_boundaries_element: str, + table_element: str, ) -> None: """Fail fast if the transcripts element is missing, or any target element name already exists.""" - if element_names["transcripts"] not in sdata.points: - raise KeyError(f"{element_names['transcripts']!r} not found in sdata.points; pass --sdata-transcripts-name to point at the right one.") - if element_names["cell_boundaries"] in sdata.shapes: - raise FileExistsError(f"{element_names['cell_boundaries']!r} already exists in sdata.shapes; pass --sdata-cell-boundaries-name to specify a different name.") - if element_names["table"] in sdata.tables: - raise FileExistsError(f"{element_names['table']!r} already exists in sdata.tables; pass --sdata-table-name to specify a different name.") + if transcripts_element not in sdata.points: + raise KeyError(f"{transcripts_element!r} not found in sdata.points; pass --sdata-transcripts-name to point at the right one.") + if cell_boundaries_element in sdata.shapes: + raise FileExistsError(f"{cell_boundaries_element!r} already exists in sdata.shapes; pass --sdata-cell-boundaries-name to specify a different name.") + if table_element in sdata.tables: + raise FileExistsError(f"{table_element!r} already exists in sdata.tables; pass --sdata-table-name to specify a different name.") @@ -231,20 +233,12 @@ def export( sdata = None if "spatialdata" in selected: - import importlib.util - - if importlib.util.find_spec("spatialdata") is None: - raise ImportError("The 'spatialdata' element needs the spatialdata package. Make sure spatialdata is installed in your environment, for example with `pip install spatialdata`.") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") - import spatialdata as _spatialdata - - sdata = _spatialdata.read_zarr(sdata_path) - _check_sdata_elements(sdata, { - "transcripts": sdata_transcripts_name, - "cell_boundaries": sdata_cell_boundaries_name, - "table": sdata_table_name, - }) + import spatialdata as sd + + sdata = sd.read_zarr(sdata_path) + _check_sdata_elements(sdata, sdata_transcripts_name, sdata_cell_boundaries_name, sdata_table_name) if set(selected) - {"spatialdata"} and output_directory is None: raise ValueError("-o/--output-directory is required unless the only element being exported is 'spatialdata'.") From 047716e907ec7bd7391c4157c5e4de38ead6b4f5 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Mon, 31 Aug 2026 17:34:15 -0400 Subject: [PATCH 16/19] Propagate base-element transformations in spatialdata export, merge lazily in dask PointsModel/ShapesModel.parse default to an Identity transform on 'global' when none is passed, silently dropping whatever scale/affine the base transcripts element carried. _write_to_sdata now reads it via get_transformation() and passes it through to both the rewritten transcripts and the new boundaries. _merge_sdata_transcripts no longer round-trips the base transcripts through pandas/polars/pandas; it merges lazily in dask (broadcast join, cumsum-built row_index) and is only materialized once, right before write_element() overwrites the on-disk store in place (required, since write_element refuses to overwrite a path a live dask graph still reads from). include_all_transcripts now defaults to True. --- src/segger/cli/export.py | 51 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index c6bb851..4d82297 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -128,6 +128,12 @@ def _legacy_join(tx: "pl.DataFrame", source_path: Optional[Path], std) -> "pl.Da tx = tx.with_columns(( (pl.col("segger_cell_id").is_not_null()) & (pl.col("segger_similarity") >= pl.col("similarity_threshold")) ).alias("filtered")) + + # if "converged" exists, also require this to be true + if "converged" in tx.columns: + tx = tx.with_columns( + (pl.col("filtered") & pl.col("converged")).alias("filtered") + ) return tx @@ -148,8 +154,8 @@ def _check_sdata_elements( -def _merge_sdata_transcripts(sdata_tx: "pl.DataFrame", tx: "pl.DataFrame", row_index: str) -> "pl.DataFrame": - """Segger's per-transcript outputs, renamed for joining onto an existing transcripts table.""" +def _merge_sdata_transcripts(sdata_tx: "dd.DataFrame", tx: "pl.DataFrame", row_index: str) -> "dd.DataFrame": + """Segger's per-transcript outputs, joined onto the existing (dask-backed) transcripts table. Stays lazy throughout.""" # rename; these names are hardcoded. make sure to update this if any name should change going forward map_columns = { @@ -159,19 +165,15 @@ def _merge_sdata_transcripts(sdata_tx: "pl.DataFrame", tx: "pl.DataFrame", row_i "converged": "segger_converged", "filtered": "segger_filtered", } - tx = tx \ - .rename(map_columns) \ - .select(row_index, *map_columns.values()) \ + tx = tx.rename(map_columns).select(row_index, *map_columns.values()).to_pandas() - # sdata_tx is already passed in as a DataFrame - if hasattr(sdata_tx, "compute"): - sdata_tx = sdata_tx.compute() - sdata_tx = pl.from_pandas(sdata_tx).with_row_index(name=row_index) + # sdata_tx has no row_index column; construct it + sdata_tx = sdata_tx.assign(**{row_index: 1}) + sdata_tx[row_index] = sdata_tx[row_index].cumsum() - 1 - # merge - sdata_tx = sdata_tx \ - .join(tx, on=row_index, how="left") \ - .with_columns(pl.col("segger_filtered").is_not_null().alias("segger_seen")) \ + # merge (tx is small enough to broadcast onto every partition; no shuffle needed) + sdata_tx = sdata_tx.merge(tx, on=row_index, how="left") + sdata_tx["segger_seen"] = sdata_tx["segger_filtered"].notnull() return sdata_tx @@ -186,23 +188,30 @@ def _write_to_sdata( ) -> None: """Append segger's columns to the existing transcripts element, and add its boundaries/table elements to the given SpatialData store.""" from spatialdata.models import PointsModel, ShapesModel, TableModel + from spatialdata.transformations import get_transformation - std = StandardTranscriptFields() + # get current transcripts + base_transcripts = sdata.points[transcripts_element] + attrs = base_transcripts.attrs.get("spatialdata_attrs", {}) + transformations = get_transformation(base_transcripts, get_all=True) - attrs = sdata.points[transcripts_element].attrs.get("spatialdata_attrs", {}) - tx = _merge_sdata_transcripts(sdata.points[transcripts_element], tx, "row_index") + # add segger info; merge stays lazy in dask, but must be materialized before overwriting + # transcripts_element on disk below, since it still lazily reads from that same store. + tx = _merge_sdata_transcripts(base_transcripts, tx, "row_index").compute().reset_index(drop=True) coordinates = {"x": "x", "y": "y"} if "z" in tx.columns: coordinates["z"] = "z" - # TODO: Consider using the transformations from the base elements! + + # add model sdata[transcripts_element] = PointsModel.parse( - tx.to_pandas(), + tx, coordinates=coordinates, feature_key=attrs.get("feature_key"), instance_key=attrs.get("instance_key"), + transformations=transformations, ) - sdata[cell_boundaries_element] = ShapesModel.parse(gdf) + sdata[cell_boundaries_element] = ShapesModel.parse(gdf, transformations=transformations) sdata[table_element] = TableModel.parse(adata) print(f"Writing {transcripts_element}, {cell_boundaries_element}, {table_element} to {sdata.path}...") @@ -223,7 +232,7 @@ def export( chaikin_iterations: Annotated[ int, Parameter(group=_group_opts, help="Chaikin corner-cutting iterations to round boundaries (0 disables).") ] = 0, - include_all_transcripts: _IncludeAll = False, + include_all_transcripts: _IncludeAll = True, sdata_transcripts_name: _SdataTranscriptsName = "transcripts", sdata_cell_boundaries_name: _SdataCellBoundariesName = "cell_boundaries_segger", sdata_table_name: _SdataTableName = "table_segger", @@ -236,13 +245,13 @@ def export( if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") import spatialdata as sd - sdata = sd.read_zarr(sdata_path) _check_sdata_elements(sdata, sdata_transcripts_name, sdata_cell_boundaries_name, sdata_table_name) if set(selected) - {"spatialdata"} and output_directory is None: raise ValueError("-o/--output-directory is required unless the only element being exported is 'spatialdata'.") + # load tx assigned, tx = load_transcripts(segmentation_path, include_all_transcripts, source_path) if output_directory is not None: output_directory.mkdir(parents=True, exist_ok=True) From cf3f58ec1b092ca3f90de175a72a319c800a1454 Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Tue, 1 Sep 2026 11:47:38 -0400 Subject: [PATCH 17/19] Fix in-place spatialdata overwrite and drop row_index from transcripts write_element(overwrite=True) always fails for an element inside its own store (discussion #520). Back up transcripts, detach in-memory handles, then delete-then-write. Also drop the row_index join key from output. --- src/segger/cli/export.py | 45 ++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index 4d82297..b5e03b9 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -175,6 +175,9 @@ def _merge_sdata_transcripts(sdata_tx: "dd.DataFrame", tx: "pl.DataFrame", row_i sdata_tx = sdata_tx.merge(tx, on=row_index, how="left") sdata_tx["segger_seen"] = sdata_tx["segger_filtered"].notnull() + # row_index is only a join key; drop it from the output + sdata_tx = sdata_tx.drop(columns=[row_index]) + return sdata_tx def _write_to_sdata( @@ -195,28 +198,52 @@ def _write_to_sdata( attrs = base_transcripts.attrs.get("spatialdata_attrs", {}) transformations = get_transformation(base_transcripts, get_all=True) - # add segger info; merge stays lazy in dask, but must be materialized before overwriting - # transcripts_element on disk below, since it still lazily reads from that same store. + # new element fully in memory, required for overwriting tx = _merge_sdata_transcripts(base_transcripts, tx, "row_index").compute().reset_index(drop=True) coordinates = {"x": "x", "y": "y"} if "z" in tx.columns: coordinates["z"] = "z" - # add model - sdata[transcripts_element] = PointsModel.parse( + # build the new (in-memory, unbacked) elements + new_transcripts = PointsModel.parse( tx, coordinates=coordinates, feature_key=attrs.get("feature_key"), instance_key=attrs.get("instance_key"), transformations=transformations, ) - sdata[cell_boundaries_element] = ShapesModel.parse(gdf, transformations=transformations) - sdata[table_element] = TableModel.parse(adata) - + new_boundaries = ShapesModel.parse(gdf, transformations=transformations) + new_table = TableModel.parse(adata) + + # SpatialData can't overwrite (#520). Do 1) backup, 2) drop in-memory handles, 3) delete original, 4) write new elements, 5) drop backup + # 1 backup + backup_element = f"{transcripts_element}_backup" + sdata[backup_element] = base_transcripts + sdata.write_element([backup_element]) + + # 2 detach in-memory + del sdata.points[backup_element] + sdata[transcripts_element] = new_transcripts + sdata[cell_boundaries_element] = new_boundaries + sdata[table_element] = new_table + + # 3 4 delete and write print(f"Writing {transcripts_element}, {cell_boundaries_element}, {table_element} to {sdata.path}...") - sdata.write_element([transcripts_element], overwrite=True) - sdata.write_element([cell_boundaries_element, table_element], overwrite=False) + try: + sdata.delete_element_from_disk(transcripts_element) + sdata.write_element([transcripts_element, cell_boundaries_element, table_element]) + except Exception: + print( + f"Write failed. The original {transcripts_element!r} is preserved on disk as " + f"{backup_element!r}; restore it from there." + ) + raise + else: + # delete backup + sdata.delete_element_from_disk(backup_element) + if sdata.has_consolidated_metadata(): + sdata.write_consolidated_metadata() def export( From b09195c87839696f664515085876660fac76ad7c Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Tue, 1 Sep 2026 11:59:00 -0400 Subject: [PATCH 18/19] Add --min-counts filter and align export table to boundaries Drop cells below min_counts from the table/anndata, and keep only cells that produced a boundary polygon so the table annotates only real shapes (no dangling SpatialData annotations). Require min_counts >= 3 for spatialdata, since boundaries need >= 3 transcripts. --- src/segger/cli/export.py | 16 ++++++++++++++++ src/segger/export/anndata_writer.py | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index b5e03b9..e8f7e58 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -77,6 +77,14 @@ help="Name of the table element segger's AnnData is written to.", ), ] +_MinCounts = Annotated[ + int, + Parameter( + group=_group_opts, + help="Drop cells with fewer than this many assigned transcripts from the table/anndata " + "(must be >= 3 for spatialdata, since boundaries need >= 3 points).", + ), +] # -- LOAD TRANSCRIPTS -- @@ -263,12 +271,16 @@ def export( sdata_transcripts_name: _SdataTranscriptsName = "transcripts", sdata_cell_boundaries_name: _SdataCellBoundariesName = "cell_boundaries_segger", sdata_table_name: _SdataTableName = "table_segger", + min_counts: _MinCounts = 10, ): """Write a segger segmentation as scverse SpatialData elements (anndata, transcripts, boundaries, spatialdata).""" selected = elements or _DEFAULT_ELEMENTS sdata = None if "spatialdata" in selected: + if min_counts < 3: + # cell boundaries need >= 3 transcripts to form a polygon; a lower table cutoff leaves cells without one + raise ValueError("--min-counts must be >= 3 for spatialdata: boundaries need >= 3 transcripts to form a polygon.") if sdata_path is None: raise ValueError("--sdata is required when exporting 'spatialdata'.") import spatialdata as sd @@ -298,7 +310,11 @@ def export( z="z", area=gdf.geometry.area if gdf is not None else None, region=sdata_cell_boundaries_name, + min_counts=min_counts, ) + # keep only cells that produced a boundary polygon, so the table annotates only real shapes + if gdf is not None: + adata = adata[adata.obs_names.isin(gdf.index)].copy() # save outputs if "transcripts" in selected: diff --git a/src/segger/export/anndata_writer.py b/src/segger/export/anndata_writer.py index a8921bc..e9d3989 100644 --- a/src/segger/export/anndata_writer.py +++ b/src/segger/export/anndata_writer.py @@ -18,6 +18,7 @@ def build_anndata( z: Optional[str] = None, region: str = "cell_boundaries", area: Optional[pd.Series] = None, + min_counts: int = 1, ) -> AnnData: """Cell x gene table built on :func:`anndata_from_transcripts`, with the SpatialData link added. @@ -42,6 +43,9 @@ def build_anndata( if area is not None: adata.obs["area"] = pd.Series(area).reindex(adata.obs_names).to_numpy() + if min_counts > 1: + adata = adata[adata.obs["n_transcripts"] >= min_counts].copy() + # SpatialData link: region/instance_key obs columns plus the attrs that join table to shapes. adata.obs["region"] = pd.Categorical([region] * adata.n_obs, categories=[region]) adata.obs["cell_id"] = adata.obs_names.to_numpy() From 2c83537346ffe25383976dc5c034e772ec54b4cf Mon Sep 17 00:00:00 2001 From: Tobiaspk Date: Fri, 11 Sep 2026 10:17:24 -0400 Subject: [PATCH 19/19] fix: drop pre-existing segger columns before merging transcripts Re-running export against transcripts that already have segger_* columns caused merge() to silently suffix them with _x/_y instead of overwriting, corrupting the segger_seen/segger_filtered computation. --- src/segger/cli/export.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/segger/cli/export.py b/src/segger/cli/export.py index e8f7e58..8bbdfdf 100644 --- a/src/segger/cli/export.py +++ b/src/segger/cli/export.py @@ -173,12 +173,17 @@ def _merge_sdata_transcripts(sdata_tx: "dd.DataFrame", tx: "pl.DataFrame", row_i "converged": "segger_converged", "filtered": "segger_filtered", } + + # rename tx = tx.rename(map_columns).select(row_index, *map_columns.values()).to_pandas() # sdata_tx has no row_index column; construct it sdata_tx = sdata_tx.assign(**{row_index: 1}) sdata_tx[row_index] = sdata_tx[row_index].cumsum() - 1 + # delete existing (overwrite is not exactly silent - export would stop if boundaries or table already exists) + sdata_tx = sdata_tx.drop(columns=[col for col in map_columns.values() if col in sdata_tx.columns]) + # merge (tx is small enough to broadcast onto every partition; no shuffle needed) sdata_tx = sdata_tx.merge(tx, on=row_index, how="left") sdata_tx["segger_seen"] = sdata_tx["segger_filtered"].notnull()