diff --git a/.gitignore b/.gitignore index 4a404ab..313da81 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,14 @@ xenium_example/ /graphify-out/ /slurm/ /slurm-logs/ + +# ...except the segmentation pipeline, which is part of the package's public +# surface rather than scratch: the CLI wrappers are documented entry points and +# the SLURM scripts are the supported way to run them on a cluster. +!/scripts/instanseg_segment.py +!/scripts/geojson_to_spatialdata.py +!/slurm/ +/slurm/* +!/slurm/segment_node_worker.sh +!/slurm/segment_slurm.sh +!/slurm/SEGMENTATION_PLAN.md diff --git a/pyproject.toml b/pyproject.toml index e8692d4..3866662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,19 @@ dependencies = [ "tifffile", ] optional-dependencies.czi = [ "bioio", "bioio-czi" ] +# Nucleus segmentation on H&E whole-slide images. Kept optional: it pulls +# torch (multi-GB CUDA wheels), which most spatialrefinery uses do not need. +optional-dependencies.segmentation = [ + # rasterio + geojson are what InstanSeg's save_geojson=True actually imports. + # They are NOT taken via `instanseg-torch[io]`: that extra also carries + # `zarr>=2.0.0,<3`, which silently downgrades zarr/numcodecs/tiffslide and + # breaks spatialdata. Its stated reason ("tiffslide doesn't support zarr v3 + # yet") is stale as of tiffslide 4.0 (Bayer-Group/tiffslide#97). + "geojson>=3", + "instanseg-torch>=0.1.1", + "rasterio>=1.3", + "tiffslide>=4", +] # https://docs.pypi.org/project_metadata/#project-urls urls.Documentation = "https://spatialrefinery.readthedocs.io/" urls.Homepage = "https://github.com/peng-lab/spatialrefinery" @@ -153,11 +166,15 @@ module = [ "bioio.*", "dask_image.*", "geopandas.*", + # From the optional `segmentation` extra, so absent in the typecheck env. + "instanseg.*", "pyarrow.*", "scipy.*", "shapely.*", "spatialdata.*", "spatialdata_io.*", + "tiffslide.*", + "torch.*", ] ignore_missing_imports = true diff --git a/scripts/geojson_to_spatialdata.py b/scripts/geojson_to_spatialdata.py new file mode 100755 index 0000000..68f3fcb --- /dev/null +++ b/scripts/geojson_to_spatialdata.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +r"""Convert a nucleus-segmentation GeoJSON plus its slide into a SpatialData zarr. + +Thin CLI wrapper around +`spatialrefinery.segmentation.to_spatialdata.geojson_to_spatialdata`. +Pairs with `instanseg_segment.py`, consuming the `cells.geojson` it writes. + +Usage +----- + python geojson_to_spatialdata.py \\ + --geojson-path results/slide.svs/cells.geojson \\ + --zarr-outdir zarrs/ --wsi-path slide.svs --template-adata template.h5ad +""" + +import argparse +import logging +import sys +from pathlib import Path + +from spatialrefinery.segmentation.to_spatialdata import geojson_to_spatialdata + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def build_parser() -> argparse.ArgumentParser: + """Build the CLI parser.""" + parser = argparse.ArgumentParser( + description="SpatialData conversion for a single segmented WSI.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("--geojson-path", required=True) + parser.add_argument("--zarr-outdir", required=True) + parser.add_argument("--wsi-path", required=True) + parser.add_argument("--template-adata", required=True) + parser.add_argument("--no-zip", action="store_true", help="Skip the .zarr.zip archive") + parser.add_argument("--no-skip-existing", action="store_true", help="Rebuild even if the zarr exists") + return parser + + +def main() -> None: + """Convert one slide's segmentation into a SpatialData zarr.""" + args = build_parser().parse_args() + + geojson_path = Path(args.geojson_path) + wsi_path = Path(args.wsi_path) + template_adata = Path(args.template_adata) + + for label, path in (("GeoJSON", geojson_path), ("WSI", wsi_path), ("Template AnnData", template_adata)): + if not path.exists(): + logger.error("%s file not found: %s", label, path) + sys.exit(1) + + # Named with the full filename, matching the segmentation stage's layout. + zarr_path = Path(args.zarr_outdir) / f"{wsi_path.name}.zarr" + if zarr_path.exists() and not args.no_skip_existing: + logger.info("Skipping %s: %s already exists", wsi_path.name, zarr_path) + sys.exit(0) + + Path(args.zarr_outdir).mkdir(parents=True, exist_ok=True) + + try: + geojson_to_spatialdata( + geojson_path=geojson_path, + zarr_path=zarr_path, + image_path=wsi_path, + template_adata_path=template_adata, + write_zip=not args.no_zip, + ) + except Exception: + logger.exception("Conversion failed for %s", wsi_path.name) + sys.exit(1) + + print(f"ZARR_PATH={zarr_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/instanseg_segment.py b/scripts/instanseg_segment.py new file mode 100755 index 0000000..42d1f80 --- /dev/null +++ b/scripts/instanseg_segment.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Segment nuclei in one H&E whole-slide image with InstanSeg. + +Thin CLI wrapper around `spatialrefinery.segmentation.instanseg.segment_wsi`. + +Prints `GEOJSON_PATH=` on success -- the SLURM worker greps for it to +hand the result to the conversion stage. + +Usage +----- + python instanseg_segment.py --wsi-path slide.svs --outdir results/ + python instanseg_segment.py --wsi-path slide.ome.tif --outdir results/ --wsi-mpp 0.27 +""" + +import argparse +import logging +import sys +from pathlib import Path + +from spatialrefinery.segmentation.instanseg import ( + DEFAULT_DETECTION_SIZE, + DEFAULT_MODEL, + DEFAULT_OVERLAP, + DEFAULT_TILE_SIZE, + segment_wsi, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def build_parser() -> argparse.ArgumentParser: + """Build the CLI parser.""" + parser = argparse.ArgumentParser( + description="InstanSeg nucleus segmentation for a single WSI.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("--wsi-path", required=True) + parser.add_argument("--outdir", required=True) + parser.add_argument("--gpu-id", type=int, default=0, help="CUDA index; -1 forces CPU") + parser.add_argument("--model", default=DEFAULT_MODEL) + parser.add_argument("--wsi-mpp", type=float, default=None, help="Microns per pixel; read from metadata if omitted") + parser.add_argument("--tile-size", type=int, default=DEFAULT_TILE_SIZE) + parser.add_argument("--overlap", type=int, default=DEFAULT_OVERLAP) + parser.add_argument("--detection-size", type=int, default=DEFAULT_DETECTION_SIZE) + parser.add_argument( + "--clahe", + type=float, + default=None, + metavar="CLIP", + help=( + "Run CLAHE at this clip limit over each tile before inference (e.g. 2.0). " + "Off by default; helps on weakly haematoxylin-stained slides where pale " + "nuclei are missed." + ), + ) + parser.add_argument( + "--seed-threshold", + type=float, + default=None, + help="Override the model seed threshold (default 0.7). Lower detects fainter nuclei.", + ) + parser.add_argument( + "--no-otsu", + action="store_true", + help="Segment every tile instead of only those inside the tissue mask", + ) + parser.add_argument("--no-skip-existing", action="store_true", help="Re-segment even if cells.geojson exists") + return parser + + +def main() -> None: + """Run segmentation for one slide and print its GeoJSON path.""" + args = build_parser().parse_args() + + wsi_path = Path(args.wsi_path) + if not wsi_path.exists(): + logger.error("WSI file not found: %s", wsi_path) + sys.exit(1) + + try: + geojson_path = segment_wsi( + wsi_path, + args.outdir, + pixel_size=args.wsi_mpp, + gpu_id=None if args.gpu_id < 0 else args.gpu_id, + model_type=args.model, + tile_size=args.tile_size, + overlap=args.overlap, + detection_size=args.detection_size, + use_otsu_threshold=not args.no_otsu, + clahe_clip=args.clahe, + seed_threshold=args.seed_threshold, + skip_existing=not args.no_skip_existing, + ) + except Exception: + logger.exception("Segmentation failed for %s", wsi_path.name) + sys.exit(1) + + # Contract with slurm/segment_node_worker.sh -- keep this line last. + print(f"GEOJSON_PATH={geojson_path}") + + +if __name__ == "__main__": + main() diff --git a/slurm/SEGMENTATION_PLAN.md b/slurm/SEGMENTATION_PLAN.md new file mode 100644 index 0000000..90c8c9a --- /dev/null +++ b/slurm/SEGMENTATION_PLAN.md @@ -0,0 +1,178 @@ +# Nucleus segmentation on H&E whole-slide images + +Status: **implemented** on branch `worktree-instanseg-segmentation`, 2026-08-31. + +Design notes for the segmentation pipeline: `spatialrefinery.segmentation`, +its CLI wrappers in `scripts/`, and the SLURM launchers here. + +## Approach + +Segmentation uses [InstanSeg](https://github.com/instanseg/instanseg) +(`instanseg-torch`), whose `brightfield_nuclei` model is the default and is +trained for H&E. + +The decisive property for this pipeline is that InstanSeg owns the +**whole-slide layer**: `eval_whole_slide_image` handles tiling, cross-tile +label matching (`match_labels`), an Otsu tissue prefilter, and GeoJSON export. +Writing that layer by hand is the expensive part of whole-slide segmentation — +cross-tile instance deduplication in particular — so a model that ships it is +worth considerably more than one that does not. + +Its runtime requirements are also undemanding: `numpy>=1.24` and `torch>=2.0` +with no upper bounds, so it installs additively into this project's existing +environment. Verified: 31 packages added, **zero removed**; `numpy`, +`pydantic`, `zarr` and `spatialdata` all untouched. + +```python +model = InstanSeg("brightfield_nuclei", image_reader="tiffslide") +model.eval_whole_slide_image( + image=str(wsi_path), + pixel_size=wsi_mpp, # else read from slide metadata + tile_size=512, overlap=80, + use_otsu_threshold=True, # skip background tiles + save_geojson=True, +) +``` + +## Three upstream defects had to be bridged + +All three are in `instanseg` 0.1.1 and unchanged on `main`. They are patched at +call time in `segmentation/_compat.py`, never vendored, so deleting that module +is all it will take once upstream fixes them. Each is pinned by a test. + +1. **zarr 3.** `eval_whole_slide_image` builds its canvas with + `zarr.DirectoryStore`, which zarr 3 renamed to `zarr.storage.LocalStore`. + InstanSeg pins `zarr>=2.0.0,<3` in its `io` extra as a result — and that cap + collides with `spatialdata` (`zarr>=3.0.0`) and `anndata` (`zarr>=3.1`). + **Installing `instanseg-torch[io]` silently downgrades zarr, numcodecs and + tiffslide and breaks spatialdata.** The `segmentation` extra therefore names + `rasterio` and `geojson` directly. The pin's stated reason ("tiffslide + doesn't support zarr v3 yet") is stale as of tiffslide 4.0 + (Bayer-Group/tiffslide#97); only the *name* is missing, and `LocalStore` is + a drop-in for all four operations InstanSeg performs on the store. +2. **`TiffSlide` is never imported.** `read_slide` calls `TiffSlide(...)` at + `inference_class.py:236`, but every import of that name in the module is + function-local, so the global is unbound and any whole-slide call raises + `NameError` before reading a single tile. +3. **The GeoJSON it writes is invalid.** The exporter emits a comma after every + feature and then closes the array, so output ends `...}},\n]`. `json.load` + rejects it, and so does QuPath. Repaired in the artefact rather than worked + around at read time, so the published file is valid for any consumer. + +Defects 2 and 3 together mean InstanSeg's whole-slide path cannot have been run +end to end upstream. All three are worth reporting. + +## Layout + +Logic lives in the package; `scripts/` holds thin wrappers, matching +`scripts/convert_to_ometiff.py`. + +| File | Purpose | +|---|---| +| `src/spatialrefinery/segmentation/_compat.py` | the three bridges above | +| `src/spatialrefinery/segmentation/instanseg.py` | `segment_wsi()` — slide → `cells.geojson` | +| `src/spatialrefinery/segmentation/to_spatialdata.py` | `geojson_to_spatialdata()` | +| `scripts/instanseg_segment.py` | CLI; emits the `GEOJSON_PATH=` contract | +| `scripts/geojson_to_spatialdata.py` | CLI | +| `slurm/segment_node_worker.sh` | per-node worker, round-robin across GPUs | +| `slurm/segment_slurm.sh` | manifest + sbatch launcher | +| `tests/test_compat.py`, `tests/test_segmentation.py` | 24 tests | + +The two stages exchange a `cells.geojson` rather than being merged: it is a +resume boundary, so a conversion failure does not force re-segmentation. + +Implementation details worth knowing: + +- **Outputs are redirected.** InstanSeg writes its `.zarr` and `.geojson` next + to the *input* file; slides usually sit on read-only dataset mounts, so + `segment_wsi` runs the model against a symlink inside the output directory. +- **The image element is built lazily** from the slide's own pyramid via + `tifffile`'s zarr interface — a level-0 plane on the test slide is + 33427 × 11949 × 3, about 1.1 GiB if materialised. +- **CRS is cleared before centroids are taken.** `gpd.read_file` tags GeoJSON + as EPSG:4326, but these are pixel coordinates; left in place, `.centroid` is + computed against a spherical datum and every centroid drifts. +- **`.gitignore`** ignores `/scripts/*.py` and `/slurm/` as scratch; explicit + negations track this pipeline. + +## Sensitivity on pale slides + +On weakly haematoxylin-stained slides InstanSeg misses pale nuclei. The cause +is staining, not the model: the crop measured has **median lightness 205/255** +(1st percentile 119), and the nuclei found were the darker ones. InstanSeg +normalises each tile with `percentile_normalize`, a *global* stretch over the +tile; where bright cytoplasm dominates the histogram it does not lift pale +nuclei above the seed threshold (default 0.7). + +Measured on one 1000 × 1000 crop: + +| variant | nuclei | vs default | +|---|---|---| +| default | 209 | — | +| `seed_threshold=0.3` | 222 | +6% | +| percentile stretch 1–99 | 219 | +5% | +| **CLAHE clip=2** | **254** | **+22%** | +| **CLAHE clip=2 + `seed_threshold=0.4`** | **266** | **+27%** | +| haematoxylin colour deconvolution | 97 | −54% | + +Overlays confirmed the extra detections are real nuclei with tight boundaries. +Two things were ruled out: **resolution is not the bottleneck** (InstanSeg +downsamples 1.83× from 0.2738 to its native 0.5 µm; suppressing that gained ++2%), and **colour deconvolution actively hurts** — the brightfield model wants +true H&E appearance. + +Confirmed on the **whole slide** (8m52s end to end, versus 8m40s without — +CLAHE costs nothing measurable): + +| | default | `--clahe 2.0 --seed-threshold 0.4` | +|---|---|---| +| nuclei | 67,168 | **83,554 (+24.4%)** | +| median area | 318 px² | 339 px² | +| objects < 20 px² | 54 | 106 (0.13% of total) | + +The area distribution shifted **up** at every percentile from 1 to 99, which is +the reassuring direction: a flood of spurious fragments would have pulled the +median down. External sanity check: Xenium's own DAPI-based segmentation of +this sample has 97,560 cells, so the default recovered 69% of that from H&E and +CLAHE 86% — different images, so exact agreement is not expected. + +Both controls are **opt-in and off by default**, so runs stay reproducible: +`--clahe 2.0 --seed-threshold 0.4` on the CLI, `CLAHE` and `SEED_THRESHOLD` on +`segment_slurm.sh`. CLAHE is applied to L in LAB (hue preserved) by wrapping +`model._to_tensor`, the single point every tile passes through in the +whole-slide loop — no fork required. Per-tile is the right granularity for an +adaptive method; the 80 px overlap and cross-tile label matching absorb seam +discontinuities. + +## Verification + +1. Driver check — CUDA 13 wheels were the main rollout risk: + `nvidia-smi --query-gpu=driver_version --format=csv` → **595.71.05**, fine, + Turing sm_75 included. +2. Environment coherence after install — `numpy 2.4.6`, `pydantic 2.12.5`, + `zarr 3.3.0`, `spatialdata 0.8.0`, `anndata 0.13.2` all unchanged. +3. `pytest tests/` → 86 passed locally; 84 passed + 2 skipped in the CI hatch + environments on both py3.12 and py3.14 (the skips are the tests needing the + optional `segmentation` extra). `prek run --all-files` clean. +4. End to end on `Xenium_V1_hKidney_nondiseased_section_he_image.ome.tif` + (867 MB, 33427 × 11949, mpp 0.2738 read from metadata): + + | stage | result | + |---|---| + | segmentation | 7m49s, 1239 tiles, 51 MB `cells.geojson`, 83,554 nuclei | + | conversion | 67s, SpatialData zarr | + + Read back: image `DataTree[cyx] (3, 33427, 11949)` with 5 pyramid levels; + `nucleus_boundaries` 83,554 Polygons, geometry column only; table + `(83554, 377)` over the Xenium panel; `obs` = `region`, `instance_id`; + centroids within slide bounds. + +## Risks + +- **InstanSeg's WSI support is documented as "limited."** It held on a 400 Mpx + slide; validate on the largest slide in a cohort before batch rollout. +- **The `[io]` extra must never be installed** — see defect 1. +- **The +24.4% is one slide from one tissue.** If staining varies across a + cohort, `--clahe 2.0` may want tuning per batch before becoming a default. +- **GPU memory** ran ~35 GB with CLAHE versus ~12 GB without. Fine on a 46 GB + card, but lower `--tile-size` if fanning out 4 GPUs per node hits OOM. diff --git a/slurm/segment_node_worker.sh b/slurm/segment_node_worker.sh new file mode 100755 index 0000000..ebc5985 --- /dev/null +++ b/slurm/segment_node_worker.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# ============================================================================== +# InstanSeg + SpatialData: per-node worker +# ============================================================================== +# Called by srun on each allocated node. Reads a manifest chunk (one WSI path +# per line) and processes the slides in parallel across the node's GPUs, using +# round-robin assignment. +# +# Both stages run in the same interpreter, from the project venv. +# +# They exchange a cells.geojson rather than being merged: it is a resume +# boundary, so a conversion failure does not force re-segmentation. +# +# Usage: invoked by slurm/segment_slurm.sh, not directly. +# bash segment_node_worker.sh \ +# [wsi_mpp] [tile_size] [overlap] \ +# [clahe_clip] [seed_threshold] +# ============================================================================== + +set -euo pipefail + +MANIFEST_CHUNK="$1" +SEG_OUTDIR="$2" +ZARR_OUTDIR="$3" +TEMPLATE_ADATA="$4" +GPUS_PER_NODE="${5:-4}" +WSI_MPP="${6:-}" +TILE_SIZE="${7:-512}" +OVERLAP="${8:-80}" +CLAHE="${9:-}" # empty = off +SEED_THRESHOLD="${10:-}" # empty = model default (0.7) + +REPO_ROOT="${REPO_ROOT:-/p/project1/hai_1240/spatialrefinery}" +PY="${PY:-${REPO_ROOT}/.venv/bin/python}" + +SEGMENT_SCRIPT="${REPO_ROOT}/scripts/instanseg_segment.py" +CONVERT_SCRIPT="${REPO_ROOT}/scripts/geojson_to_spatialdata.py" + +LOG_DIR="${LOG_DIR:-$(dirname "$ZARR_OUTDIR")/slurm_logs}" +mkdir -p "$LOG_DIR" + +if [ ! -x "$PY" ]; then + echo "ERROR: interpreter not found: $PY" >&2 + echo " Create it with: uv sync --extra segmentation" >&2 + exit 2 +fi + +echo "=======================================================" +echo "NODE WORKER: $(hostname)" +echo " Manifest chunk: $MANIFEST_CHUNK" +echo " GPUs per node: $GPUS_PER_NODE" +echo " Interpreter: $PY" +echo " Started at: $(date -Is)" +echo "=======================================================" + +mapfile -t WSI_LIST < "$MANIFEST_CHUNK" +TOTAL_WSIS=${#WSI_LIST[@]} + +if [ "$TOTAL_WSIS" -eq 0 ]; then + echo "No WSIs in manifest chunk. Nothing to do." + exit 0 +fi + +echo "Processing $TOTAL_WSIS WSIs across $GPUS_PER_NODE GPUs..." + +OPTIONAL_ARGS=() +if [ -n "$WSI_MPP" ]; then + OPTIONAL_ARGS+=(--wsi-mpp "$WSI_MPP") +fi +if [ -n "$CLAHE" ]; then + OPTIONAL_ARGS+=(--clahe "$CLAHE") +fi +if [ -n "$SEED_THRESHOLD" ]; then + OPTIONAL_ARGS+=(--seed-threshold "$SEED_THRESHOLD") +fi + +process_wsi() { + local WSI_PATH="$1" + local GPU_ID="$2" + local SAMPLE_NAME + SAMPLE_NAME=$(basename "$WSI_PATH") + local LOG_FILE="${LOG_DIR}/${SAMPLE_NAME}_gpu${GPU_ID}.log" + + { + echo "### [$SAMPLE_NAME] GPU $GPU_ID -- $(date -Is)" + + echo "--- Stage 1: InstanSeg segmentation ---" + # The process sees one GPU, so --gpu-id stays 0. + local SEGMENT_OUT + SEGMENT_OUT=$(CUDA_VISIBLE_DEVICES="$GPU_ID" "$PY" "$SEGMENT_SCRIPT" \ + --wsi-path "$WSI_PATH" \ + --outdir "$SEG_OUTDIR" \ + --gpu-id 0 \ + --tile-size "$TILE_SIZE" \ + --overlap "$OVERLAP" \ + "${OPTIONAL_ARGS[@]}" 2>&1) + echo "$SEGMENT_OUT" + + local GEOJSON_PATH + GEOJSON_PATH=$(echo "$SEGMENT_OUT" | grep '^GEOJSON_PATH=' | tail -1 | cut -d= -f2-) + if [ -z "$GEOJSON_PATH" ]; then + GEOJSON_PATH="$SEG_OUTDIR/$SAMPLE_NAME/cells.geojson" + fi + if [ ! -f "$GEOJSON_PATH" ]; then + echo "FAILED: stage 1 produced no cells.geojson at $GEOJSON_PATH" + exit 1 + fi + echo "Stage 1 complete: $GEOJSON_PATH" + + echo "--- Stage 2: SpatialData conversion ---" + "$PY" "$CONVERT_SCRIPT" \ + --geojson-path "$GEOJSON_PATH" \ + --zarr-outdir "$ZARR_OUTDIR" \ + --wsi-path "$WSI_PATH" \ + --template-adata "$TEMPLATE_ADATA" + + echo "[$SAMPLE_NAME] done at $(date -Is)" + } > "$LOG_FILE" 2>&1 +} + +for i in "${!WSI_LIST[@]}"; do + WSI_PATH="${WSI_LIST[$i]}" + GPU_ID=$((i % GPUS_PER_NODE)) + echo "---> [$((i + 1))/$TOTAL_WSIS] $(basename "$WSI_PATH") -> GPU $GPU_ID" + + process_wsi "$WSI_PATH" "$GPU_ID" & + + if (( $(jobs -rp | wc -l) >= GPUS_PER_NODE )); then + wait -n || echo "A worker exited non-zero (continuing with remaining WSIs)" + fi +done + +echo "Waiting for remaining workers..." +wait + +echo "=======================================================" +echo "NODE WORKER: $(hostname) - COMPLETE" +echo " Processed: $TOTAL_WSIS WSIs" +echo " Finished at: $(date -Is)" +echo "=======================================================" diff --git a/slurm/segment_slurm.sh b/slurm/segment_slurm.sh new file mode 100755 index 0000000..7d1ccee --- /dev/null +++ b/slurm/segment_slurm.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# ============================================================================== +# SLURM launcher: InstanSeg segmentation + SpatialData conversion +# ============================================================================== +# Discovers WSI files, writes a manifest, and submits a job that spreads the +# slides across nodes (each with several GPUs). +# +# Usage: +# ./segment_slurm.sh [sample_subset] +# +# Examples: +# ./slurm/segment_slurm.sh /data/wsis /data/seg /data/zarr /data/template.h5ad +# NODES=4 ./slurm/segment_slurm.sh /data/wsis /data/seg /data/zarr /data/template.h5ad s1,s2 +# ============================================================================== + +set -euo pipefail + +ACCOUNT="${ACCOUNT:-hai_1240}" +PARTITION="${PARTITION:-dc-hwai}" +NODES="${NODES:-1}" +GPUS_PER_NODE="${GPUS_PER_NODE:-4}" +TIME="${TIME:-24:00:00}" +MEMORY="${MEMORY:-500G}" +CPUS_PER_NODE="${CPUS_PER_NODE:-64}" + +# Segmentation settings. InstanSeg tiles internally, so there is no batch size +# or worker pool to size here. +WSI_MPP="${WSI_MPP:-}" # empty = read from slide metadata +TILE_SIZE="${TILE_SIZE:-512}" +OVERLAP="${OVERLAP:-80}" +# Sensitivity, both off by default. Worth setting on weakly stained cohorts: +# CLAHE=2.0 with SEED_THRESHOLD=0.4 recovered 27% more nuclei on a pale kidney H&E. +CLAHE="${CLAHE:-}" +SEED_THRESHOLD="${SEED_THRESHOLD:-}" + +WSI_EXTENSIONS=("svs" "ndpi" "ome.tif" "ome.tiff" "tif" "tiff" "mrxs") + +REPO_ROOT="${REPO_ROOT:-/p/project1/hai_1240/spatialrefinery}" +PY="${PY:-${REPO_ROOT}/.venv/bin/python}" +NODE_WORKER_SCRIPT="${REPO_ROOT}/slurm/segment_node_worker.sh" + +if [ "$#" -lt 4 ] || [ "$#" -gt 5 ]; then + cat < [comma_separated_sample_ids] + +Environment variables: + NODES=4 Number of nodes (default: 1) + GPUS_PER_NODE=4 GPUs per node (default: 4) + WSI_MPP=0.2738 Microns per pixel (default: read from slide metadata) + TILE_SIZE=512 InstanSeg tile size (default: 512) + OVERLAP=80 Tile overlap in pixels (default: 80) + CLAHE=2.0 CLAHE clip limit for pale slides (default: off) + SEED_THRESHOLD=0.4 Model seed threshold (default: off, model uses 0.7) + TIME=24:00:00 Max runtime + MEMORY=500G Memory per node + REPO_ROOT=... Repo checkout (default: $REPO_ROOT) +USAGE + exit 1 +fi + +WSI_DIR="$1" +SEG_OUTDIR="$2" +ZARR_OUTDIR="$3" +TEMPLATE_ADATA="$4" +SAMPLE_SUBSET="${5:-}" + +BASE_PATH="$(dirname "$ZARR_OUTDIR")" +LOG_DIR="${BASE_PATH}/slurm_logs" +MANIFEST_DIR="${BASE_PATH}/manifests" + +mkdir -p "$SEG_OUTDIR" "$ZARR_OUTDIR" "$LOG_DIR" "$MANIFEST_DIR" + +if [ ! -d "$WSI_DIR" ]; then + echo "ERROR: WSI directory not found: $WSI_DIR" >&2 + exit 1 +fi +if [ ! -f "$TEMPLATE_ADATA" ]; then + echo "ERROR: template AnnData not found: $TEMPLATE_ADATA" >&2 + exit 1 +fi +if [ ! -f "$NODE_WORKER_SCRIPT" ]; then + echo "ERROR: node worker not found: $NODE_WORKER_SCRIPT" >&2 + exit 1 +fi +if [ ! -x "$PY" ]; then + echo "ERROR: interpreter not found: $PY" >&2 + echo " Create it with: uv sync --extra segmentation" >&2 + exit 1 +fi + +echo "Scanning for WSI files in: $WSI_DIR" +MANIFEST="${MANIFEST_DIR}/manifest_$(date +%Y%m%d_%H%M%S).txt" +: > "$MANIFEST" + +if [ -n "$SAMPLE_SUBSET" ]; then + echo " Filtering to: $SAMPLE_SUBSET" + IFS=',' read -r -a SAMPLE_IDS <<< "$SAMPLE_SUBSET" + for sample_id in "${SAMPLE_IDS[@]}"; do + for ext in "${WSI_EXTENSIONS[@]}"; do + [ -f "$WSI_DIR/${sample_id}.${ext}" ] && echo "$WSI_DIR/${sample_id}.${ext}" >> "$MANIFEST" + done + done +else + for ext in "${WSI_EXTENSIONS[@]}"; do + find "$WSI_DIR" -maxdepth 1 -name "*.${ext}" -type f >> "$MANIFEST" 2>/dev/null || true + done +fi + +sort -u -o "$MANIFEST" "$MANIFEST" +TOTAL_WSIS=$(wc -l < "$MANIFEST") + +if [ "$TOTAL_WSIS" -eq 0 ]; then + echo "No WSI files found. Exiting." + rm -f "$MANIFEST" + exit 0 +fi + +cat < Node \$i (\$TARGET_NODE): \$(wc -l < "\$CHUNK_FILE") WSIs" + + srun --nodes=1 --ntasks=1 --exclusive \\ + --nodelist="\$TARGET_NODE" \\ + bash ${NODE_WORKER_SCRIPT} \\ + "\$CHUNK_FILE" \\ + "${SEG_OUTDIR}" \\ + "${ZARR_OUTDIR}" \\ + "${TEMPLATE_ADATA}" \\ + "${GPUS_PER_NODE}" \\ + "${WSI_MPP}" \\ + "${TILE_SIZE}" \\ + "${OVERLAP}" \\ + "${CLAHE}" \\ + "${SEED_THRESHOLD}" & +done + +wait + +echo "=======================================================" +echo "JOB COMPLETE \$(date -Is)" +echo "=======================================================" + +rm -f \${CHUNK_PREFIX}* +EOF + +echo +echo "Submitted. Monitor with: squeue -u \$USER" +echo " Logs: $LOG_DIR" +echo " Manifest: $MANIFEST" diff --git a/src/spatialrefinery/__init__.py b/src/spatialrefinery/__init__.py index a296e67..fdf0595 100644 --- a/src/spatialrefinery/__init__.py +++ b/src/spatialrefinery/__init__.py @@ -16,14 +16,14 @@ "xenium_to_spatialdata_zip": "spatialrefinery.io.xenium", } -__all__ = ["__version__", "core", "io", *_LAZY_ATTRS] +__all__ = ["__version__", "core", "io", "segmentation", *_LAZY_ATTRS] def __getattr__(name: str): - """Lazily expose `spatialrefinery.core`/`.io` and the documented top-level functions.""" + """Lazily expose `spatialrefinery.core`/`.io`/`.segmentation` and the documented top-level functions.""" import importlib - if name in ("core", "io"): + if name in ("core", "io", "segmentation"): module = importlib.import_module(f"spatialrefinery.{name}") globals()[name] = module return module diff --git a/src/spatialrefinery/segmentation/__init__.py b/src/spatialrefinery/segmentation/__init__.py new file mode 100644 index 0000000..6c602a8 --- /dev/null +++ b/src/spatialrefinery/segmentation/__init__.py @@ -0,0 +1,23 @@ +"""Nucleus segmentation on H&E whole-slide images, and its SpatialData export. + +Both submodules pull heavy optional dependencies -- `instanseg` and `torch` for +`instanseg`, `spatialdata` and `geopandas` for `to_spatialdata` -- so they are +exposed lazily here, matching `spatialrefinery.core`. Install them with the +`segmentation` extra: + + uv pip install 'spatialrefinery[segmentation]' +""" + +from __future__ import annotations + +__all__ = ["instanseg", "to_spatialdata"] + + +def __getattr__(name: str): + if name in __all__: + import importlib + + module = importlib.import_module(f"spatialrefinery.segmentation.{name}") + globals()[name] = module + return module + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/spatialrefinery/segmentation/_compat.py b/src/spatialrefinery/segmentation/_compat.py new file mode 100644 index 0000000..a70933f --- /dev/null +++ b/src/spatialrefinery/segmentation/_compat.py @@ -0,0 +1,139 @@ +r"""Bridges over three InstanSeg defects that block its whole-slide path here. + +Both are upstream bugs in `instanseg` 0.1.1 (and unchanged on `main`). They +are patched at call time rather than vendored, so removing this module is all +it takes once InstanSeg fixes them. + +1. **zarr 3.** `eval_whole_slide_image` builds its label canvas with + `zarr.DirectoryStore`, a zarr-2 name that zarr 3 renamed to + `zarr.storage.LocalStore`. InstanSeg therefore pins `zarr>=2.0.0,<3` in its + `io` extra -- a cap that collides head-on with `spatialdata` (`zarr>=3.0.0`) + and `anndata` (`zarr>=3.1`), so installing that extra breaks this package + outright. The pin's stated reason ("tiffslide + doesn't support zarr v3 yet") is stale: tiffslide 4.0 added zarr-3 support + in Bayer-Group/tiffslide#97. Only the name is missing. + +2. **`TiffSlide` is never imported.** `InstanSeg.read_slide` calls + `TiffSlide(image_str)` at `inference_class.py:236`, but every import of that + name in the module is function-local (lines 63, 93, 183), so `read_slide` + raises `NameError` on any whole-slide image -- the code path is simply + untested upstream. + +3. **Invalid GeoJSON.** The exporter writes a comma after every feature and + then closes the array, so output ends `...}},\\n]` -- which `json.load` + and QuPath both reject. `repair_geojson_trailing_comma` fixes the artefact + rather than working around it at read time. + +All three are idempotent and are exercised by `tests/test_compat.py`. +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +def patch_zarr_directory_store() -> bool: + """Alias `zarr.DirectoryStore` to `zarr.storage.LocalStore` when it is missing. + + `LocalStore` is a drop-in for the four things InstanSeg does with the + store: construct from a path, hand to `zarr.zeros(..., overwrite=True)`, + slice-assign tiles, and reopen with `zarr.open(path, mode="r")`. + + Returns + ------- + bool + True if the alias was installed (zarr 3), False if `zarr.DirectoryStore` + already exists (zarr 2) and nothing was changed. + """ + import zarr + + if hasattr(zarr, "DirectoryStore"): + return False + + zarr.DirectoryStore = zarr.storage.LocalStore # type: ignore[attr-defined] + logger.debug("Aliased zarr.DirectoryStore -> zarr.storage.LocalStore (zarr %s)", zarr.__version__) + return True + + +def patch_instanseg_tiffslide() -> bool: + """Inject `TiffSlide` into `instanseg.inference_class`'s module globals. + + `read_slide` references the name without importing it, so every + whole-slide call raises `NameError` until it is bound. + + Returns + ------- + bool + True if the name was injected, False if it was already present. + + Raises + ------ + ImportError + If tiffslide is not installed; install `spatialrefinery[segmentation]`. + """ + from instanseg import inference_class + from tiffslide import TiffSlide + + if getattr(inference_class, "TiffSlide", None) is not None: + return False + + inference_class.TiffSlide = TiffSlide + logger.debug("Injected TiffSlide into instanseg.inference_class (upstream NameError in read_slide)") + return True + + +def patch_instanseg() -> None: + """Apply every InstanSeg compatibility patch. Safe to call repeatedly.""" + patch_zarr_directory_store() + patch_instanseg_tiffslide() + + +def repair_geojson_trailing_comma(path) -> bool: + r"""Drop the trailing comma InstanSeg leaves before the closing bracket. + + `_zarr_to_json_export` streams features out with a comma after each one and + then writes `]`, so every GeoJSON it produces ends `...}},\\n]` and is + invalid JSON. `json.load` rejects it, and so does any other consumer + (QuPath included), which makes this worth fixing in the artefact rather + than working around at read time. + + Only the tail is rewritten -- these files run to tens of megabytes. + + Parameters + ---------- + path + The GeoJSON to repair, modified in place. + + Returns + ------- + bool + True if a trailing comma was removed, False if the file was already + well-formed. + """ + import re + from pathlib import Path + + path = Path(path) + window = 4096 + # Matched on bytes, never decoded: a 4 KiB window can start mid-UTF-8 + # sequence, and decoding would both raise and desynchronise the offsets + # used to seek back into the file. + with path.open("r+b") as handle: + size = handle.seek(0, 2) + start = max(0, size - window) + handle.seek(start) + tail = handle.read() + + # `, ] EOF`, allowing whitespace either side of the comma. + match = re.search(rb",(\s*\]\s*)$", tail) + if match is None: + return False + + handle.seek(start + match.start()) + handle.write(match.group(1)) + handle.truncate() + + logger.debug("Removed trailing comma from %s", path) + return True diff --git a/src/spatialrefinery/segmentation/instanseg.py b/src/spatialrefinery/segmentation/instanseg.py new file mode 100644 index 0000000..c00b079 --- /dev/null +++ b/src/spatialrefinery/segmentation/instanseg.py @@ -0,0 +1,215 @@ +"""Nucleus segmentation on H&E whole-slide images via InstanSeg. + +InstanSeg owns the whole-slide layer this needs -- tiling, cross-tile label +matching, an Otsu tissue prefilter and GeoJSON export -- and its runtime +requirements (`numpy>=1.24`, `torch>=2.0`, no upper bounds) sit inside the +environment this package already has, so it installs additively without +disturbing `numpy`, `pydantic`, `zarr` or `spatialdata`. + +See `_compat` for the three upstream defects that had to be bridged to make +that whole-slide path usable here. + +Results land in `//cells.geojson`. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +logger = logging.getLogger(__name__) + +#: InstanSeg appends this to the input stem when naming its outputs. +PREDICTION_TAG = "_instanseg_prediction" + +DEFAULT_MODEL = "brightfield_nuclei" +DEFAULT_TILE_SIZE = 512 +DEFAULT_OVERLAP = 80 +DEFAULT_DETECTION_SIZE = 20 + + +def _resolve_device(gpu_id: int | None) -> str: + """Return the torch device string, falling back to CPU when no GPU is visible.""" + import torch + + if gpu_id is None or not torch.cuda.is_available(): + if gpu_id is not None: + logger.warning("No CUDA device visible; falling back to CPU (this will be slow).") + return "cpu" + return f"cuda:{gpu_id}" + + +def _find_prediction_geojson(directory: Path) -> Path | None: + """Return the GeoJSON InstanSeg wrote into `directory`, if any. + + InstanSeg names it `.geojson`. `stem` strips only the + final suffix, so `slide.ome.tif` yields `slide.ome_instanseg_prediction.geojson` + -- hence a glob rather than a constructed name. + """ + matches = sorted(directory.glob(f"*{PREDICTION_TAG}.geojson")) + return matches[0] if matches else None + + +DEFAULT_CLAHE_GRID = 8 + + +def _apply_clahe(tile, clip_limit: float, grid: int = DEFAULT_CLAHE_GRID): + """Locally equalise a tile's lightness, leaving hue alone. + + Applied to L in LAB rather than to RGB, so the haematoxylin/eosin hue + balance the model was trained on is preserved and only local contrast + changes. + """ + import cv2 + import numpy as np + + if tile.ndim != 3 or tile.shape[-1] < 3: + return tile + rgb = np.ascontiguousarray(tile[..., :3], dtype=np.uint8) + lab = cv2.cvtColor(rgb, cv2.COLOR_RGB2LAB) + lab[..., 0] = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(grid, grid)).apply(lab[..., 0]) + return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) + + +def _enable_tile_clahe(model, clip_limit: float, grid: int = DEFAULT_CLAHE_GRID) -> None: + """Make `model` run CLAHE over every tile before inference. + + `eval_whole_slide_image` reads each tile and hands it straight to + `self._to_tensor`, so wrapping that one method is enough to reach every + tile without forking InstanSeg. Per-tile is also the right granularity: + CLAHE is adaptive by design, and the model's 80 px tile overlap plus + cross-tile label matching absorb the small discontinuities at the seams. + """ + original = model._to_tensor + + def _to_tensor_with_clahe(image): + return original(_apply_clahe(image, clip_limit, grid)) + + model._to_tensor = _to_tensor_with_clahe + + +def segment_wsi( + wsi_path: str | Path, + outdir: str | Path, + *, + pixel_size: float | None = None, + gpu_id: int | None = 0, + model_type: str = DEFAULT_MODEL, + tile_size: int = DEFAULT_TILE_SIZE, + overlap: int = DEFAULT_OVERLAP, + detection_size: int = DEFAULT_DETECTION_SIZE, + use_otsu_threshold: bool = True, + clahe_clip: float | None = None, + seed_threshold: float | None = None, + skip_existing: bool = True, +) -> Path: + """Segment nuclei in one whole-slide image and return the GeoJSON path. + + Parameters + ---------- + wsi_path + The slide to segment. Read through tiffslide, so SVS/NDPI/OME-TIFF and + other TIFF-backed formats work. + outdir + Parent directory. Results go to `//cells.geojson`; + the directory is named with the full filename (extension included) so + that `a.svs` and `a.ndpi` cannot collide. + pixel_size + Microns per pixel. Read from the slide metadata when omitted. InstanSeg + rejects a value outside [0.1, 1] micron, so pass this explicitly for + slides with missing or nonsensical resolution tags. + gpu_id + CUDA device index, or None to force CPU. Under the SLURM worker each + process sees a single GPU via `CUDA_VISIBLE_DEVICES`, so this stays 0. + use_otsu_threshold + Skip tiles outside the tissue mask, so background is not segmented. + clahe_clip + Run CLAHE over each tile at this clip limit before inference. Off by + default. Worth setting (2.0 is a reasonable start) on weakly + haematoxylin-stained slides, where InstanSeg's per-tile percentile + normalisation leaves pale nuclei below the seed threshold: on a pale + kidney H&E it recovered 22% more nuclei, and 27% together with + `seed_threshold=0.4`. + seed_threshold + Override the model's seed threshold (default 0.7). Lower detects + fainter nuclei at some risk of over-segmentation. + skip_existing + Return immediately if `cells.geojson` is already present, which makes + an interrupted batch resumable. + + Returns + ------- + Path + The written `cells.geojson`. + """ + wsi_path = Path(wsi_path) + sample_dir = Path(outdir) / wsi_path.name + cells_geojson = sample_dir / "cells.geojson" + + if skip_existing and cells_geojson.exists(): + logger.info("Skipping %s: %s already exists", wsi_path.name, cells_geojson) + return cells_geojson + + if not wsi_path.exists(): + raise FileNotFoundError(f"WSI not found: {wsi_path}") + + sample_dir.mkdir(parents=True, exist_ok=True) + + # InstanSeg writes its .zarr and .geojson next to the *input* file. The + # slides live on a shared read-only dataset mount, so run it against a + # symlink inside the output directory and let the outputs land there. + linked_wsi = sample_dir / wsi_path.name + if linked_wsi.is_symlink() or linked_wsi.exists(): + linked_wsi.unlink() + linked_wsi.symlink_to(wsi_path.resolve()) + + from instanseg import InstanSeg + + from spatialrefinery.segmentation._compat import ( + patch_instanseg, + repair_geojson_trailing_comma, + ) + + # Bridges two upstream defects that break the whole-slide path; see _compat. + patch_instanseg() + + device = _resolve_device(gpu_id) + logger.info("Segmenting %s with InstanSeg(%s) on %s", wsi_path.name, model_type, device) + + model = InstanSeg(model_type, device=device, image_reader="tiffslide", verbosity=1) + if clahe_clip is not None: + logger.info("Applying CLAHE (clip=%.1f) to each tile before inference", clahe_clip) + _enable_tile_clahe(model, clahe_clip) + + # Only forwarded when set, so the model's own defaults stay in charge. + model_kwargs = {} if seed_threshold is None else {"seed_threshold": seed_threshold} + + try: + model.eval_whole_slide_image( + image=str(linked_wsi), + pixel_size=pixel_size, + tile_size=tile_size, + overlap=overlap, + detection_size=detection_size, + use_otsu_threshold=use_otsu_threshold, + save_geojson=True, + **model_kwargs, + ) + finally: + linked_wsi.unlink(missing_ok=True) + + produced = _find_prediction_geojson(sample_dir) + if produced is None: + raise FileNotFoundError( + f"InstanSeg reported success but wrote no *{PREDICTION_TAG}.geojson in {sample_dir}. " + "GeoJSON export needs rasterio and geojson: install spatialrefinery[segmentation]." + ) + + # InstanSeg leaves a trailing comma before the closing bracket, so the file + # it just wrote is not valid JSON until this runs. + if repair_geojson_trailing_comma(produced): + logger.debug("Repaired trailing comma in %s", produced.name) + + produced.replace(cells_geojson) + logger.info("Wrote %s", cells_geojson) + return cells_geojson diff --git a/src/spatialrefinery/segmentation/to_spatialdata.py b/src/spatialrefinery/segmentation/to_spatialdata.py new file mode 100644 index 0000000..66c2588 --- /dev/null +++ b/src/spatialrefinery/segmentation/to_spatialdata.py @@ -0,0 +1,208 @@ +"""Turn a nucleus-segmentation GeoJSON plus its slide into a SpatialData zarr. + +The image element is built directly from the slide's own pyramid with +`tifffile`'s zarr interface, which keeps the read lazy -- a level-0 plane on a +typical whole slide is 33427 x 11949 x 3, about 1.1 GiB if materialised. + +The table is all-zero counts over a template's `var`; it exists so the shapes +element carries a SpatialData-valid annotation. Nucleus segmentation produces +no expression data, so no cell-type column is written. +""" + +from __future__ import annotations + +import json +import logging +import os +import zipfile +from pathlib import Path +from typing import cast + +logger = logging.getLogger(__name__) + +#: Suffixes `tifffile` can open directly. SVS and NDPI are TIFF containers, so +#: the whole family goes through the same lazy path. +_TIFF_SUFFIXES = (".tif", ".tiff", ".svs", ".ndpi", ".scn", ".bif", ".svslide") + +DEFAULT_SCALE_FACTORS = (2, 2, 2, 2) + + +def explode_multipolygons(geojson_path: str | Path, save_path: str | Path): + """Split multi-polygons into individual polygons, preserving all attributes. + + Returns the exploded frame and writes it to `save_path`. + """ + import geopandas as gpd + from shapely.geometry import Polygon, shape + + with open(geojson_path) as f: + features = json.load(f) + + geometries = [] + properties = [] + + for feature in features: + if "geometry" not in feature: + continue + geom = feature["geometry"] + props = feature.get("properties", {}) + + if geom["type"] == "Polygon": + coords = geom["coordinates"] + geometries.append(Polygon(coords[0], coords[1:] if len(coords) > 1 else None)) + properties.append(props) + elif geom["type"] == "MultiPolygon": + for poly_coords in geom["coordinates"]: + geometries.append(Polygon(poly_coords[0], poly_coords[1:] if len(poly_coords) > 1 else None)) + properties.append(props) + else: + geometries.append(shape(geom)) + properties.append(props) + + gdf = gpd.GeoDataFrame(properties, geometry=geometries) + + # The loop above already flattens, so this is close to a no-op; kept because + # a non-Polygon geometry can still arrive as a collection via `shape()`. + exploded_gdf = gdf.explode(index_parts=False).reset_index(drop=True) + exploded_gdf.to_file(save_path, driver="GeoJSON") + + return exploded_gdf + + +def wsi_image_element(image_path: str | Path, *, scale_factors=DEFAULT_SCALE_FACTORS): + """Build a lazy multiscale `Image2DModel` element from a whole-slide image. + + `tifffile.imread(aszarr=True)` exposes the file's own chunking, so `dask` + reads only the blocks a write actually touches rather than pulling the + level-0 plane into RAM. + """ + import dask.array as da + import tifffile + from spatialdata.models import Image2DModel + + image_path = Path(image_path) + if not any(image_path.name.lower().endswith(s) for s in _TIFF_SUFFIXES): + raise ValueError( + f"{image_path.name} is not a TIFF-backed slide. Convert it first with " + "`spatialrefinery.core.converter.convert_to_ometiff`." + ) + + store = tifffile.imread(str(image_path), aszarr=True, level=0) + arr = da.from_zarr(store) + + # tifffile hands back (y, x, sample) for RGB slides; SpatialData wants (c, y, x). + if arr.ndim == 3 and arr.shape[-1] in (3, 4): + arr = arr[..., :3].transpose(2, 0, 1) + elif arr.ndim == 2: + arr = arr[None, ...] + + channels = ["r", "g", "b"][: arr.shape[0]] + return Image2DModel.parse( + arr, + dims=("c", "y", "x"), + c_coords=channels, + scale_factors=list(scale_factors), + ) + + +def geojson_to_spatialdata( + geojson_path: str | Path, + zarr_path: str | Path, + image_path: str | Path, + template_adata_path: str | Path, + *, + write_zip: bool = True, +): + """Assemble and write the SpatialData zarr for one segmented slide. + + The table is all-zero counts over the template's `var`; it exists so the + shapes element carries a SpatialData-valid annotation. + """ + import anndata as ad + import geopandas as gpd + import numpy as np + import pandas as pd + import spatialdata + from spatialdata.models import ShapesModel + + from spatialrefinery.core.utils import fix_table_validation_errors + + # GDAL truncates large GeoJSON features unless this is lifted. + os.environ["OGR_GEOJSON_MAX_OBJ_SIZE"] = "0" + + geojson_path = Path(geojson_path) + zarr_path = Path(zarr_path) + image_path = Path(image_path) + + temp_geojson = geojson_path.parent / f"{geojson_path.stem}_exploded.geojson" + if temp_geojson.exists(): + logger.info("Reusing existing exploded GeoJSON: %s", temp_geojson) + else: + logger.info("Exploding multi-polygons from %s", geojson_path) + explode_multipolygons(geojson_path, save_path=temp_geojson) + + logger.info("Reading exploded GeoJSON") + gdf = gpd.read_file(temp_geojson) + if gdf.empty: + raise ValueError(f"No polygons found in {geojson_path}") + + # The GeoJSON spec declares EPSG:4326, so pyogrio tags the frame as + # geographic -- but these are pixel coordinates on a slide, not lon/lat. + # Left in place, `.centroid` warns and invites a spherical reprojection + # that would silently corrupt every centroid. + gdf = gdf.set_crs(None, allow_override=True) + + centroids = gdf.geometry.centroid + spatial_coords = np.column_stack([centroids.x, centroids.y]) + gdf.index = gdf.index.astype(str) + + logger.info("Loading template AnnData: %s", template_adata_path) + template_adata = ad.read_h5ad(template_adata_path) + + # anndata types `.var` as `DataFrame | Dataset2D`; the second arm only + # occurs for a backed store, and `read_h5ad` above loads into memory. + var = cast("pd.DataFrame", template_adata.var).copy() + + table = ad.AnnData( + X=np.zeros((len(gdf), var.shape[0]), dtype=np.float32), + obs=pd.DataFrame(index=gdf.index), + var=var, + ) + table.obsm["spatial"] = spatial_coords + + # InstanSeg tags every feature `object_type`; it carries no information + # once the polygons are in a shapes element. + shapes = gdf[[gdf.geometry.name]].copy() + + logger.info("Building SpatialData object (%d nuclei)", len(gdf)) + sdata = spatialdata.SpatialData( + images={"he_image": wsi_image_element(image_path)}, + shapes={"nucleus_boundaries": ShapesModel.parse(shapes)}, + attrs={"tissue_segmentation_image": "he_image"}, + tables={"table": table}, + ) + + sdata["table"] = fix_table_validation_errors(sdata["table"]) + sdata["table"].obs["region"] = "nucleus_boundaries" + sdata["table"].obs["instance_id"] = sdata["table"].obs.index.astype(str) + sdata.set_table_annotates_spatialelement( + "table", region="nucleus_boundaries", instance_key="instance_id", region_key="region" + ) + + logger.info("Writing SpatialData zarr to %s", zarr_path) + sdata.write(str(zarr_path)) + + if write_zip: + zip_path = Path(f"{zarr_path}.zip") + if zip_path.exists(): + logger.warning("Zip already exists, leaving it alone: %s", zip_path) + else: + logger.info("Creating uncompressed zip archive") + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_STORED) as zipf: + for root, _, files in os.walk(zarr_path): + for file in files: + file_path = Path(root) / file + zipf.write(file_path, file_path.relative_to(zarr_path.parent)) + + temp_geojson.unlink(missing_ok=True) + return sdata diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 0000000..525ac2b --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,142 @@ +"""Tests for the InstanSeg compatibility bridges. + +InstanSeg's whole-slide path carries three defects that break it in this +environment: it is written against zarr 2 (and pins `zarr<3`, which conflicts +with spatialdata), it calls `TiffSlide` without importing it, and it emits +invalid JSON. These tests pin each bridge, so a future release that changes +any of them fails here rather than halfway through a multi-minute whole-slide +run. + +`instanseg` and `tiffslide` come from the optional `segmentation` extra, which +the CI test environment does not install, so the tests that need them skip +rather than fail. The zarr and GeoJSON bridges are exercised unconditionally. +""" + +from __future__ import annotations + +import json + +import numpy as np +import pytest +import zarr + +from spatialrefinery.segmentation._compat import ( + patch_instanseg, + patch_instanseg_tiffslide, + patch_zarr_directory_store, + repair_geojson_trailing_comma, +) + +# --------------------------------------------------------------------- # +# 1. zarr 3 +# --------------------------------------------------------------------- # + + +def test_patch_is_idempotent(): + """Calling the patch twice must not raise, and must report no-op the second time.""" + patch_zarr_directory_store() + assert patch_zarr_directory_store() is False + assert hasattr(zarr, "DirectoryStore") + + +def test_directory_store_round_trip(tmp_path): + """Exercise exactly what InstanSeg does: create, slice-assign, reopen, read.""" + patch_zarr_directory_store() + path = tmp_path / "canvas.zarr" + + # 1. construct a store from a path + store = zarr.DirectoryStore(path) + + # 2. allocate a chunked int32 canvas + canvas = zarr.zeros((1, 600, 600), chunks=(1, 512, 512), dtype=np.int32, store=store, overwrite=True) + assert canvas.shape == (1, 600, 600) + + # 3. slice-assign a tile, the way tiles are stitched into the canvas + tile = np.arange(512 * 512, dtype=np.int32).reshape(512, 512) + canvas[0, 0:512, 0:512] = tile + + # 4. reopen read-only, as the GeoJSON export does + reopened = zarr.open(str(path), mode="r") + assert reopened.shape == (1, 600, 600) + np.testing.assert_array_equal(np.asarray(reopened[0, 0:512, 0:512]), tile) + + +def test_untouched_region_stays_zero(tmp_path): + """Labels must not leak outside the written window.""" + patch_zarr_directory_store() + store = zarr.DirectoryStore(tmp_path / "c.zarr") + canvas = zarr.zeros((1, 64, 64), chunks=(1, 32, 32), dtype=np.int32, store=store, overwrite=True) + canvas[0, 0:16, 0:16] = 7 + assert int(np.asarray(canvas[0, 32:64, 32:64]).sum()) == 0 + + +# --------------------------------------------------------------------- # +# 2. missing TiffSlide import +# --------------------------------------------------------------------- # + + +def test_tiffslide_is_injected_into_instanseg(): + """`InstanSeg.read_slide` calls `TiffSlide(...)` without importing it. + + Every import of the name in `inference_class` is function-local, so the + module global is unbound and any whole-slide call dies with `NameError` + before reading a single tile. Guard the injection that fixes it. + """ + inference_class = pytest.importorskip("instanseg.inference_class") + TiffSlide = pytest.importorskip("tiffslide").TiffSlide + + patch_instanseg_tiffslide() + assert inference_class.TiffSlide is TiffSlide + + # Idempotent: a second call reports "already bound" rather than rebinding. + assert patch_instanseg_tiffslide() is False + + +def test_patch_instanseg_applies_both_bridges(): + """The convenience entry point must leave both import-time defects patched.""" + inference_class = pytest.importorskip("instanseg.inference_class") + + patch_instanseg() + assert hasattr(zarr, "DirectoryStore") + assert getattr(inference_class, "TiffSlide", None) is not None + + +# --------------------------------------------------------------------- # +# 3. invalid GeoJSON +# --------------------------------------------------------------------- # + + +def test_repair_geojson_trailing_comma(tmp_path): + """InstanSeg closes its feature array as `...}},\\n]`, which is invalid JSON.""" + p = tmp_path / "cells.geojson" + p.write_text('[\n{"a": 1},\n{"a": 2},\n]') + + with pytest.raises(json.JSONDecodeError): + json.loads(p.read_text()) + + assert repair_geojson_trailing_comma(p) is True + assert json.loads(p.read_text()) == [{"a": 1}, {"a": 2}] + + +def test_repair_is_noop_on_valid_geojson(tmp_path): + """A well-formed file must be left byte-for-byte alone.""" + p = tmp_path / "cells.geojson" + original = '[\n{"a": 1}\n]' + p.write_text(original) + + assert repair_geojson_trailing_comma(p) is False + assert p.read_text() == original + + +def test_repair_survives_multibyte_tail(tmp_path): + """The 4 KiB window can start mid-UTF-8, so the scan must work on bytes. + + Decoding the window would raise on a split sequence, and character offsets + would not line up with the byte offsets used to seek back into the file. + """ + p = tmp_path / "cells.geojson" + padding = "µ" * 3000 # two bytes each, so the window boundary splits one + p.write_text(f'[\n{{"name": "{padding}"}},\n]', encoding="utf-8") + + assert repair_geojson_trailing_comma(p) is True + assert json.loads(p.read_text(encoding="utf-8")) == [{"name": padding}] diff --git a/tests/test_segmentation.py b/tests/test_segmentation.py new file mode 100644 index 0000000..d227b81 --- /dev/null +++ b/tests/test_segmentation.py @@ -0,0 +1,340 @@ +"""Tests for `spatialrefinery.segmentation`. + +The InstanSeg model itself is not exercised here -- it needs a GPU and a +downloaded checkpoint, and is covered by the smoke test in +`slurm/SEGMENTATION_PLAN.md`. What these tests pin is the glue around it: +where the GeoJSON is looked for, how its properties are handled, and that the +image element is built lazily from the slide's own pyramid. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import anndata as ad +import numpy as np +import pandas as pd +import pytest +import tifffile as tf + +from spatialrefinery.segmentation.instanseg import ( + PREDICTION_TAG, + _find_prediction_geojson, + segment_wsi, +) +from spatialrefinery.segmentation.to_spatialdata import ( + explode_multipolygons, + geojson_to_spatialdata, + wsi_image_element, +) + + +def make_slide(path: Path, height: int = 256, width: int = 192, seed: int = 0) -> np.ndarray: + """Write a random RGB tiled TIFF and return its pixels.""" + rng = np.random.default_rng(seed) + image = rng.integers(0, 255, (height, width, 3), dtype=np.uint8) + with tf.TiffWriter(path) as writer: + writer.write(image, tile=(64, 64), photometric="rgb") + return image + + +def square(x0: float, y0: float, size: float = 4.0) -> list: + """Return a closed square ring as GeoJSON coordinates.""" + return [[[x0, y0], [x0 + size, y0], [x0 + size, y0 + size], [x0, y0 + size], [x0, y0]]] + + +def write_geojson(path: Path, n: int = 3) -> None: + """Write a GeoJSON with InstanSeg's real property schema. + + Taken verbatim from a whole-slide run: a bare list (not a FeatureCollection), + and every feature carries `object_type`, `measurements`, and a *constant* + `classification` of "Detection" -- which is a marker, not a cell type, and + must not reach the table. + """ + features = [ + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": square(10.0 * i, 10.0 * i)}, + "properties": { + "object_type": "detection", + "measurements": [{"name": "Label", "value": i + 1}], + "classification": "Detection", + }, + } + for i in range(n) + ] + path.write_text(json.dumps(features)) + + +def make_template(path: Path, n_vars: int = 5) -> None: + """Write a minimal template AnnData carrying only `var`.""" + var = pd.DataFrame(index=[f"gene_{i}" for i in range(n_vars)]) + ad.AnnData(X=np.zeros((1, n_vars), dtype=np.float32), var=var).write_h5ad(path) + + +# --------------------------------------------------------------------- # +# Output discovery +# --------------------------------------------------------------------- # + + +def test_find_prediction_geojson_handles_double_suffix(tmp_path): + """`slide.ome.tif` has stem `slide.ome`, so the name cannot be reconstructed.""" + expected = tmp_path / f"slide.ome{PREDICTION_TAG}.geojson" + expected.touch() + assert _find_prediction_geojson(tmp_path) == expected + + +def test_find_prediction_geojson_returns_none_when_absent(tmp_path): + """A missing GeoJSON is reported as None, not an IndexError.""" + (tmp_path / "unrelated.geojson").touch() + assert _find_prediction_geojson(tmp_path) is None + + +# --------------------------------------------------------------------- # +# segment_wsi guard rails (no model involved) +# --------------------------------------------------------------------- # + + +def test_segment_wsi_skips_existing_without_loading_model(tmp_path, monkeypatch): + """`--skip-existing` must return before importing InstanSeg, so resume is cheap.""" + wsi = tmp_path / "slide.tif" + make_slide(wsi) + sample_dir = tmp_path / "out" / "slide.tif" + sample_dir.mkdir(parents=True) + cells = sample_dir / "cells.geojson" + cells.write_text("[]") + + # Any attempt to build the model would blow up here. + monkeypatch.setattr( + "spatialrefinery.segmentation._compat.patch_instanseg", + lambda: pytest.fail("model path entered despite existing cells.geojson"), + ) + + assert segment_wsi(wsi, tmp_path / "out") == cells + + +def test_segment_wsi_missing_slide_raises(tmp_path): + """A missing slide fails loudly rather than producing an empty result.""" + with pytest.raises(FileNotFoundError): + segment_wsi(tmp_path / "nope.tif", tmp_path / "out") + + +# --------------------------------------------------------------------- # +# GeoJSON handling +# --------------------------------------------------------------------- # + + +def test_explode_multipolygons_splits_parts(tmp_path): + """A MultiPolygon becomes one row per part, each keeping the parent's properties.""" + src = tmp_path / "cells.geojson" + src.write_text( + json.dumps( + [ + { + "type": "Feature", + "geometry": {"type": "MultiPolygon", "coordinates": [square(0, 0), square(20, 20)]}, + "properties": {"object_type": "annotation"}, + }, + { + "type": "Feature", + "geometry": {"type": "Polygon", "coordinates": square(40, 40)}, + "properties": {"object_type": "annotation"}, + }, + ] + ) + ) + + gdf = explode_multipolygons(src, tmp_path / "exploded.geojson") + + assert len(gdf) == 3 + assert gdf.geometry.geom_type.unique().tolist() == ["Polygon"] + assert (tmp_path / "exploded.geojson").exists() + + +def test_instanseg_classification_is_a_constant_marker(tmp_path): + """InstanSeg's `classification` is always "Detection", never a cell type. + + Carrying that constant into `table.obs` would add a column that looks like + a cell-type label but distinguishes nothing, so it must stay out. + """ + src = tmp_path / "cells.geojson" + write_geojson(src, n=3) + gdf = explode_multipolygons(src, tmp_path / "exploded.geojson") + assert set(gdf["classification"]) == {"Detection"} + + +# --------------------------------------------------------------------- # +# Image element +# --------------------------------------------------------------------- # + + +def test_wsi_image_element_is_channel_first_multiscale(tmp_path): + """The element must be (c, y, x) with an RGB channel axis, built from the file lazily.""" + wsi = tmp_path / "slide.tif" + image = make_slide(wsi, height=256, width=192) + + element = wsi_image_element(wsi, scale_factors=(2,)) + + # A multiscale element is a DataTree; its full-resolution level is "scale0". + full = element["scale0"].image if hasattr(element, "__getitem__") else element + assert full.dims == ("c", "y", "x") + assert full.shape == (3, 256, 192) + np.testing.assert_array_equal(np.asarray(full.data[:, :8, :8]), image[:8, :8, :].transpose(2, 0, 1)) + + +def test_wsi_image_element_rejects_non_tiff(tmp_path): + """A format tifffile cannot open should point at the OME-TIFF converter.""" + bogus = tmp_path / "slide.czi" + bogus.write_bytes(b"not a tiff") + with pytest.raises(ValueError, match="convert_to_ometiff"): + wsi_image_element(bogus) + + +# --------------------------------------------------------------------- # +# End-to-end conversion +# --------------------------------------------------------------------- # + + +def test_geojson_to_spatialdata_writes_expected_elements(tmp_path): + """The written zarr must carry the image, the shapes, and a table annotating them.""" + import spatialdata + + wsi = tmp_path / "slide.tif" + make_slide(wsi) + geojson = tmp_path / "cells.geojson" + write_geojson(geojson, n=4) + template = tmp_path / "template.h5ad" + make_template(template, n_vars=5) + + zarr_path = tmp_path / "slide.tif.zarr" + geojson_to_spatialdata( + geojson_path=geojson, + zarr_path=zarr_path, + image_path=wsi, + template_adata_path=template, + write_zip=False, + ) + + assert zarr_path.exists() + sdata = spatialdata.read_zarr(zarr_path) + assert "he_image" in sdata.images + assert "nucleus_boundaries" in sdata.shapes + assert len(sdata["nucleus_boundaries"]) == 4 + + table = sdata["table"] + assert table.n_obs == 4 + assert table.var_names.tolist() == [f"gene_{i}" for i in range(5)] + assert "classification" not in table.obs.columns + assert set(table.obs["region"]) == {"nucleus_boundaries"} + assert table.obsm["spatial"].shape == (4, 2) + + +def test_geojson_to_spatialdata_rejects_empty_geojson(tmp_path): + """An empty segmentation should fail loudly, not write a degenerate zarr.""" + wsi = tmp_path / "slide.tif" + make_slide(wsi) + geojson = tmp_path / "cells.geojson" + geojson.write_text("[]") + template = tmp_path / "template.h5ad" + make_template(template) + + with pytest.raises(ValueError, match="No polygons"): + geojson_to_spatialdata( + geojson_path=geojson, + zarr_path=tmp_path / "out.zarr", + image_path=wsi, + template_adata_path=template, + write_zip=False, + ) + + +def test_centroids_are_planar_pixel_coordinates(tmp_path): + """Centroids must be plain pixel means, not reprojected through a geographic CRS. + + `gpd.read_file` tags GeoJSON as EPSG:4326 by default; if that CRS survives, + `.centroid` is computed against a spherical datum and every coordinate + drifts. The squares here are axis-aligned, so the answer is exact. + """ + wsi = tmp_path / "slide.tif" + make_slide(wsi) + geojson = tmp_path / "cells.geojson" + write_geojson(geojson, n=3) # squares of side 4 at (0,0), (10,10), (20,20) + template = tmp_path / "template.h5ad" + make_template(template) + + import spatialdata + + zarr_path = tmp_path / "out.zarr" + geojson_to_spatialdata( + geojson_path=geojson, + zarr_path=zarr_path, + image_path=wsi, + template_adata_path=template, + write_zip=False, + ) + + spatial = spatialdata.read_zarr(zarr_path)["table"].obsm["spatial"] + expected = np.array([[2.0, 2.0], [12.0, 12.0], [22.0, 22.0]]) + np.testing.assert_allclose(np.sort(spatial, axis=0), expected, atol=1e-9) + + +# --------------------------------------------------------------------- # +# Sensitivity controls +# --------------------------------------------------------------------- # + + +def test_clahe_widens_dynamic_range_without_shifting_shape_or_dtype(): + """CLAHE must return a drop-in tile whose lightness is more spread out. + + Spread is the property that matters and the one that holds regardless of + texture: this cohort's slides are pale (median lightness 205/255), which + compresses nuclei against cytoplasm until local equalisation pulls them + apart. Measured on a real crop from the kidney slide, std goes 21.8 -> 34.0. + """ + from spatialrefinery.segmentation.instanseg import _apply_clahe + + rng = np.random.default_rng(0) + tile = np.full((128, 128, 3), 205, dtype=np.uint8) + tile[40:60, 40:60] = 180 # a faint nucleus + tile = np.clip(tile + rng.integers(-3, 4, tile.shape), 0, 255).astype(np.uint8) + + out = _apply_clahe(tile, clip_limit=2.0) + + assert out.shape == tile.shape + assert out.dtype == np.uint8 + assert out.mean(axis=2).std() > tile.mean(axis=2).std() + + +def test_apply_clahe_passes_through_non_rgb(): + """A tile without three channels is returned untouched rather than crashing.""" + from spatialrefinery.segmentation.instanseg import _apply_clahe + + grey = np.full((16, 16), 128, dtype=np.uint8) + assert _apply_clahe(grey, clip_limit=2.0) is grey + + +def test_enable_tile_clahe_wraps_every_tile(): + """The wrapper must intercept `_to_tensor`, which is the WSI loop's only tile hook.""" + from spatialrefinery.segmentation.instanseg import _enable_tile_clahe + + seen = [] + + class FakeModel: + def _to_tensor(self, image): + seen.append(image) + return image + + model = FakeModel() + original = model._to_tensor + _enable_tile_clahe(model, clip_limit=2.0) + assert model._to_tensor is not original + + tile = np.full((64, 64, 3), 200, dtype=np.uint8) + tile[20:30, 20:30] = 170 + model._to_tensor(tile) + + assert len(seen) == 1 + # The model saw the enhanced tile, not the raw one. + assert not np.array_equal(seen[0], tile) + assert seen[0].shape == tile.shape diff --git a/uv.lock b/uv.lock index 6812fe9..9dbb10b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -28,6 +28,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] +[[package]] +name = "affine" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/e9/4a4480601992a529c5d0f406605f70ca59aeaef4a6f5ba8905cfde217d0b/affine-3.0.1.tar.gz", hash = "sha256:e1b3c38c5d4d3ef5024a182a6d1bf1e0c51ab221825781c741aeb4d0c079a7e2", size = 20981, upload-time = "2026-08-28T18:38:14.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/87/e62f55c956b583380e7d2a71705dfd431ee32dd1689d50491ba0c610fc11/affine-3.0.1-py3-none-any.whl", hash = "sha256:cda3b303325e7bf2bf34817e68753a0d1c4cacbdd451fe67c4878dc2ecbaa540", size = 10887, upload-time = "2026-08-28T18:38:12.837Z" }, +] + [[package]] name = "aicspylibczi" version = "3.3.1" @@ -853,38 +865,107 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "python_full_version < '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/b1/ef21259ec74fe0b265ed201379de1d0ef7c14178313ee03705952f1b7093/cuda_pathfinder-1.8.0-py3-none-any.whl", hash = "sha256:c44e574dc997fae2814721d1ae97d0fd6db76db82decbe9b753bf75de53f515e", size = 62539, upload-time = "2026-08-27T21:33:03.229Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1038,6 +1119,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, ] +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + [[package]] name = "execnet" version = "2.1.2" @@ -1086,6 +1176,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/e1/62cc96341f01bdff2ba967441939178fcd1900d11ce7e6554d9954a5d7ec/fastjsonschema-2.22.1-py3-none-any.whl", hash = "sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999", size = 26239, upload-time = "2026-07-27T13:31:03.251Z" }, ] +[[package]] +name = "fastremap" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/3c/54d2378454d92ca1a894939d7db8b1a828ba45737e05532750520dd03e9f/fastremap-1.20.0.tar.gz", hash = "sha256:a64ffb4a99fd06c6f54f0ffbf48c73532fa238c429a4278b45767a55957468a0", size = 55917, upload-time = "2026-06-11T15:36:19.713Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/4e/751c65c98ddc2bb70e73e6c6fe3471e2f2091dd76001b5c73f827b9803b0/fastremap-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5638c0096bd5648343e8387b7fe9db45bf3f605ae95ccb9b65ea430a6db4d52e", size = 739587, upload-time = "2026-06-11T15:35:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/34ad4f73183defb188accb1947e4bee06b8b86f5644419e31faf5fa98c9c/fastremap-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec8707820d06f3fd524c4a05b51174eaf9650827172cdf77efa1ed18293412e4", size = 609890, upload-time = "2026-06-11T15:35:22.921Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4b/adfb43a0170ba3b542021faff83d0a6d1426603cc4389e248b140fabda23/fastremap-1.20.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d834527d15139611624ac11d307b44dd5fc9e37f18fb2592ac5612d4852a54c", size = 7327271, upload-time = "2026-06-11T15:35:25.632Z" }, + { url = "https://files.pythonhosted.org/packages/33/e4/8c526e16c80c52fbd982e2a048bea10dcd256dc7422b29bb8d6cf76bf3b7/fastremap-1.20.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2daa147a1c02a74a4e2344a5631482e51e194bcf3bcc50410639511eed260f74", size = 7550963, upload-time = "2026-06-11T15:35:33.441Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/1541bc16846088cdcd5d55b1b61f6c1d3eb8f3e15c4b435dce3ca8eaa0ee/fastremap-1.20.0-cp312-cp312-win32.whl", hash = "sha256:23ca9cb8ff3ff182e391bcaea6af497fee2f2f861a6dff66d97d578b18124caf", size = 450602, upload-time = "2026-06-11T15:35:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/27/47/b5a91118129433c19e6f686e8a6934cb1758b75554537161cd2efd697ed1/fastremap-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:823f51f444c54c6e32fd0e61b0b9bc620e39de97cc16db0e513e15ebe902311a", size = 627051, upload-time = "2026-06-11T15:35:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/fb69ac50568ca79dda1acea70df98c733fefb1f95fb12667090b34ce0d63/fastremap-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:50a265e258304a09d7afecf3f806b50a7db7a955d0bedba8feb33c11c378035c", size = 738322, upload-time = "2026-06-11T15:35:38.294Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d0/5b9ed5c553d77e301a9ae69b5aa349aa9cc06e06b2bfd2980260d997c928/fastremap-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2cb35c664850c3a3d8f60512268e68b6f717c7a1baa518a20b1050bce42f3c7f", size = 609416, upload-time = "2026-06-11T15:35:39.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/8c/ccf8f996efdb83391ac0236806f0fe8e21a2cd81bc7dfe58d4be4a514163/fastremap-1.20.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fcdbe8b589b86e61c2025b4c8e8f8182726f3d1b32e70d265156fa969b91196", size = 7285779, upload-time = "2026-06-11T15:35:41.689Z" }, + { url = "https://files.pythonhosted.org/packages/94/ec/2c8ed4fbec5d6a9a48ad17278686a9a2a6c2ad4279420a0724d75f0dafc4/fastremap-1.20.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd645a162abeaa0b0d2ae9c497314c2819a339b8c617e1718eea72e0e067cc4b", size = 7430315, upload-time = "2026-06-11T15:35:44.203Z" }, + { url = "https://files.pythonhosted.org/packages/76/a7/d08fa28ef70e7106bbc0e0581f3c4d57b7891fc9570570c0420206f8b27d/fastremap-1.20.0-cp313-cp313-win32.whl", hash = "sha256:6603d64c3547b84913acdb49b360dd9c6864d5defd0564ef839f4202849274e4", size = 454626, upload-time = "2026-06-11T15:35:46.495Z" }, + { url = "https://files.pythonhosted.org/packages/6a/33/0b130e2f6efd1b8d8862a14c20b2eadba18fe58d561dfe8ba40841ba2cca/fastremap-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:a653f54c1f5b52eca443eab33ca9700585c9d1298e309d5cc5ff8ce6d05111b0", size = 626936, upload-time = "2026-06-11T15:35:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/00275ba5776982a7992fd828f284ba29af2674ed8f66d547502b36cdc849/fastremap-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f06a89985c1b5619c2fd1ffe0824da83689a0f1562c58b1667e18a8ec241faf3", size = 735745, upload-time = "2026-06-11T15:35:47.947Z" }, + { url = "https://files.pythonhosted.org/packages/41/f3/9fbaaa754dbe42a4557120af2da782c50e9fbbd0b429a86706ab8d75e0f5/fastremap-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410e6a014b7f6e7c10aed91edc870773ce07023a6937129b3b96e69c99ee484f", size = 618685, upload-time = "2026-06-11T15:35:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ac/f2087ade77fb852258b03b9d4ba89d3b50c1e29792d92d0826a4025c83ba/fastremap-1.20.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:114ab797b7621f50e4fbc4ef3d5f9cf0604e5199a2f2bf9b185bd1318e54765b", size = 7241954, upload-time = "2026-06-11T15:35:51.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/363d99040c496fae137de7be1036a16489d470f94d96f94eb3ef8f52eedd/fastremap-1.20.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a5e2abf28eb75b9b852abfcc1ceab169473d36ca2a5526e0637567d411e6e8", size = 7326676, upload-time = "2026-06-11T15:35:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/90/29/8d246dd3c0c5c5a408eddf12cca5aa5832d26344ccd22f4bcb7952270241/fastremap-1.20.0-cp314-cp314-win32.whl", hash = "sha256:9fb5bb87167a36e519b23ac28a40c2296f70082dba05db0022ed051c685da887", size = 460685, upload-time = "2026-06-11T15:35:56.489Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0d/bb044a005bfeab74e3c488593ae31237f9912d8f2a82d085eaec054ec83f/fastremap-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:8a1431fb5f60792f6072d4bf2216c248512c6baab8a6f1385b76b655c420896a", size = 642651, upload-time = "2026-06-11T15:35:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/8782a8e39cba8fd9f9ea7f0140f909af038219c3203cf0ed72906a2c9047/fastremap-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d19af90c62356685d77b3bb852d5701dff001224dcf847c7a2fec904eb86ec0", size = 796849, upload-time = "2026-06-11T15:35:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/ce/33/02e447089ecdb3827513b9deb5e6298a6cd79c5a954369c2a9b2f287af3f/fastremap-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c31a3e6aa9a88375c2b2b58f277b83ada5eb8d8122b3c0bc74b3cb719260a08a", size = 680463, upload-time = "2026-06-11T15:36:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/6b/af/1190afa75ee6a7a22d710a4786f3cdc8d861695282f5c839d5fd6e9ff273/fastremap-1.20.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78088c09176a5c14364b4c2fcbf5970375516adc94638ce02f2c7af257cb7dc2", size = 7351102, upload-time = "2026-06-11T15:36:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ba/3b0f124b1fa2960e47d07004ba187cc4499a42d7d85029970c5a6cc43e43/fastremap-1.20.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb517805c2b82b28cc7231fa5469327d6b3327a7ae8d036ab87f019e5d7096d0", size = 7203188, upload-time = "2026-06-11T15:36:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/0fe3bb3ecc078dc4367c1651e23bd28ced778212be564bca972c4872b00d/fastremap-1.20.0-cp314-cp314t-win32.whl", hash = "sha256:b7701f3e5c870f84906afc9ca1daed2f046a98c4fadc66e994687439e26ade0a", size = 576045, upload-time = "2026-06-11T15:36:08.746Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c8/e6707c4f9161063cf31bdba1284361d81716642dd18342d2349dc42cc986/fastremap-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e45c389f99b77c764281350d3158e5ad3eb864d786733dedb63cc699a89ad363", size = 804680, upload-time = "2026-06-11T15:36:07.431Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z" }, +] + [[package]] name = "flexcache" version = "0.3" @@ -1266,6 +1400,15 @@ s3 = [ { name = "s3fs" }, ] +[[package]] +name = "geojson" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/a9/bd61eee2c7904947094b74866b569f3fd5a8d6ac907ecdfecef74b19d459/geojson-3.3.0.tar.gz", hash = "sha256:92e83b9cb378a450b42f1207bb9b2a031f9fc89185f335153c44369b8b8b71fd", size = 25141, upload-time = "2026-05-28T21:48:08.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/5e/fdd72167b57158d743353f71d453200719744d1e75f18b1c8230508db370/geojson-3.3.0-py3-none-any.whl", hash = "sha256:a2d885187eeaa8b357600b3fcc9d963cb4300d1694196636dbd7eddc82fd0825", size = 15181, upload-time = "2026-05-28T21:48:06.648Z" }, +] + [[package]] name = "geopandas" version = "1.1.4" @@ -1315,9 +1458,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f9/03e26be3487c5238e81f2b84714959a86ea8515a869828cf41f4fc54b34e/greenlet-3.5.4-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf", size = 629603, upload-time = "2026-07-22T12:43:43.456Z" }, { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, - { url = "https://files.pythonhosted.org/packages/57/6b/7c55ca72ef80d57c16c4a55210f82582622462dc4485799a30f4ec6f3372/greenlet-3.5.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f", size = 432554, upload-time = "2026-07-22T12:39:51.379Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, @@ -1325,9 +1466,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, - { url = "https://files.pythonhosted.org/packages/1b/80/fb4d4788bbc8e54761f1fc88533af9523a6e86299fa113d6e8a8503ed9fc/greenlet-3.5.4-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c", size = 632845, upload-time = "2026-07-22T12:43:45.19Z" }, { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, - { url = "https://files.pythonhosted.org/packages/42/e3/6086fa578ebb72772722cdc4bcd628459814b42e0c2db1e3cbd6552b3271/greenlet-3.5.4-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861", size = 435053, upload-time = "2026-07-22T12:39:52.715Z" }, { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, @@ -1335,9 +1474,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/9c/bf/250c2921c7b585dde12f5239e313ca2dcbc464d161ecca36e4e6ef21762d/greenlet-3.5.4-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c", size = 677968, upload-time = "2026-07-22T12:43:46.788Z" }, { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/18/40/10bfcf6513558d82f7b95dd728001c63bd388259fe27d3e30ae01f103430/greenlet-3.5.4-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c", size = 480643, upload-time = "2026-07-22T12:39:54.149Z" }, { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, @@ -1345,18 +1482,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, - { url = "https://files.pythonhosted.org/packages/ae/db/24a10af12bf8e639cec46c38b9ce1a282543ba42ff4fb0b31a970f1ab603/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8", size = 681690, upload-time = "2026-07-22T12:43:48.109Z" }, { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, - { url = "https://files.pythonhosted.org/packages/f4/60/44a2eca7b9fd71ae0fae7ff184da1cd3169d176652b97aa1cffcbb0ef961/greenlet-3.5.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd", size = 510263, upload-time = "2026-07-22T12:39:55.678Z" }, { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, - { url = "https://files.pythonhosted.org/packages/51/a7/dafc7415d430b0a43a16396eb49ecb3b62fd720877fb259cc4dcfaf5f31e/greenlet-3.5.4-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3", size = 681428, upload-time = "2026-07-22T12:43:49.623Z" }, { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d9/6298f3432de301d4718766cf934bd73c418c73f81fbb77247319364b0d96/greenlet-3.5.4-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb", size = 487446, upload-time = "2026-07-22T12:39:57.044Z" }, { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, @@ -1364,9 +1497,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/88/15/0b167aeea95285b0e654ddce651922f666c089363c2ec528ca8b9a9ba74f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d", size = 685995, upload-time = "2026-07-22T12:43:50.993Z" }, { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, - { url = "https://files.pythonhosted.org/packages/de/90/c023ec337f32ff505be7db759c80d98f0532bb94d0c6fa13645efe9bee2e/greenlet-3.5.4-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05", size = 516928, upload-time = "2026-07-22T12:39:58.359Z" }, { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, @@ -1457,12 +1588,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/e8/042eeac7c78565f1b4c14ba3dc6a96aad9b958e008a1e291a1cbf72123e9/imagecodecs-2026.6.26-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c48767bbc6ed0b40df3607cafdf59dac6f4467b91c26e835744d7ee4085cfdf7", size = 13164944, upload-time = "2026-06-28T18:26:29.33Z" }, { url = "https://files.pythonhosted.org/packages/1d/dd/0e0c86df9a55a0a90ced8cf5214abb459015cda5f2e713e0495ccd9bf23f/imagecodecs-2026.6.26-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9c130cf207efcb35acf74d6040317d217294640e478aa733fb51be758bcaddbc", size = 32436234, upload-time = "2026-06-28T18:26:33.35Z" }, { url = "https://files.pythonhosted.org/packages/b9/9a/2d63eb2ffa46863bf0ea05884ce1ea2a73a5153b428937b8cbe4faa81536/imagecodecs-2026.6.26-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:014f5f809418eb3dfe29badd4af29ddeeaba8b5eabda4bd502e2292727e4094f", size = 33446845, upload-time = "2026-06-28T18:26:38.238Z" }, - { url = "https://files.pythonhosted.org/packages/14/68/ece973cd5b1d8305b95ad827b3c6033aed166c5ce93780b6092dbfd5deec/imagecodecs-2026.6.26-cp314-cp314t-win32.whl", hash = "sha256:daa276e5645750404a28a911dcbfb94c7d32b2d8903b5f05167474ea02bd5b40", size = 17330378, upload-time = "2026-06-28T18:26:42.172Z" }, { url = "https://files.pythonhosted.org/packages/ad/e6/ab2e9ee84e44768ceb73bb9301988ea4ab9022f02ef9d824d6ebb641a0fb/imagecodecs-2026.6.26-cp314-cp314t-win_amd64.whl", hash = "sha256:0718d75672768871aa6adac2129c8f74c167fb4a3da25f660f3afc94b420304f", size = 22122461, upload-time = "2026-06-28T18:26:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/58/53/7943321eb84c6d39e71ca47e689557a976c6ebafec48b759b3be0bd7c601/imagecodecs-2026.6.26-cp314-cp314t-win_arm64.whl", hash = "sha256:69b90b7b729d33cd9417d3d572d3092d2d49f7974638ee00e2cdc3ed1fba11d6", size = 17657147, upload-time = "2026-06-28T18:26:48.924Z" }, - { url = "https://files.pythonhosted.org/packages/f8/de/130820754c21c00dfdc6852661de2bb30161b5a439a8643a446dae97a02f/imagecodecs-2026.6.26-cp315-cp315t-win32.whl", hash = "sha256:a10c347225f5fb1b9d1a36cfadccad9b173609c30e0dd3f68706675d6f4f9b58", size = 17330338, upload-time = "2026-06-28T18:26:52.04Z" }, - { url = "https://files.pythonhosted.org/packages/a7/df/b4f6f2a9e9102ca674673cb1b432bbebfe2d99acb6364390768024d56ae6/imagecodecs-2026.6.26-cp315-cp315t-win_amd64.whl", hash = "sha256:85ab027a8bc900f13b1cdaac05bb602ea58749731ae18743a7956642a7da7a2c", size = 22116857, upload-time = "2026-06-28T18:26:55.65Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c4/d87b33e8df5c6ec19f5cd1d5ab39195b2077ac21536ff5146c20d3fb8694/imagecodecs-2026.6.26-cp315-cp315t-win_arm64.whl", hash = "sha256:41e18fcd2124c083b00f988eeb9c5cf89274e5c2f473ee03d54761b71048085c", size = 17651071, upload-time = "2026-06-28T18:26:59.305Z" }, ] [[package]] @@ -1508,6 +1634,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "instanseg-torch" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "fastremap" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "requests" }, + { name = "scikit-image" }, + { name = "torch" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/37/6cbb4a91a4b4b95fb4636aacc077c1ca28162f51674494d7001bf9a470e5/instanseg_torch-0.1.1.tar.gz", hash = "sha256:eb1127acf4f6b2bb91a5940fe83c370807a53d17cafc2f49bfbba19a7c38026d", size = 111576, upload-time = "2025-12-16T01:32:33.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/c4/74c6ea8db68f5603f788b4e938cb5d0213826ea7faf544b1e966e9f74913/instanseg_torch-0.1.1-py3-none-any.whl", hash = "sha256:1a26c87925ba5c2d40efa2cbc31db93381256848df3c359c6f960db4dda329b4", size = 126090, upload-time = "2025-12-16T01:32:31.973Z" }, +] + [[package]] name = "ipykernel" version = "7.3.0" @@ -2255,6 +2400,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "msgpack" version = "1.2.1" @@ -2746,6 +2900,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, ] +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + [[package]] name = "ome-types" version = "0.6.3" @@ -3819,6 +4125,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/43/d7e2b9ad768c07b5473bea3ac7db9ca4d995c09399cbea3d4df1c0bd4955/rangehttpserver-1.4.0-py2.py3-none-any.whl", hash = "sha256:2a0c6926e4341de4cc19ec861292b005e4194ff497b1eefdeccb2992a5045452", size = 7773, upload-time = "2024-08-27T18:08:41.861Z" }, ] +[[package]] +name = "rasterio" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "affine" }, + { name = "attrs" }, + { name = "certifi" }, + { name = "click" }, + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/1a/ee73b447f1623a6bb6490af08d4bbed3fb6e38b0adc54553a0d244d4103a/rasterio-1.5.1.tar.gz", hash = "sha256:c1b6ae15f4ccad704f1fe8417da5c2250145c7bcdb91acb53833bf5aefdd9e48", size = 457868, upload-time = "2026-08-08T02:45:15.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/88/95d2d41889f86fc7f532caf672c789db1ecf48fcf56a62c6a26450025c65/rasterio-1.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c209e670abf0f30695a784c4f0170366462b9a3eaea3e49122ab2ce81799460b", size = 23239114, upload-time = "2026-08-08T02:35:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7c/d38dc9ba3caa6feb0202178327ccc6e0010973aae9211e46fbc59c2a7970/rasterio-1.5.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:833511f045ca49dafbf25da4762a0b19dcf5d046ded8b318c094363e2d65f474", size = 24957692, upload-time = "2026-08-08T02:34:23.295Z" }, + { url = "https://files.pythonhosted.org/packages/34/52/b85411d37e87ce5561693918a944ecbe6dc3af1d4a375c9d77f2ec1c283e/rasterio-1.5.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:821d49afd18498fb0918b72961b1b667193c64edf8cc484081c522d10b33980d", size = 36943874, upload-time = "2026-08-08T02:36:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/79/6c/badf2c4d54482a1192d0057994eb48a6ff25ba6a84011d8fa2d9c230bf73/rasterio-1.5.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a270366c4f44f7bb4367d7068b6de08c56b2b04049e3dfcd06d451141fee3a7f", size = 38396579, upload-time = "2026-08-08T02:32:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/c12e0a536efc60f7730e96855c5cae20bd3af850debf08a99ec50d37a46a/rasterio-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:6fbafe970d44ec06179c7884cdb160a90311e3d19da25bff970fab06123f0201", size = 30621456, upload-time = "2026-08-08T02:37:46.26Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/cc447f6d8dfad6f86f8bb9bdb0bf0083ef44ec848b8bf60269b56eea87c1/rasterio-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:e31b65231a631b0b59471aeae4423167d42f88da0c5ee5a519dc07deb7a4666b", size = 28859019, upload-time = "2026-08-08T02:38:57.974Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f0/d3c613d845b35a3800b812f0fe0351675f6310972bbd6349f844499ebd6a/rasterio-1.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:937944c445d4ea4d6969e9dbfcf305143c05fc4df2f8fadfa360cb2d0f65d7b0", size = 23227832, upload-time = "2026-08-08T02:35:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/54c1976340df39ea6bd77ff4b41eec932ff260b635a8512ceae0945595c4/rasterio-1.5.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:e863b614cadad435a4394792b6cdc84324b15197629ad5a20c8d56cbc4eeff92", size = 24947000, upload-time = "2026-08-08T02:34:28.155Z" }, + { url = "https://files.pythonhosted.org/packages/9f/be/b759df521f9d3df913af5bf71a3799305f5cabd5183b51319907b5738d5d/rasterio-1.5.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b117209922784ef7948f702a0e9eb26be744f4b24d31f5eb97c45c0f04e2e4c3", size = 36842195, upload-time = "2026-08-08T02:36:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bd/1e84b24be51ce3950c73832c098b4cab49190ffdf343f13b51a35ca73a00/rasterio-1.5.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9093a9277559ac0f954d9a0266886bc4037e3ffd7a40837f6ea6486a96c8c258", size = 38311716, upload-time = "2026-08-08T02:32:56.612Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/676dd01db4e8a1735b6d893cd0f76920dab41d1b508579a7f5778ee1cf3c/rasterio-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:b3686865af2c67114f5dfb67df483db61bccad4078083a3dad481ea035e5d701", size = 30619960, upload-time = "2026-08-08T02:37:55.016Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7f/182833e6a7ad02191a72c8e0aa8ab4283379cf2f7c92c4d3ca0f9c881848/rasterio-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:0898355908ed3614ba1415532432cf4a1a308b189b2c9d8307a26a44bcb1713b", size = 28856656, upload-time = "2026-08-08T02:39:03.232Z" }, + { url = "https://files.pythonhosted.org/packages/16/ae/90778e26e8c85f8187feeb1b86d789381e5715f005074846ee5568115a00/rasterio-1.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c8702772d6e91c0578d84af88de7d4926c48b0b6eca33d0660e90d7f37c06bf1", size = 23240236, upload-time = "2026-08-08T02:35:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3e/27c957e788913445266728e9a81deb3a91fd5b8dff5bc9dd8281b3cceb18/rasterio-1.5.1-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:d8064e9b2820c3f1027399f410d8cdbfb57bdedfad46e5381afb527a9b6f74a2", size = 24951609, upload-time = "2026-08-08T02:34:39.468Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/6ca85602f26f55e0b673af34eb458f041f222673549ce6b54f93ba9707b5/rasterio-1.5.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ee01dc4b114a21078af87da916816c6c62ba5b5d216725f148ae695ef4deebf2", size = 36811929, upload-time = "2026-08-08T02:36:43.183Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4a/d691dfcbf3692ee2f57bc9fa7de61b03fca136cff1927ce08ab72bf4beab/rasterio-1.5.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ff9db698a88e716254fb0788768f75d7b58532c8253674261fa05abd40f621e", size = 38176919, upload-time = "2026-08-08T02:33:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/cc05ee6ae639801f79c35667bf0b1b163819625483b3231fb70858c1155f/rasterio-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:546ca44c4417772a48c5224b037f10330a75cde74b429f07ac43838e20f9b2c9", size = 31446190, upload-time = "2026-08-08T02:38:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/07c25fe92cf9a9e7095f396291dbc48186f17f3320119ccd1d5d8cc91fe9/rasterio-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:a685d2e97684b275b12859f9e0b95a8f511a51a2ac51b284428c6f50f1bcfd17", size = 29705188, upload-time = "2026-08-08T02:39:08.802Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/c4848b8bc666a16dc768d89ab63b2c0f509aa1e94f292d7d6109c081da9b/rasterio-1.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c361ed93cb8ceb314bf42d98815f2b6dd0254f7367d2b932435a8cf0aeaecf96", size = 23379128, upload-time = "2026-08-08T02:35:52.521Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/228a659e55c3da4bec7cdb857ad8fb201ea80a5c85a1bd1717e4c7997788/rasterio-1.5.1-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:00c78697cb565e97f99deb485fc7f23f28ff30e857124872ef0c5a3e83dd7e47", size = 25062694, upload-time = "2026-08-08T02:34:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/0c/59/15f99c480fffc044dde1ea8cde9c7a746997ceb639c624505f6a058e7f4b/rasterio-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:82719d4de76f3bafb165e932ad997b07116aa08621db079750ef1031dc514e11", size = 37417460, upload-time = "2026-08-08T02:36:52.372Z" }, + { url = "https://files.pythonhosted.org/packages/79/9a/7303424657fd71a1f8f4d17d8186de38e98e84f6fe36fe0a8660b7c918bf/rasterio-1.5.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8683074903018918341908e714f792ed5d696bad441ecec1787de8eb896573cd", size = 38474098, upload-time = "2026-08-08T02:33:11.294Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/fbfa20a98485cb39b70574e3bf9bd12603ffc319f80f64e0fbada1c743bf/rasterio-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0cd79ed52481d55395f1d3def075ff45173ed07ccd238fddeb0c63522b3d137", size = 31731560, upload-time = "2026-08-08T02:38:09.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/db/89bb06e64809c7644fc221895494068b23d7078509c8d070754bb01043cc/rasterio-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:11e2e9c68971beeec603c2613fa7740cf0a02dd026ef1aaa75d1cdf4246fbde6", size = 29788520, upload-time = "2026-08-08T02:39:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/c639c0fc46d775d47e70e0bec4af549313509dfcfbeaffdb903c16859f0e/rasterio-1.5.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:3aa1a3441587341b78558b5359aa5d2694b45a911d7b5df23af726420e420bce", size = 23239908, upload-time = "2026-08-08T02:35:59.075Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/ab1d83a84cae5514606dfba896761f02086db5af0f07b811e70e9b4ed23d/rasterio-1.5.1-cp315-cp315-macosx_15_0_x86_64.whl", hash = "sha256:60d74d11f290510639046c447256085eae0a925acdfe07e37f7436fb97cb9f46", size = 24951595, upload-time = "2026-08-08T02:34:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/51/5d/2c8f4d8bb0582b7dac694413892af901f14a4526a8a9f46d28cfb36b8d0b/rasterio-1.5.1-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:5933e56b15ef29fcbec370a2e0b5ac39b33023d85a3f5b01cdf0cbe4231c6204", size = 36816241, upload-time = "2026-08-08T02:37:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/f39df98b09524d0b44fdc54722e3b044fbf6620723415eef241760b5ffdb/rasterio-1.5.1-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:01f927ee7ef08cbc111b34092df266308fd2c64fad2e21a97c991973eb8e0c25", size = 38221966, upload-time = "2026-08-08T02:33:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/16/a2/d6eb95f239878d2a964a1cf2785378626c7cf4431ce2f54bc3b0210c0a6f/rasterio-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:55e18b8a6a7f4a9769942d48a1545c5a97920cf841fe5e4099032ba855bdf95c", size = 31446344, upload-time = "2026-08-08T02:38:15.559Z" }, + { url = "https://files.pythonhosted.org/packages/87/7c/5f390f8e9fa17cf55c62558dd3a08adfc1cbe7216f2718e8edf5caadab72/rasterio-1.5.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:94ae0fe4c0eb031da99614a1a1cd27819f900f86bc84976cd9f713eee05584a3", size = 23378528, upload-time = "2026-08-08T02:36:04.295Z" }, + { url = "https://files.pythonhosted.org/packages/eb/cd/788c62b2f5764f81d9906d3ec15dfc4ba4535487e2dd9f6f46d2151e495c/rasterio-1.5.1-cp315-cp315t-macosx_15_0_x86_64.whl", hash = "sha256:bf5d2cf43791651522fedb284cc08398f96fecc018dff24d7fc97965c909ab3a", size = 25060703, upload-time = "2026-08-08T02:34:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/59/d2/13f9d16f2cfd4e163973bff03dc730d21d1b6ebdcfc6bf8c4eca1e9ec746/rasterio-1.5.1-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:e4ea970d39f0b134fc91141d767b3c49bc2dea38a32f0b036fbdd5ec2e8ca457", size = 37369002, upload-time = "2026-08-08T02:37:08.127Z" }, + { url = "https://files.pythonhosted.org/packages/ed/20/8a44acb103a9f6a5a0f605d4c8825a2de26ea5420b561b4a23b9da864f6a/rasterio-1.5.1-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:fbb933659035f0aca32e4dc0dc022c7162b4f6598b15366c5b19b4a7e65c1991", size = 38538441, upload-time = "2026-08-08T02:33:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/12/3f/aa7fd1beda974c7e2ded73abdbc3978c7d60165ee70c0ee29c0ebef22a8b/rasterio-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ac450a2c1e6de990eaaee347ebceca160b1531c9f13c5db489d0e3113ea13984", size = 31728213, upload-time = "2026-08-08T02:38:21.338Z" }, +] + [[package]] name = "readfcs" version = "2.1.0" @@ -4509,6 +4865,12 @@ czi = [ { name = "bioio" }, { name = "bioio-czi" }, ] +segmentation = [ + { name = "geojson" }, + { name = "instanseg-torch" }, + { name = "rasterio" }, + { name = "tiffslide" }, +] [package.dev-dependencies] dev = [ @@ -4546,7 +4908,9 @@ requires-dist = [ { name = "bioio", marker = "extra == 'czi'" }, { name = "bioio-czi", marker = "extra == 'czi'" }, { name = "dask" }, + { name = "geojson", marker = "extra == 'segmentation'", specifier = ">=3" }, { name = "geopandas" }, + { name = "instanseg-torch", marker = "extra == 'segmentation'", specifier = ">=0.1.1" }, { name = "matplotlib" }, { name = "numpy" }, { name = "opencv-python-headless" }, @@ -4554,6 +4918,7 @@ requires-dist = [ { name = "openslide-python" }, { name = "pandas" }, { name = "pillow" }, + { name = "rasterio", marker = "extra == 'segmentation'", specifier = ">=1.3" }, { name = "rich" }, { name = "session-info2" }, { name = "shapely" }, @@ -4561,8 +4926,9 @@ requires-dist = [ { name = "spatialdata-io" }, { name = "spatialdata-plot" }, { name = "tifffile" }, + { name = "tiffslide", marker = "extra == 'segmentation'", specifier = ">=4" }, ] -provides-extras = ["czi"] +provides-extras = ["czi", "segmentation"] [package.metadata.requires-dev] dev = [{ name = "twine", specifier = ">=4.0.2" }] @@ -4850,6 +5216,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tabulate" version = "0.10.0" @@ -4889,6 +5267,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/ed/75bf4d6ae6fec7233ef466f27dffc99f91fde53a31e69f02640b418317ec/tifffile-2026.7.31-py3-none-any.whl", hash = "sha256:81adfa08012be1c478f99b83cda2f529eef8620cfbdf94fc41eef6f1d7b47dc5", size = 271576, upload-time = "2026-08-01T02:28:31.068Z" }, ] +[[package]] +name = "tiffslide" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "imagecodecs" }, + { name = "pillow" }, + { name = "tifffile" }, + { name = "typing-extensions" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/58/7322b0864f45cfe8ecd24fdcd41cd0e3817626134728babae03a9e26ed6e/tiffslide-4.0.0.tar.gz", hash = "sha256:8e056c1784b591249f15fed1a68edf69f641602817a5e9763d4aa97c1457e891", size = 747516, upload-time = "2026-07-03T01:24:37.047Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/7e/2357f0fe30599f5d4b162248257761b82b464271761dbe6a3c0a051a1bac/tiffslide-4.0.0-py3-none-any.whl", hash = "sha256:e34141e5c9c3aac9c535b4ad1904b3f14f26e9bf9a3d04314e4289ed267985c1", size = 34727, upload-time = "2026-07-03T01:24:35.949Z" }, +] + [[package]] name = "toolz" version = "1.1.0" @@ -4898,6 +5293,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, ] +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + [[package]] name = "tornado" version = "6.5.8" @@ -4936,6 +5370,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" }, ] +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + [[package]] name = "twine" version = "7.0.0"