spatial-aim 1.3.0__tar.gz → 1.3.1__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 (133) hide show
  1. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/PKG-INFO +1 -1
  2. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/PKG-INFO +1 -1
  3. spatial_aim-1.3.1/src/spatial_aim.egg-info/scm_version.json +8 -0
  4. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/scatter.py +54 -33
  5. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/state_names.py +67 -7
  6. spatial_aim-1.3.0/src/spatial_aim.egg-info/scm_version.json +0 -8
  7. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/.github/workflows/ci.yml +0 -0
  8. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/.github/workflows/release.yml +0 -0
  9. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/.gitignore +0 -0
  10. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/.pre-commit-config.yaml +0 -0
  11. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/LICENSE +0 -0
  12. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/README.md +0 -0
  13. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/docs/aim_gui.gif +0 -0
  14. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/docs/figure_2_1.png +0 -0
  15. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/environment.yml +0 -0
  16. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/pyproject.toml +0 -0
  17. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/renovate.json +0 -0
  18. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/sample_dataset/ST/index.csv +0 -0
  19. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/sample_dataset/ST/sample_st.h5ad +0 -0
  20. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/sample_dataset/pairs.csv +0 -0
  21. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/sample_dataset/scRNA/index.csv +0 -0
  22. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/sample_dataset/scRNA/sample_sc.h5ad +0 -0
  23. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/setup.cfg +0 -0
  24. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/SOURCES.txt +0 -0
  25. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/dependency_links.txt +0 -0
  26. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/entry_points.txt +0 -0
  27. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/requires.txt +0 -0
  28. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/scm_file_list.json +0 -0
  29. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatial_aim.egg-info/top_level.txt +0 -0
  30. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/__init__.py +0 -0
  31. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/__main__.py +0 -0
  32. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/adata_schema.py +0 -0
  33. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/aggregation.py +0 -0
  34. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/__init__.py +0 -0
  35. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/coherence.py +0 -0
  36. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/__init__.py +0 -0
  37. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/collect.py +0 -0
  38. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/criteria.py +0 -0
  39. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/run.py +0 -0
  40. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/scoring.py +0 -0
  41. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/compare_ks/selection.py +0 -0
  42. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/loading.py +0 -0
  43. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/reconstruction.py +0 -0
  44. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/run.py +0 -0
  45. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/shape_consistency.py +0 -0
  46. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/analysis/spatial_organization.py +0 -0
  47. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/cli.py +0 -0
  48. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/clustering.py +0 -0
  49. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/config.py +0 -0
  50. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/__init__.py +0 -0
  51. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/README.md +0 -0
  52. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/__init__.py +0 -0
  53. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/_common.py +0 -0
  54. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/_segmentation.py +0 -0
  55. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_01_mouse_ssp.py +0 -0
  56. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_02_colorectal_cancer.py +0 -0
  57. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_03_mouse_liver.py +0 -0
  58. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_04_mouse_kidney.py +0 -0
  59. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_05_mouse_embryo.py +0 -0
  60. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_06_mouse_retina.py +0 -0
  61. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_07_mouse_mpfc.py +0 -0
  62. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_08_mouse_visual_cortex.py +0 -0
  63. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_09_human_breast_cancer.py +0 -0
  64. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/curate_10_human_mtg.py +0 -0
  65. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/dump_retina_counts.R +0 -0
  66. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/curate_datasets/environment_retina_qs.yml +0 -0
  67. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/__init__.py +0 -0
  68. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/common.py +0 -0
  69. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/runner.py +0 -0
  70. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/validate_pair.py +0 -0
  71. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/validate_sc.py +0 -0
  72. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/validate_segmentation.py +0 -0
  73. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/data/validate/validate_st.py +0 -0
  74. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/__init__.py +0 -0
  75. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/__main__.py +0 -0
  76. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/app.py +0 -0
  77. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/compute.py +0 -0
  78. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/data_access.py +0 -0
  79. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/loaders.py +0 -0
  80. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/__init__.py +0 -0
  81. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/cell_type.py +0 -0
  82. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/common.py +0 -0
  83. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/headline.py +0 -0
  84. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/ksweep.py +0 -0
  85. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/markers.py +0 -0
  86. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/report.py +0 -0
  87. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/render/shape.py +0 -0
  88. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/scaffold.py +0 -0
  89. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/sidebar.py +0 -0
  90. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/texts.py +0 -0
  91. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/validation.py +0 -0
  92. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/__init__.py +0 -0
  93. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/cards.py +0 -0
  94. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/cell_type_modal.py +0 -0
  95. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/choose_k.py +0 -0
  96. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/compare_tab.py +0 -0
  97. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/controls.py +0 -0
  98. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/mapper_tab.py +0 -0
  99. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/reference_tab.py +0 -0
  100. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/views/report.py +0 -0
  101. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/widgets/__init__.py +0 -0
  102. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/widgets/headline_plot.py +0 -0
  103. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/widgets/linked_plot.py +0 -0
  104. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/widgets/plotly_fit.py +0 -0
  105. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/gui/widgets/slider.py +0 -0
  106. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/io.py +0 -0
  107. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/__init__.py +0 -0
  108. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/base.py +0 -0
  109. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/confidence.py +0 -0
  110. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/nearest_centroid.py +0 -0
  111. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/reference.py +0 -0
  112. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/mapping/wann.py +0 -0
  113. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/metrics/__init__.py +0 -0
  114. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/metrics/cossim.py +0 -0
  115. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/metrics/modularity.py +0 -0
  116. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/metrics/shape_consistency.py +0 -0
  117. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/metrics/topology.py +0 -0
  118. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/__init__.py +0 -0
  119. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/environment_dot.yml +0 -0
  120. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/environment_tacco.yml +0 -0
  121. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/environment_tangram.yml +0 -0
  122. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/registry.py +0 -0
  123. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/run_dot.R +0 -0
  124. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/run_dot.py +0 -0
  125. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/run_tacco.py +0 -0
  126. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/reference_aligners/run_tangram.py +0 -0
  127. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/sweep.py +0 -0
  128. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/tree.py +0 -0
  129. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/src/spatialaim/utils.py +0 -0
  130. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/tests/test_cli.py +0 -0
  131. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/tests/test_gui_imports.py +0 -0
  132. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/tests/test_registry.py +0 -0
  133. {spatial_aim-1.3.0 → spatial_aim-1.3.1}/tests/test_sweep_sample.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatial-aim
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: SpatialAIM — annotation-independent mapping of scRNA-seq references onto high-resolution spatial transcriptomics
5
5
  Author-email: Dominik Pysch <dominik.pysch@fau.de>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatial-aim
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: SpatialAIM — annotation-independent mapping of scRNA-seq references onto high-resolution spatial transcriptomics
5
5
  Author-email: Dominik Pysch <dominik.pysch@fau.de>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.3.1",
