spatialdata 0.7.3a2__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. spatialdata-0.8.0/.claude/skills/memray/SKILL.md +37 -0
  2. spatialdata-0.8.0/.claude/skills/profimp/SKILL.md +30 -0
  3. spatialdata-0.8.0/.claude/skills/pyspy/SKILL.md +45 -0
  4. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/workflows/test.yaml +4 -4
  5. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.gitignore +9 -2
  6. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.mypy.ini +1 -1
  7. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.pre-commit-config.yaml +3 -3
  8. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/PKG-INFO +11 -7
  9. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/README.md +6 -2
  10. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/_version.py +2 -2
  11. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/benchmarks/README.md +26 -0
  12. spatialdata-0.8.0/benchmarks/benchmark_dataloader.py +75 -0
  13. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/models_utils.md +1 -0
  14. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/index.md +7 -1
  15. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/pyproject.toml +39 -5
  16. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/_utils.py +3 -3
  17. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/concatenate.py +0 -8
  18. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/rasterize_bins.py +4 -8
  19. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/transform.py +35 -12
  20. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/query/_utils.py +5 -6
  21. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/query/relational_query.py +188 -133
  22. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/query/spatial_query.py +119 -120
  23. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/spatialdata.py +44 -22
  24. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/validation.py +4 -0
  25. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/_utils.py +65 -13
  26. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/io_points.py +1 -2
  27. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/io_raster.py +112 -15
  28. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/io_shapes.py +1 -2
  29. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_types.py +2 -2
  30. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/dataloader/__init__.py +3 -5
  31. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/dataloader/datasets.py +35 -17
  32. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/__init__.py +2 -0
  33. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/_utils.py +3 -2
  34. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/chunks_utils.py +2 -2
  35. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/models.py +51 -2
  36. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_rasterize_bins.py +0 -18
  37. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_spatialdata_operations.py +8 -5
  38. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_transform.py +74 -0
  39. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/query/test_relational_query.py +37 -1
  40. spatialdata-0.8.0/tests/core/query/test_relational_query_match_sdata_to_table.py +235 -0
  41. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/query/test_spatial_query.py +101 -1
  42. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_multi_table.py +1 -1
  43. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_readwrite.py +72 -0
  44. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_utils.py +24 -1
  45. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/models/test_models.py +8 -8
  46. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/utils/test_sanitize.py +16 -0
  47. spatialdata-0.7.3a2/tests/core/query/test_relational_query_match_sdata_to_table.py +0 -147
  48. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.bumpversion.cfg +0 -0
  49. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.editorconfig +0 -0
  50. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  51. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  52. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/codecov.yml +0 -0
  53. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/release.yml +0 -0
  54. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/workflows/build_image.yml +0 -0
  55. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.github/workflows/release.yaml +0 -0
  56. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.gitmodules +0 -0
  57. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/.readthedocs.yaml +0 -0
  58. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/CHANGELOG.md +0 -0
  59. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/Dockerfile +0 -0
  60. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/LICENSE +0 -0
  61. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/asv.conf.json +0 -0
  62. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/benchmarks/__init__.py +0 -0
  63. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/benchmarks/benchmark_imports.py +0 -0
  64. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/benchmarks/spatialdata_benchmark.py +0 -0
  65. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/benchmarks/utils.py +0 -0
  66. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/Makefile +0 -0
  67. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/_static/.gitkeep +0 -0
  68. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/_static/css/custom.css +0 -0
  69. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/_static/img/spatialdata_horizontal.png +0 -0
  70. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/SpatialData.md +0 -0
  71. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/data_formats.md +0 -0
  72. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/dataloader.md +0 -0
  73. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/datasets.md +0 -0
  74. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/io.md +0 -0
  75. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/models.md +0 -0
  76. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/operations.md +0 -0
  77. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/testing.md +0 -0
  78. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/transformations.md +0 -0
  79. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api/transformations_utils.md +0 -0
  80. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/api.md +0 -0
  81. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/changelog.md +0 -0
  82. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/conf.py +0 -0
  83. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/contributing.md +0 -0
  84. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/design_doc.md +0 -0
  85. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/extensions/typed_returns.py +0 -0
  86. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/glossary.md +0 -0
  87. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/installation.md +0 -0
  88. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/references.bib +0 -0
  89. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/references.md +0 -0
  90. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/docs/user_guide.md +0 -0
  91. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/__init__.py +0 -0
  92. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/__main__.py +0 -0
  93. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_bridges/__init__.py +0 -0
  94. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/__init__.py +0 -0
  95. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/_deepcopy.py +0 -0
  96. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/_elements.py +0 -0
  97. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/centroids.py +0 -0
  98. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/data_extent.py +0 -0
  99. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/__init__.py +0 -0
  100. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/_utils.py +0 -0
  101. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/aggregate.py +0 -0
  102. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/map.py +0 -0
  103. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/rasterize.py +0 -0
  104. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/operations/vectorize.py +0 -0
  105. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_core/query/__init__.py +0 -0
  106. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_docs.py +0 -0
  107. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/__init__.py +0 -0
  108. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/format.py +0 -0
  109. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/io_table.py +0 -0
  110. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_io/io_zarr.py +0 -0
  111. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_logging.py +0 -0
  112. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/_utils.py +0 -0
  113. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/config.py +0 -0
  114. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/datasets.py +0 -0
  115. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/io/__init__.py +0 -0
  116. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/_accessor.py +0 -0
  117. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/models/pyramids_utils.py +0 -0
  118. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/testing.py +0 -0
  119. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/__init__.py +0 -0
  120. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/_utils.py +0 -0
  121. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/ngff/__init__.py +0 -0
  122. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/ngff/_utils.py +0 -0
  123. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/ngff/ngff_coordinate_system.py +0 -0
  124. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/ngff/ngff_transformations.py +0 -0
  125. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/operations.py +0 -0
  126. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/src/spatialdata/transformations/transformations.py +0 -0
  127. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/__init__.py +0 -0
  128. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/conftest.py +0 -0
  129. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/__init__.py +0 -0
  130. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/__init__.py +0 -0
  131. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/conftest.py +0 -0
  132. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_aggregations.py +0 -0
  133. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_map.py +0 -0
  134. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_rasterize.py +0 -0
  135. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/operations/test_vectorize.py +0 -0
  136. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/query/__init__.py +0 -0
  137. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/test_centroids.py +0 -0
  138. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/test_data_extent.py +0 -0
  139. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/test_deepcopy.py +0 -0
  140. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/test_get_attrs.py +0 -0
  141. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/core/test_validation.py +0 -0
  142. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/data/multipolygon.json +0 -0
  143. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/data/points.json +0 -0
  144. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/data/polygon.json +0 -0
  145. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/dataloader/__init__.py +0 -0
  146. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/dataloader/conftest.py +0 -0
  147. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/dataloader/test_datasets.py +0 -0
  148. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/datasets/__init__.py +0 -0
  149. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/datasets/test_datasets.py +0 -0
  150. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/__init__.py +0 -0
  151. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_attrs_io.py +0 -0
  152. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_format.py +0 -0
  153. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_metadata.py +0 -0
  154. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_partial_read.py +0 -0
  155. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/io/test_pyramids_performance.py +0 -0
  156. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/models/__init__.py +0 -0
  157. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/models/test_accessor.py +0 -0
  158. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/models/test_chunks_utils.py +0 -0
  159. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/models/test_pyramids_utils.py +0 -0
  160. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/test_init.py +0 -0
  161. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/__init__.py +0 -0
  162. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/ngff/__init__.py +0 -0
  163. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/ngff/conftest.py +0 -0
  164. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/ngff/test_ngff_coordinate_system.py +0 -0
  165. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/ngff/test_ngff_transformations.py +0 -0
  166. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/test_transformations.py +0 -0
  167. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/transformations/test_transformations_utils.py +0 -0
  168. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/utils/__init__.py +0 -0
  169. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/utils/test_element_utils.py +0 -0
  170. {spatialdata-0.7.3a2 → spatialdata-0.8.0}/tests/utils/test_testing.py +0 -0
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: memray
3
+ description: Profile the memory usage of a Python script using memray and visualize a temporal flamegraph in the browser. Use when the user wants to investigate memory consumption, find leaks, or understand allocation patterns.
4
+ compatibility: Requires the pixi profiling environment (pixi run -e profiling). Supports Linux and macOS.
5
+ allowed-tools: Bash(pixi run -e profiling memray-run:*) Bash(pixi run -e profiling memray-flame:*) Bash(open:*) Bash(xdg-open:*) Bash(python -m webbrowser:*)
6
+ ---
7
+
8
+ ## Steps
9
+
10
+ 1. Ask the user which script to profile (full or relative path).
11
+
12
+ 2. Run the script under memray:
13
+
14
+ ```bash
15
+ pixi run -e profiling memray-run script.py
16
+ ```
17
+
18
+ This produces a binary file named `memray-script.py.<pid>.bin` in the current directory.
19
+
20
+ 3. Generate the flamegraph HTML report from the `.bin` file:
21
+
22
+ ```bash
23
+ pixi run -e profiling memray-flame memray-script.py.<pid>.bin
24
+ ```
25
+
26
+ Replace `<pid>` with the actual PID shown in the filename. This writes `memray-flamegraph-script.py.<pid>.html`.
27
+
28
+ 4. Open the report in the browser:
29
+ - macOS: `open memray-flamegraph-script.py.<pid>.html`
30
+ - Linux: `xdg-open memray-flamegraph-script.py.<pid>.html`
31
+ - Either: `python -m webbrowser memray-flamegraph-script.py.<pid>.html`
32
+
33
+ ## Notes
34
+
35
+ - The `--temporal` flag (included in `memray-flame`) shows memory over time, not just peak — use this to spot leaks and allocation bursts.
36
+ - To find the `.bin` file if unsure of the name: `ls memray-*.bin`
37
+ - To compare runs, save the previous report: `cp memray-flamegraph-script.py.<pid>.html memray-flamegraph-before.html`
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: profimp
3
+ description: Profile Python import time using profimp and open a waterfall HTML report. Use when investigating slow startup or wanting to identify which imports are most expensive.
4
+ compatibility: Requires profimp (available as a pixi dependency). macOS or Linux.
5
+ allowed-tools: Bash(profimp:*) Bash(open:*) Bash(xdg-open:*) Bash(python -m webbrowser:*)
6
+ ---
7
+
8
+ ## Steps
9
+
10
+ 1. Ask what to profile. Suggest common patterns for this repo:
11
+ - `import spatialdata`
12
+ - `from spatialdata import SpatialData`
13
+ - `from spatialdata_io import xenium`
14
+
15
+ 2. Run:
16
+
17
+ ```bash
18
+ profimp --html "<import_stmt>" > /tmp/profimp.html
19
+ ```
20
+
21
+ 3. Open the report:
22
+ - macOS: `open /tmp/profimp.html`
23
+ - Linux: `xdg-open /tmp/profimp.html`
24
+ - Either: `python -m webbrowser /tmp/profimp.html`
25
+
26
+ ## Notes
27
+
28
+ - The report is a waterfall chart showing every sub-import and its timing.
29
+ - To compare before/after: `cp /tmp/profimp.html /tmp/profimp-before.html` before re-running.
30
+ - `pixi run python -m profimp` also works if `profimp` is not on PATH.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: pyspy
3
+ description: Profile the execution time of a Python script using py-spy and visualize the result with speedscope. Use when the user wants to benchmark performance, find slow code paths, or profile CPU time.
4
+ compatibility: Requires the pixi profiling environment (pixi run -e profiling). Speedscope must be installed separately (npm install -g speedscope). sudo is required on macOS.
5
+ allowed-tools: Bash(pixi run -e profiling pyspy:*) Bash(pixi run -e profiling speedscope:*) Bash(sudo pixi run -e profiling pyspy:*)
6
+ ---
7
+
8
+ ## Steps
9
+
10
+ 1. Ask the user which script to profile (full or relative path).
11
+
12
+ 2. Run py-spy to record the profile. The output is always written to `profile.speedscope.json` in the current directory.
13
+
14
+ **Linux** (no sudo needed):
15
+
16
+ ```bash
17
+ pixi run -e profiling pyspy script.py
18
+ ```
19
+
20
+ **macOS** (sudo required — py-spy needs to attach to the process):
21
+
22
+ ```bash
23
+ sudo pixi run -e profiling pyspy script.py
24
+ ```
25
+
26
+ If sudo fails to find the pixi environment, use absolute paths:
27
+
28
+ ```bash
29
+ sudo /path/to/.pixi/envs/profiling/bin/py-spy record --gil \
30
+ -o profile.speedscope.json --format speedscope \
31
+ -- /path/to/.pixi/envs/profiling/bin/python script.py
32
+ ```
33
+
34
+ 3. Open the result in speedscope:
35
+ ```bash
36
+ pixi run -e profiling speedscope
37
+ ```
38
+ This opens `profile.speedscope.json` in the browser via the local speedscope CLI.
39
+
40
+ ## Notes
41
+
42
+ - If the speedscope view is blank, switch threads using the thread selector in the top-right corner.
43
+ - To save a profile before overwriting: `cp profile.speedscope.json profile-before.speedscope.json`
44
+ - `--gil` records only time when the GIL is held (Python-level CPU time). Drop it to include C extension time.
45
+ - speedscope must be installed globally: `npm install -g speedscope`
@@ -19,11 +19,12 @@ jobs:
19
19
  fail-fast: false
20
20
  matrix:
21
21
  include:
22
- - {os: windows-latest, python: "3.11", dask-version: "2025.12.0", name: "min dask"}
22
+ - {os: windows-latest, python: "3.12", dask-version: "2026.3.0", name: "min dask"}
23
23
  - {os: windows-latest, python: "3.14", dask-version: "latest"}
24
- - {os: ubuntu-latest, python: "3.11", dask-version: "latest"}
24
+ - {os: ubuntu-latest, python: "3.12", dask-version: "latest"}
25
+ - {os: ubuntu-latest, python: "3.13", dask-version: "latest"}
25
26
  - {os: ubuntu-latest, python: "3.14", dask-version: "latest"}
26
- - {os: macos-latest, python: "3.11", dask-version: "latest"}
27
+ - {os: macos-latest, python: "3.12", dask-version: "latest"}
27
28
  - {os: macos-latest, python: "3.14", prerelease: "allow", name: "prerelease"}
28
29
  env:
29
30
  OS: ${{ matrix.os }}
@@ -41,7 +42,6 @@ jobs:
41
42
  - name: Install dependencies
42
43
  run: |
43
44
  if [[ "${PRERELEASE}" == "allow" ]]; then
44
- sed -i '' 's/requires-python.*//' pyproject.toml # otherwise uv complains that anndata requires python>=3.12 and we only do >=3.11 😱
45
45
  uv add git+https://github.com/scverse/anndata.git
46
46
  uv add pandas>=3.dev0
47
47
  fi
@@ -46,11 +46,18 @@ spatialdata-sandbox
46
46
  # version file
47
47
  _version.py
48
48
 
49
- # other
50
- node_modules/
49
+ # agents configurations
50
+ .claude/settings.local.json
51
51
 
52
+ # benchmarking and profiling
52
53
  .asv/
54
+ profile.speedscope.json
55
+
56
+ # other
57
+ node_modules/
53
58
 
54
59
  .mypy_cache
55
60
  .ruff_cache
56
61
  uv.lock
62
+ pixi.lock
63
+
@@ -1,5 +1,5 @@
1
1
  [mypy]
2
- python_version = 3.11
2
+ python_version = 3.12
3
3
 
4
4
  ignore_errors = False
5
5
  warn_redundant_casts = True
@@ -9,18 +9,18 @@ ci:
9
9
  skip: []
10
10
  repos:
11
11
  - repo: https://github.com/rbubley/mirrors-prettier
12
- rev: v3.8.3
12
+ rev: v3.8.4
13
13
  hooks:
14
14
  - id: prettier
15
15
  exclude: ^.github/workflows/test.yaml
16
16
  - repo: https://github.com/pre-commit/mirrors-mypy
17
- rev: v1.20.2
17
+ rev: v2.1.0
18
18
  hooks:
19
19
  - id: mypy
20
20
  additional_dependencies: [numpy, types-requests]
21
21
  exclude: tests/|docs/
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
- rev: v0.15.12
23
+ rev: v0.15.18
24
24
  hooks:
25
25
  - id: ruff
26
26
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialdata
3
- Version: 0.7.3a2
3
+ Version: 0.8.0
4
4
  Summary: Spatial data format.
5
5
  Project-URL: Documentation, https://spatialdata.scverse.org/en/latest
6
6
  Project-URL: Source, https://github.com/scverse/spatialdata.git
@@ -37,21 +37,21 @@ License: BSD 3-Clause License
37
37
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
38
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
39
  License-File: LICENSE
40
- Requires-Python: >=3.11
40
+ Requires-Python: >=3.12
41
41
  Requires-Dist: anndata>=0.9.1
42
42
  Requires-Dist: annsel>=0.1.2
43
43
  Requires-Dist: click
44
44
  Requires-Dist: dask-image
45
- Requires-Dist: dask<2026.1.2,>=2025.12.0
45
+ Requires-Dist: dask>=2026.3.0
46
46
  Requires-Dist: datashader
47
- Requires-Dist: distributed<2026.1.2
47
+ Requires-Dist: distributed>=2026.3.0
48
48
  Requires-Dist: fsspec[http,s3]
49
49
  Requires-Dist: geopandas>=0.14
50
50
  Requires-Dist: multiscale-spatial-image==2.0.3
51
51
  Requires-Dist: networkx
52
52
  Requires-Dist: numba>=0.55.0
53
53
  Requires-Dist: numpy
54
- Requires-Dist: ome-zarr>=0.14.0
54
+ Requires-Dist: ome-zarr>=0.16.0
55
55
  Requires-Dist: pandas
56
56
  Requires-Dist: pooch
57
57
  Requires-Dist: pyarrow
@@ -82,7 +82,7 @@ Description-Content-Type: text/markdown
82
82
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/scverse/spatialdata/main.svg)](https://results.pre-commit.ci/latest/github/scverse/spatialdata/main)
83
83
  [![codecov](https://codecov.io/gh/scverse/spatialdata/branch/main/graph/badge.svg?token=X19DRSIMCU)](https://codecov.io/gh/scverse/spatialdata)
84
84
  [![documentation badge](https://readthedocs.org/projects/scverse-spatialdata/badge/?version=latest)](https://spatialdata.scverse.org/en/latest/)
85
- [![DOI](https://zenodo.org/badge/487366481.svg)](https://zenodo.org/badge/latestdoi/487366481)
85
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20056407.svg)](https://doi.org/10.5281/zenodo.20056407)
86
86
  [![Downloads](https://static.pepy.tech/badge/spatialdata)](https://pepy.tech/project/spatialdata)
87
87
  [![Release](https://github.com/scverse/spatialdata/actions/workflows/release.yaml/badge.svg?event=release)](https://github.com/scverse/spatialdata/actions/workflows/release.yaml)
88
88
  [![Documentation][badge-pypi]][link-pypi]
@@ -91,11 +91,15 @@ Description-Content-Type: text/markdown
91
91
  [badge-pypi]: https://badge.fury.io/py/spatialdata.svg
92
92
  [link-pypi]: https://pypi.org/project/spatialdata/
93
93
 
94
- SpatialData is a data framework that comprises a FAIR storage format and a collection of python libraries for performant access, alignment, and processing of uni- and multi-modal spatial omics datasets. This repository contains the core spatialdata library. See the links below to learn more about other packages in the SpatialData ecosystem.
94
+ SpatialData is a data framework, a schema and a serialization format for uni- and multi-modal spatial omics datasets. It is supported by a growing suite of software libraries for performant access, manipulation and analysis in multiple languages, including Python, R and JavaScript.
95
+
96
+ This repository contains the core spatialdata library. See the links below to learn more about other packages in the SpatialData ecosystem.
95
97
 
96
98
  - [spatialdata-io](https://github.com/scverse/spatialdata-io): load data from common spatial omics technologies into spatialdata.
97
99
  - [spatialdata-plot](https://github.com/scverse/spatialdata-plot): Static plotting library for spatialdata.
98
100
  - [napari-spatialdata](https://github.com/scverse/napari-spatialdata): napari plugin for interactive exploration and annotation of spatial data.
101
+ - [SpatialData (R)](https://github.com/HelenaLC/SpatialData): R implementation of the SpatialData framework.
102
+ - [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js): JavaScript/TypeScript implementation of the SpatialData framework.
99
103
 
100
104
  [//]: # "numfocus-fiscal-sponsor-attribution"
101
105
 
@@ -6,7 +6,7 @@
6
6
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/scverse/spatialdata/main.svg)](https://results.pre-commit.ci/latest/github/scverse/spatialdata/main)
7
7
  [![codecov](https://codecov.io/gh/scverse/spatialdata/branch/main/graph/badge.svg?token=X19DRSIMCU)](https://codecov.io/gh/scverse/spatialdata)
8
8
  [![documentation badge](https://readthedocs.org/projects/scverse-spatialdata/badge/?version=latest)](https://spatialdata.scverse.org/en/latest/)
9
- [![DOI](https://zenodo.org/badge/487366481.svg)](https://zenodo.org/badge/latestdoi/487366481)
9
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20056407.svg)](https://doi.org/10.5281/zenodo.20056407)
10
10
  [![Downloads](https://static.pepy.tech/badge/spatialdata)](https://pepy.tech/project/spatialdata)
11
11
  [![Release](https://github.com/scverse/spatialdata/actions/workflows/release.yaml/badge.svg?event=release)](https://github.com/scverse/spatialdata/actions/workflows/release.yaml)
12
12
  [![Documentation][badge-pypi]][link-pypi]
@@ -15,11 +15,15 @@
15
15
  [badge-pypi]: https://badge.fury.io/py/spatialdata.svg
16
16
  [link-pypi]: https://pypi.org/project/spatialdata/
17
17
 
18
- SpatialData is a data framework that comprises a FAIR storage format and a collection of python libraries for performant access, alignment, and processing of uni- and multi-modal spatial omics datasets. This repository contains the core spatialdata library. See the links below to learn more about other packages in the SpatialData ecosystem.
18
+ SpatialData is a data framework, a schema and a serialization format for uni- and multi-modal spatial omics datasets. It is supported by a growing suite of software libraries for performant access, manipulation and analysis in multiple languages, including Python, R and JavaScript.
19
+
20
+ This repository contains the core spatialdata library. See the links below to learn more about other packages in the SpatialData ecosystem.
19
21
 
20
22
  - [spatialdata-io](https://github.com/scverse/spatialdata-io): load data from common spatial omics technologies into spatialdata.
21
23
  - [spatialdata-plot](https://github.com/scverse/spatialdata-plot): Static plotting library for spatialdata.
22
24
  - [napari-spatialdata](https://github.com/scverse/napari-spatialdata): napari plugin for interactive exploration and annotation of spatial data.
25
+ - [SpatialData (R)](https://github.com/HelenaLC/SpatialData): R implementation of the SpatialData framework.
26
+ - [SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js): JavaScript/TypeScript implementation of the SpatialData framework.
23
27
 
24
28
  [//]: # "numfocus-fiscal-sponsor-attribution"
25
29
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.3a2'
22
- __version_tuple__ = version_tuple = (0, 7, 3, 'a2')
21
+ __version__ = version = '0.8.0'
22
+ __version_tuple__ = version_tuple = (0, 8, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -65,6 +65,32 @@ git checkout - && git stash pop
65
65
  asv compare main HEAD
66
66
  ```
67
67
 
68
+ ### Dataloader benchmarks
69
+
70
+ Dataloader benchmarks live in `benchmarks/benchmark_dataloader.py`. They use a synthetic in-memory `SpatialData` (2048×2048 image, 500 circle regions) and compute two metrics:
71
+
72
+ - `time_init` — constructing `ImageTilesDataset` (includes bounding-box pre-computation).
73
+ - `time_fetch` — iterating over all 500 tiles once (pure `__getitem__` calls, no `DataLoader` overhead).
74
+
75
+ Run both in your current environment:
76
+
77
+ ```bash
78
+ asv run --python=same --show-stderr -b TimeDataloader
79
+ ```
80
+
81
+ Or a single method:
82
+
83
+ ```bash
84
+ asv run --python=same --show-stderr -b TimeDataloader.time_init
85
+ asv run --python=same --show-stderr -b TimeDataloader.time_fetch
86
+ ```
87
+
88
+ Compare against `main` in one shot:
89
+
90
+ ```bash
91
+ asv continuous --show-stderr -v -b TimeDataloader main HEAD
92
+ ```
93
+
68
94
  ### Querying benchmarks
69
95
 
70
96
  Querying using a bounding box without a spatial index is highly impacted by large amounts of points (transcripts), more than table rows (cells).
@@ -0,0 +1,75 @@
1
+ # type: ignore
2
+ """Benchmarks for ImageTilesDataset: init time and iteration time."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import anndata as ad
7
+ import geopandas as gpd
8
+ import numpy as np
9
+ import pandas as pd
10
+ from shapely.geometry import Point
11
+
12
+ import spatialdata as sd
13
+ from spatialdata.dataloader import ImageTilesDataset
14
+ from spatialdata.models import Image2DModel, ShapesModel, TableModel
15
+ from spatialdata.transformations import Identity
16
+
17
+ _RNG = np.random.default_rng(42)
18
+
19
+ _IMAGE_SIZE = 2048
20
+ _N_CIRCLES = 500
21
+ _N_CHANNELS = 3
22
+
23
+ _DATASET_KWARGS = {
24
+ "regions_to_images": {"circles": "image"},
25
+ "regions_to_coordinate_systems": {"circles": "global"},
26
+ "table_name": "table",
27
+ "return_annotations": "instance_id",
28
+ }
29
+
30
+
31
+ def _make_sdata() -> sd.SpatialData:
32
+ img_data = _RNG.integers(0, 256, size=(_N_CHANNELS, _IMAGE_SIZE, _IMAGE_SIZE), dtype=np.uint8).astype(np.float32)
33
+ image = Image2DModel.parse(img_data, dims=["c", "y", "x"], transformations={"global": Identity()})
34
+
35
+ radius = 32.0
36
+ cx = _RNG.uniform(radius, _IMAGE_SIZE - radius, size=_N_CIRCLES)
37
+ cy = _RNG.uniform(radius, _IMAGE_SIZE - radius, size=_N_CIRCLES)
38
+ geom = gpd.GeoDataFrame({"geometry": [Point(x, y) for x, y in zip(cx, cy, strict=True)]})
39
+ geom["radius"] = radius
40
+ circles = ShapesModel.parse(geom, transformations={"global": Identity()})
41
+
42
+ table = ad.AnnData(
43
+ _RNG.random((_N_CIRCLES, 10)).astype(np.float32),
44
+ obs=pd.DataFrame(
45
+ {
46
+ "region": pd.Categorical(["circles"] * _N_CIRCLES),
47
+ "instance_id": np.arange(_N_CIRCLES, dtype=np.int64),
48
+ },
49
+ index=[str(i) for i in range(_N_CIRCLES)],
50
+ ),
51
+ )
52
+ table = TableModel.parse(table, region="circles", region_key="region", instance_key="instance_id")
53
+
54
+ return sd.SpatialData(images={"image": image}, shapes={"circles": circles}, tables={"table": table})
55
+
56
+
57
+ class TimeDataloader:
58
+ """Time ImageTilesDataset construction and tile iteration."""
59
+
60
+ def setup(self):
61
+ self.sdata = _make_sdata()
62
+ self.ds = ImageTilesDataset(sdata=self.sdata, **_DATASET_KWARGS)
63
+
64
+ def teardown(self):
65
+ del self.ds
66
+ del self.sdata
67
+
68
+ def time_init(self):
69
+ """Time constructing ImageTilesDataset (bounding-box pre-computation)."""
70
+ ImageTilesDataset(sdata=self.sdata, **_DATASET_KWARGS)
71
+
72
+ def time_fetch(self):
73
+ """Time iterating over every tile once."""
74
+ for i in range(len(self.ds)):
75
+ _ = self.ds[i]
@@ -4,6 +4,7 @@
4
4
  .. currentmodule:: spatialdata.models
5
5
 
6
6
  .. autofunction:: get_model
7
+ .. autofunction:: validate_element
7
8
  .. autodata:: SpatialElement
8
9
  .. autofunction:: get_axes_names
9
10
  .. autofunction:: get_spatial_axes
@@ -6,11 +6,15 @@
6
6
 
7
7
  # An open and universal framework for processing spatial omics data.
8
8
 
9
- SpatialData is a data framework that comprises a FAIR storage format and a collection of python libraries for performant access, alignment, and processing of uni- and multi-modal spatial omics datasets. This page provides documentation on how to install, use, and extend the core `spatialdata` library. See the links below to learn more about other packages in the SpatialData ecosystem.
9
+ SpatialData is a data framework, a schema and a serialization format for uni- and multi-modal spatial omics datasets. It is supported by a growing suite of software libraries for performant access, manipulation and analysis in multiple languages, including Python, R and JavaScript.
10
+
11
+ This page provides documentation on how to install, use, and extend the core `spatialdata` library. See the links below to learn more about other packages in the SpatialData ecosystem.
10
12
 
11
13
  - `spatialdata-io`: load data from common spatial omics technologies into `spatialdata` ([repository][spatialdata-io-repo], [documentation][spatialdata-io-docs]).
12
14
  - `spatialdata-plot`: Static plotting library for `spatialdata` ([repository][spatialdata-plot-repo], [documentation][spatialdata-plot-docs]).
13
15
  - `napari-spatialdata-repo`: napari plugin for interactive exploration and annotation of `spatialdata` ([repository][napari-spatialdata-repo], [documentation][napari-spatialdata-docs]).
16
+ - `SpatialData` (R): R implementation of the SpatialData framework ([repository][spatialdata-r-repo]).
17
+ - `SpatialData.js`: JavaScript/TypeScript implementation of the SpatialData framework ([repository][spatialdata-js-repo]).
14
18
 
15
19
  Please see our publication {cite}`marconatoSpatialDataOpenUniversal2024` for citation and to learn more.
16
20
 
@@ -119,3 +123,5 @@ references.md
119
123
  [napari-spatialdata-docs]: https://spatialdata.scverse.org/projects/napari/en/stable/notebooks/spatialdata.html
120
124
  [spatialdata-io-docs]: https://spatialdata.scverse.org/projects/io/en/stable/
121
125
  [spatialdata-plot-docs]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
126
+ [spatialdata-r-repo]: https://github.com/HelenaLC/SpatialData
127
+ [spatialdata-js-repo]: https://github.com/Taylor-CCB-Group/SpatialData.js
@@ -15,7 +15,7 @@ maintainers = [
15
15
  urls.Documentation = "https://spatialdata.scverse.org/en/latest"
16
16
  urls.Source = "https://github.com/scverse/spatialdata.git"
17
17
  urls.Home-page = "https://github.com/scverse/spatialdata.git"
18
- requires-python = ">=3.11"
18
+ requires-python = ">=3.12"
19
19
  dynamic= [
20
20
  "version" # allow version to be set by git tags
21
21
  ]
@@ -26,8 +26,8 @@ dependencies = [
26
26
  "annsel>=0.1.2",
27
27
  "click",
28
28
  "dask-image",
29
- "dask>=2025.12.0,<2026.1.2",
30
- "distributed<2026.1.2",
29
+ "dask>=2026.3.0",
30
+ "distributed>=2026.3.0",
31
31
  "datashader",
32
32
  "fsspec[s3,http]",
33
33
  "geopandas>=0.14",
@@ -35,7 +35,7 @@ dependencies = [
35
35
  "networkx",
36
36
  "numba>=0.55.0",
37
37
  "numpy",
38
- "ome_zarr>=0.14.0",
38
+ "ome_zarr>=0.16.0",
39
39
  "pandas",
40
40
  "pooch",
41
41
  "pyarrow",
@@ -145,7 +145,7 @@ exclude = [
145
145
 
146
146
  ]
147
147
  line-length = 120
148
- target-version = "py311"
148
+ target-version = "py312"
149
149
 
150
150
  [tool.ruff.lint]
151
151
  ignore = [
@@ -210,3 +210,37 @@ convention = "numpy"
210
210
 
211
211
  # pyupgrade typing rewrite TODO: remove at some point from per-file ignore
212
212
  # "UP006", "UP007"
213
+
214
+ [tool.pixi.workspace]
215
+ channels = ["conda-forge"]
216
+ platforms = ["osx-arm64", "linux-64"]
217
+
218
+ [tool.pixi.dependencies]
219
+ proj = "*"
220
+
221
+ [tool.pixi.tasks]
222
+ test = "pytest"
223
+ test-parallel = "pytest -n auto --dist worksteal"
224
+ pre-commit = "pre-commit run"
225
+ pre-commit-all = "pre-commit run --all-files"
226
+
227
+ [tool.pixi.feature.profiling.dependencies]
228
+ py-spy = "*"
229
+
230
+ [tool.pixi.feature.profiling.pypi-dependencies]
231
+ spatialdata = { path = ".", editable = true }
232
+ profimp = "*"
233
+ memray = "*"
234
+
235
+ [tool.pixi.feature.profiling.tasks]
236
+ # Usage: pixi run -e profiling pyspy script.py (prefix with sudo on macOS)
237
+ pyspy = "py-spy record --gil -o profile.speedscope.json --format speedscope -- python"
238
+ # Usage: pixi run -e profiling speedscope
239
+ speedscope = "npx --yes speedscope profile.speedscope.json"
240
+ # Usage: pixi run -e profiling memray-run script.py
241
+ memray-run = "memray run"
242
+ # Usage: pixi run -e profiling memray-flame memray-script.py.<pid>.bin
243
+ memray-flame = "memray flamegraph --temporal"
244
+
245
+ [tool.pixi.environments]
246
+ profiling = { features = ["profiling"], solve-group = "default" }
@@ -158,9 +158,9 @@ def sanitize_table(data: AnnData, inplace: bool = True) -> AnnData | None:
158
158
  # Handle other attributes
159
159
  for attr in ("obsm", "obsp", "varm", "varp", "uns", "layers"):
160
160
  d = getattr(sanitized, attr)
161
- new_keys = {old: get_unique_name(old, attr) for old in d}
162
- # Create new dictionary with sanitized keys
163
- new_dict = {new_keys[old]: value for old, value in d.items()}
161
+ # None is a valid key in layers (anndata >= 0.13: represents X); skip sanitizing it
162
+ new_keys = {old: get_unique_name(old, attr) for old in d if old is not None}
163
+ new_dict = {(new_keys[old] if old is not None else old): value for old, value in d.items()}
164
164
  setattr(sanitized, attr, new_dict)
165
165
 
166
166
  return None if inplace else sanitized
@@ -219,14 +219,6 @@ def concatenate(
219
219
  return sdata
220
220
 
221
221
 
222
- def _filter_table_in_coordinate_systems(table: AnnData, coordinate_systems: list[str]) -> AnnData:
223
- table_mapping_metadata = table.uns[TableModel.ATTRS_KEY]
224
- region_key = table_mapping_metadata[TableModel.REGION_KEY_KEY]
225
- new_table = table[table.obs[region_key].isin(coordinate_systems)].copy()
226
- new_table.uns[TableModel.ATTRS_KEY][TableModel.REGION_KEY] = new_table.obs[region_key].unique().tolist()
227
- return new_table
228
-
229
-
230
222
  def _fix_ensure_unique_element_names(
231
223
  sdatas: dict[str, SpatialData],
232
224
  rename_tables: bool,
@@ -87,14 +87,10 @@ def rasterize_bins(
87
87
  table = sdata.tables[table_name]
88
88
  if not isinstance(element, GeoDataFrame | DaskDataFrame | DataArray):
89
89
  raise ValueError("The bins should be a GeoDataFrame, a DaskDataFrame or a DataArray.")
90
- if isinstance(element, DataArray):
91
- if "c" in element.dims:
92
- raise ValueError(
93
- "If bins is a DataArray, it should hold labels; found a image element instead, with"
94
- f" 'c': {element.dims}."
95
- )
96
- if not np.issubdtype(element.dtype, np.integer):
97
- raise ValueError(f"If bins is a DataArray, it should hold integers. Found dtype {element.dtype}.")
90
+ if isinstance(element, DataArray) and "c" in element.dims:
91
+ raise ValueError(
92
+ f"If bins is a DataArray, it should hold labels; found a image element instead, with 'c': {element.dims}."
93
+ )
98
94
 
99
95
  _, region_key, instance_key = get_table_keys(table)
100
96
  if not table.obs[region_key].dtype == "category":
@@ -6,10 +6,11 @@ import warnings
6
6
  from functools import singledispatch
7
7
  from typing import TYPE_CHECKING, Any, cast
8
8
 
9
+ import dask
9
10
  import dask.array as da
11
+ import dask.dataframe as dd
10
12
  import dask_image.ndinterp
11
13
  import numpy as np
12
- import pandas as pd
13
14
  from dask.array.core import Array as DaskArray
14
15
  from dask.dataframe import DataFrame as DaskDataFrame
15
16
  from geopandas import GeoDataFrame
@@ -442,20 +443,26 @@ def _(
442
443
  axes = get_axes_names(data)
443
444
  arrays = []
444
445
 
445
- # Workaround to prevent partition collaps and missing dependency problem for now.
446
+ # Dask's expression optimizer can collapse partitions at compute time, making the partition
447
+ # structure inside vs. outside a disable_dask_tune_optimization() context inconsistent. To avoid
448
+ # index-alignment failures (e.g. "cannot reindex on an axis with duplicate labels" from parquet
449
+ # files that each start their index at 0) and length-mismatch errors, we materialise the non-axis
450
+ # columns and compute the axis arrays inside a single context where the partition structure is
451
+ # stable, then do plain pandas operations and re-wrap with dd.from_delayed (not dd.from_pandas,
452
+ # which sorts by index and would scramble rows for non-monotonic or duplicate indices).
446
453
  with disable_dask_tune_optimization() if data.npartitions > 1 else contextlib.nullcontext():
454
+ lengths = [len(part) for part in data.partitions]
447
455
  for ax in axes:
448
456
  # TODO We have to pass on the lengths explicitly as automatic determination with dask graph optimization
449
- # leads to collaps of the partitions. However this causes a missing dependency problem, which for now is
457
+ # leads to collapse of the partitions. However this causes a missing dependency problem, which for now is
450
458
  # prevented by setting the optimization to False when performing this operation.
451
- arrays.append(data[ax].to_dask_array(lengths=[len(part) for part in data.partitions]).reshape(-1, 1))
459
+ arrays.append(data[ax].to_dask_array(lengths=lengths).reshape(-1, 1))
452
460
 
453
- xdata = DataArray(da.concatenate(arrays, axis=1), coords={"points": range(len(data)), "dim": list(axes)})
454
- xtransformed = transformation._transform_coordinates(xdata)
455
- transformed = data.drop(columns=list(axes)).copy()
456
- # dummy transformation that will be replaced by _adjust_transformation()
457
- default_cs = {DEFAULT_COORDINATE_SYSTEM: Identity()}
458
- transformed.attrs[TRANSFORM_KEY] = default_cs
461
+ xdata = DataArray(da.concatenate(arrays, axis=1), coords={"points": range(sum(lengths)), "dim": list(axes)})
462
+ xtransformed = transformation._transform_coordinates(xdata)
463
+
464
+ # Compute non-axis columns while the partition structure is still stable; preserves original index.
465
+ transformed_pd = data.drop(columns=list(axes)).compute()
459
466
 
460
467
  for ax in axes:
461
468
  indices = xtransformed["dim"] == ax
@@ -463,8 +470,24 @@ def _(
463
470
  # TODO: discuss with dask team
464
471
  # This is not nice, but otherwise there is a problem with the joint graph of new_ax and transformed, causing
465
472
  # a getattr missing dependency of dependent from_dask_array.
466
- new_col = pd.Series(new_ax.data.flatten().compute(), index=transformed.index)
467
- transformed[ax] = new_col
473
+ # Assigning a numpy array is positional (no index alignment), so the original index is preserved.
474
+ transformed_pd[ax] = new_ax.data.flatten().compute()
475
+
476
+ # Reconstruct as a dask DataFrame via delayed partitions so that:
477
+ # (a) row order matches the original (dd.from_pandas sorts by index, which scrambles rows for
478
+ # non-monotonic or duplicate indices such as those produced by multi-file parquet reads), and
479
+ # (b) the original index is preserved exactly.
480
+ offsets = np.cumsum([0] + lengths)
481
+ delayed_parts = [dask.delayed(transformed_pd.iloc[offsets[i] : offsets[i + 1]]) for i in range(len(lengths))]
482
+ transformed = dd.from_delayed(delayed_parts, meta=transformed_pd.iloc[:0])
483
+ # Preserve spatialdata_attrs (feature_key, instance_key, …) from the original element;
484
+ # dd.from_delayed starts with empty attrs so we must copy them explicitly.
485
+ for k, v in data.attrs.items():
486
+ if k != TRANSFORM_KEY:
487
+ transformed.attrs[k] = v
488
+ # dummy transformation that will be replaced by _adjust_transformation()
489
+ default_cs = {DEFAULT_COORDINATE_SYSTEM: Identity()}
490
+ transformed.attrs[TRANSFORM_KEY] = default_cs
468
491
 
469
492
  old_transformations = cast(dict[str, Any], get_transformation(data, get_all=True))
470
493