3
+ "distance": 0,
4
+ "node": "ga41027f02b0a96f69bf54115cca3fbeeedd3640c",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-15"
8
+ }
@@ -39,6 +39,13 @@ logger = logging.getLogger(__name__)
39
39
  # Grey (as a CSS rgba string) for spots drawn below the confidence threshold.
40
40
  _GREY_RGBA = "rgba(184,184,184,0.9)"
41
41
 
42
+ # The spatial panel draws each state in up to _MAX_CHUNKS interleaved chunks (see
43
+ # add_spatial_traces), capped at _MAX_STATE_TRACES traces in all: 20 chunks for a
44
+ # handful of states is all but a random order, while a K of 50 still gets 4
45
+ # without the panel turning into thousands of WebGL traces.
46
+ _MAX_CHUNKS = 20
47
+ _MAX_STATE_TRACES = 200
48
+
42
49
  # --- EXPERIMENTAL: per-state centroid markers on the UMAP panels --------------
43
50
  # Hand-set switch (see the question in the session that added this). When True,
44
51
  # each UMAP panel gets one diamond marker per state at the state's *expression
@@ -373,40 +380,54 @@ def add_spatial_traces(
373
380
  col=col,
374
381
  )
375
382
 
376
- for state in sorted(np.unique(hard[keep]).tolist()):
377
- mask = keep & (hard == state)
378
- show = state not in legend_shown
379
- legend_shown.add(state)
380
- genes = top_genes_suffix(top_genes, state)
381
- head = f"{state_label(state, names)}{genes}"
382
- if confidence is not None:
383
- customdata = confidence[mask][:, None]
384
- hovertemplate = (
385
- f"{head}<br>"
386
- "confidence %{customdata[0]:.3f}<br>"
387
- "x %{x:.1f} y %{y:.1f}<extra></extra>"
388
- )
389
- else:
390
- customdata = None
391
- hovertemplate = f"{head}<br>x %{{x:.1f}} y %{{y:.1f}}<extra></extra>"
392
- fig.add_trace(
393
- go.Scattergl(
394
- x=coords[mask, 0],
395
- y=coords[mask, 1],
396
- mode="markers",
397
- marker=dict(
398
- size=dot_size, color=hex_color(palette.get(state)), opacity=1.0
383
+ # Draw order decides which colour wins where spots overlap: one trace per
384
+ # state, stacked by state id, lets the last states cover the others however
385
+ # rare they are. So each state is split into random chunks and the chunks are
386
+ # drawn round-robin (state 0 chunk 0, state 1 chunk 0, ..., state 0 chunk 1,
387
+ # ...), which comes close to a random order spot by spot. Every chunk is still
388
+ # a trace in its state's legendgroup, so the headline widget's toggling,
389
+ # clicks and hover work unchanged; only the first chunk carries the legend
390
+ # entry. The seed is fixed, so a rerun draws the same picture.
391
+ states = sorted(np.unique(hard[keep]).tolist())
392
+ n_chunks = max(1, min(_MAX_CHUNKS, _MAX_STATE_TRACES // max(1, len(states))))
393
+ chunk = np.random.default_rng(0).integers(n_chunks, size=len(hard))
394
+ for c in range(n_chunks):
395
+ for state in states:
396
+ mask = keep & (hard == state) & (chunk == c)
397
+ if not mask.any():
398
+ continue
399
+ show = state not in legend_shown
400
+ legend_shown.add(state)
401
+ genes = top_genes_suffix(top_genes, state)
402
+ head = f"{state_label(state, names)}{genes}"
403
+ if confidence is not None:
404
+ customdata = confidence[mask][:, None]
405
+ hovertemplate = (
406
+ f"{head}<br>"
407
+ "confidence %{customdata[0]:.3f}<br>"
408
+ "x %{x:.1f} y %{y:.1f}<extra></extra>"
409
+ )
410
+ else:
411
+ customdata = None
412
+ hovertemplate = f"{head}<br>x %{{x:.1f}} y %{{y:.1f}}<extra></extra>"
413
+ fig.add_trace(
414
+ go.Scattergl(
415
+ x=coords[mask, 0],
416
+ y=coords[mask, 1],
417
+ mode="markers",
418
+ marker=dict(
419
+ size=dot_size, color=hex_color(palette.get(state)), opacity=1.0
420
+ ),
421
+ name=state_label(state, names),
422
+ legendgroup=f"state{state}",
423
+ showlegend=show,
424
+ meta="spatial",
425
+ customdata=customdata,
426
+ hovertemplate=hovertemplate,
399
427
  ),
400
- name=state_label(state, names),
401
- legendgroup=f"state{state}",
402
- showlegend=show,
403
- meta="spatial",
404
- customdata=customdata,
405
- hovertemplate=hovertemplate,
406
- ),
407
- row=row,
408
- col=col,
409
- )
428
+ row=row,
429
+ col=col,
430
+ )
410
431
 
411
432
  _pin_spatial_axes(fig, coords, row=row, col=col, equal_aspect=equal_aspect)
412
433
 
@@ -9,7 +9,11 @@ the same population whichever mapper placed it there. The names live in
9
9
  {"7": {"0": "Astrocytes", "3": "L2/3 IT"}, "12": {...}}
10
10
 
11
11
  Only the names actually set are stored; ``label`` falls back to ``Cell type <n>``
12
- for every other state, so a partly-named K is the normal case.
12
+ for every other state, so a partly-named K is the normal case. A run started
13
+ from an annotation (``--start_from_annotation``) is the exception at K = T (T =
14
+ the number of annotated types): there the tree cut is the identity, so every
15
+ state *is* one annotated type and defaults to that type's name
16
+ (``annotation_names``); a name set by hand still wins.
13
17
 
14
18
  This is the GUI's only write path onto disk. Nothing in the sweep or the analysis
15
19
  reads the file -- losing it costs the labels and nothing else. Because the key is
@@ -22,8 +26,15 @@ from __future__ import annotations
22
26
 
23
27
  import json
24
28
  import logging
29
+ from functools import lru_cache
25
30
  from pathlib import Path
26
31
 
32
+ import pandas as pd
33
+
34
+ from spatialaim.io import START_CLUSTER_TO_STATE_FILENAME, START_CLUSTERING_FILENAME
35
+
36
+ from . import data_access
37
+
27
38
  logger = logging.getLogger(__name__)
28
39
 
29
40
  FILENAME = "cell_type_names.json"
@@ -52,8 +63,48 @@ def load_all(output_dir: Path) -> dict[str, dict[str, str]]:
52
63
  return data if isinstance(data, dict) else {}
53
64
 
54
65
 
55
- def load(output_dir: Path, k: int) -> dict[int, str]:
56
- """``{state: name}`` for one K, holding only the states that have a name.
66
+ def annotation_names(output_dir: Path, k: int) -> dict[int, str]:
67
+ """``{state: annotated type}`` when this output dir's runs start from an
68
+ annotation and ``k`` is T, the number of annotated types -- the level where
69
+ the tree cut is the identity, so each state is exactly one annotated type.
70
+ Empty for every other K, for Leiden starts, and when no run root has the
71
+ files yet. All mappers cut the same tree, so any run root serves."""
72
+ if data_access.start_from_annotation_from_config(output_dir) is None:
73
+ return {}
74
+ for root in sorted(Path(output_dir).glob("*/")):
75
+ clustering = root / START_CLUSTERING_FILENAME
76
+ cut = data_access.k_dir(root, k) / START_CLUSTER_TO_STATE_FILENAME
77
+ if clustering.exists() and cut.exists():
78
+ # mtimes in the key: a rerun into the same dir invalidates the cache.
79
+ return dict(
80
+ _annotation_names(
81
+ str(clustering),
82
+ str(cut),
83
+ clustering.stat().st_mtime,
84
+ cut.stat().st_mtime,
85
+ )
86
+ )
87
+ return {}
88
+
89
+
90
+ @lru_cache(maxsize=32)
91
+ def _annotation_names(
92
+ clustering: str, cut: str, _m1: float, _m2: float
93
+ ) -> tuple[tuple[int, str], ...]:
94
+ """``annotation_names`` for one run root's files (cached per file version)."""
95
+ names = data_access.load_start_cluster_names(Path(clustering).parent)
96
+ states = pd.read_csv(cut)["state"].to_numpy(dtype=int)
97
+ if (
98
+ names is None
99
+ or len(states) != len(names)
100
+ or int(states.max()) + 1 != len(names)
101
+ ):
102
+ return () # not the identity level: K != T
103
+ return tuple((int(s), n) for s, n in zip(states, names) if n)
104
+
105
+
106
+ def _stored(output_dir: Path, k: int) -> dict[int, str]:
107
+ """``{state: name}`` for the names set by hand at this K.
57
108
 
58
109
  Tolerates hand-edits: entries with a non-numeric state or a blank name are
59
110
  skipped rather than surfacing as broken labels.
@@ -72,19 +123,28 @@ def load(output_dir: Path, k: int) -> dict[int, str]:
72
123
  return out
73
124
 
74
125
 
126
+ def load(output_dir: Path, k: int) -> dict[int, str]:
127
+ """``{state: name}`` for one K, holding only the states that have a name:
128
+ the annotated type names at K = T of an annotation start
129
+ (``annotation_names``), overridden by any name set by hand."""
130
+ return annotation_names(output_dir, k) | _stored(output_dir, k)
131
+
132
+
75
133
  def save(output_dir: Path, k: int, names: dict[int, str]) -> None:
76
134
  """Replace this K's names, leaving every other K in the file untouched.
77
135
 
78
- A blank name drops that state back to its default label instead of storing an
79
- empty string, and a K with nothing left set drops out of the file entirely.
136
+ A blank name -- or one equal to the state's annotated type (see
137
+ ``annotation_names``) -- drops that state back to its default label instead
138
+ of being stored, and a K with nothing left set drops out of the file entirely.
80
139
  The write goes through a temp file and an atomic replace, so an interrupted
81
140
  save cannot leave a half-written file that would read back as no names at all.
82
141
  """
83
142
  data = load_all(output_dir)
143
+ defaults = annotation_names(output_dir, k)
84
144
  cleaned = {
85
145
  str(int(s)): n.strip()
86
146
  for s, n in names.items()
87
- if isinstance(n, str) and n.strip()
147
+ if isinstance(n, str) and n.strip() and n.strip() != defaults.get(int(s))
88
148
  }
89
149
  if cleaned:
90
150
  data[str(int(k))] = cleaned
@@ -103,7 +163,7 @@ def set_name(output_dir: Path, k: int, state: int, name: str) -> None:
103
163
  """Set (or, when blank, clear) one state's name at this K, keeping the
104
164
  others -- the cell-type modal's write, through the same ``save`` as the
105
165
  all-states naming dialog."""
106
- names = load(output_dir, k)
166
+ names = _stored(output_dir, k)
107
167
  names[int(state)] = name
108
168
  save(output_dir, k, names)
109
169
 
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.3.0",
3
- "distance": 0,
4
- "node": "g3ff1552b03833c77cdbaada6b0efd4e446c91b6a",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-14"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes