zmap-tools 0.2.1__tar.gz → 0.2.2.2__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 (21) hide show
  1. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/PKG-INFO +15 -11
  2. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/pyproject.toml +17 -11
  3. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/predict/knn_backend.py +93 -15
  4. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/predict/predict.py +142 -37
  5. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap_tools.egg-info/PKG-INFO +15 -11
  6. zmap_tools-0.2.2.2/src/zmap_tools.egg-info/requires.txt +17 -0
  7. zmap_tools-0.2.1/src/zmap_tools.egg-info/requires.txt +0 -13
  8. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/LICENSE +0 -0
  9. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/README.md +0 -0
  10. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/setup.cfg +0 -0
  11. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/__init__.py +0 -0
  12. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/dotplot/__init__.py +0 -0
  13. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/dotplot/dotplot_gene.py +0 -0
  14. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/dotplot/dotplot_group.py +0 -0
  15. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/predict/__init__.py +0 -0
  16. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/reference/__init__.py +0 -0
  17. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/reference/load_h5ad.py +0 -0
  18. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap/reference/markers.py +0 -0
  19. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap_tools.egg-info/SOURCES.txt +0 -0
  20. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap_tools.egg-info/dependency_links.txt +0 -0
  21. {zmap_tools-0.2.1 → zmap_tools-0.2.2.2}/src/zmap_tools.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zmap-tools
3
- Version: 0.2.1
3
+ Version: 0.2.2.2
4
4
  Summary: API for working with ZMAP (Zebrafish Multi-Atlas Project)
5
5
  Author-email: Daniel Wagner <Daniel.Wagner@ucsf.edu>
6
6
  License: BSD 3-Clause License
@@ -47,19 +47,23 @@ Classifier: Programming Language :: Python :: 3.12
47
47
  Requires-Python: >=3.10
48
48
  Description-Content-Type: text/markdown
49
49
  License-File: LICENSE
50
- Requires-Dist: anndata>=0.10
51
- Requires-Dist: scanpy>=1.9
52
- Requires-Dist: numpy>=1.24
53
- Requires-Dist: pandas>=2.0
54
- Requires-Dist: matplotlib>=3.7
50
+ Requires-Dist: anndata==0.12.18
51
+ Requires-Dist: scanpy==1.12.1
52
+ Requires-Dist: numpy==2.0.2
53
+ Requires-Dist: numba==0.60.0
54
+ Requires-Dist: llvmlite==0.43.0
55
+ Requires-Dist: pandas<3.0,>=2.0
56
+ Requires-Dist: matplotlib<4.0,>=3.7
55
57
  Requires-Dist: requests>=2.31
56
- Requires-Dist: scipy>=1.10
57
- Requires-Dist: scikit-learn>=1.3
58
- Requires-Dist: faiss-cpu>=1.8
59
- Requires-Dist: seaborn>=0.12
58
+ Requires-Dist: scipy==1.16.3
59
+ Requires-Dist: scikit-learn==1.6.1
60
+ Requires-Dist: faiss-cpu==1.14.3
61
+ Requires-Dist: seaborn==0.13.2
60
62
  Requires-Dist: tqdm>=4.66
61
63
  Requires-Dist: adjustText>=0.8
62
- Requires-Dist: symphonypy>=0.2
64
+ Requires-Dist: symphonypy==0.2.4
65
+ Requires-Dist: igraph==1.0.0
66
+ Requires-Dist: leidenalg==0.12.0
63
67
  Dynamic: license-file
64
68
 
65
69
  # zmap-tools
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zmap-tools"
7
- version = "0.2.1"
7
+ version = "0.2.2.2"
8
8
  description = "API for working with ZMAP (Zebrafish Multi-Atlas Project)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,19 +27,25 @@ classifiers = [
27
27
  ]
28
28
 
29
29
  dependencies = [
30
- "anndata>=0.10",
31
- "scanpy>=1.9",
32
- "numpy>=1.24",
33
- "pandas>=2.0",
34
- "matplotlib>=3.7",
30
+ "anndata==0.12.18",
31
+ "scanpy==1.12.1",
32
+
33
+ "numpy==2.0.2",
34
+ "numba==0.60.0",
35
+ "llvmlite==0.43.0",
36
+
37
+ "pandas>=2.0,<3.0",
38
+ "matplotlib>=3.7,<4.0",
35
39
  "requests>=2.31",
36
- "scipy>=1.10",
37
- "scikit-learn>=1.3",
38
- "faiss-cpu>=1.8",
39
- "seaborn>=0.12",
40
+ "scipy==1.16.3",
41
+ "scikit-learn==1.6.1",
42
+ "faiss-cpu==1.14.3",
43
+ "seaborn==0.13.2",
40
44
  "tqdm>=4.66",
41
45
  "adjustText>=0.8",
42
- "symphonypy>=0.2",
46
+ "symphonypy==0.2.4",
47
+ "igraph==1.0.0",
48
+ "leidenalg==0.12.0",
43
49
  ]
44
50
 
45
51
  [project.urls]
@@ -18,10 +18,12 @@ _FAISS_INDEX_CACHE_ORDER: list[tuple[str, str, int, int]] = []
18
18
 
19
19
 
20
20
  def _as_float32_c(x: np.ndarray) -> np.ndarray:
21
+ """Cast array to float32 and ensure C-contiguous memory layout (required by FAISS)."""
21
22
  return np.ascontiguousarray(np.asarray(x, dtype=np.float32))
22
23
 
23
24
 
24
25
  def _l2_normalize(x: np.ndarray) -> np.ndarray:
26
+ """Row-wise L2-normalize an array to unit vectors (used for cosine similarity via inner product)."""
25
27
  x = _as_float32_c(x)
26
28
  norms = np.linalg.norm(x, axis=1, keepdims=True)
27
29
  norms = np.where(norms == 0, 1.0, norms)
@@ -29,6 +31,7 @@ def _l2_normalize(x: np.ndarray) -> np.ndarray:
29
31
 
30
32
 
31
33
  def _parse_cuda_device(device: str) -> int:
34
+ """Parse a device string like 'cuda:1' into an integer GPU index. Returns 0 on failure."""
32
35
  d = str(device).strip().lower()
33
36
  if ":" in d:
34
37
  try:
@@ -44,6 +47,17 @@ def _search_sklearn(
44
47
  n_neighbors: int,
45
48
  metric: str,
46
49
  ) -> tuple[np.ndarray, np.ndarray]:
50
+ """
51
+ kNN search using scikit-learn's NearestNeighbors.
52
+
53
+ Used as the fallback when FAISS is unavailable or fails. Slower than FAISS
54
+ on large references but supports any sklearn-compatible metric.
55
+
56
+ Returns
57
+ -------
58
+ neighbor_indices : np.ndarray, shape (n_query, n_neighbors)
59
+ distances : np.ndarray, shape (n_query, n_neighbors)
60
+ """
47
61
  nn = NearestNeighbors(n_neighbors=n_neighbors, metric=metric)
48
62
  nn.fit(ref)
49
63
  distances, neighbor_indices = nn.kneighbors(query, return_distance=True)
@@ -59,6 +73,44 @@ def _search_faiss(
59
73
  nprobe: int | None = None,
60
74
  cache_key: str | None = None,
61
75
  ) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
76
+ """
77
+ kNN search using FAISS with optional GPU acceleration and index caching.
78
+
79
+ Builds an IVF-Flat index by default for fast approximate search on large references.
80
+ Falls back to an exact Flat index if IVF training fails. Cosine similarity is
81
+ implemented as inner product after L2-normalization; euclidean uses L2 distance.
82
+
83
+ Parameters
84
+ ----------
85
+ ref : np.ndarray, shape (n_ref, d)
86
+ Reference embedding matrix.
87
+ query : np.ndarray, shape (n_query, d)
88
+ Query embedding matrix.
89
+ n_neighbors : int
90
+ Number of nearest neighbors to return.
91
+ metric : {'euclidean', 'cosine'}
92
+ Distance metric.
93
+ device : str
94
+ Target device — 'cpu', 'auto', 'cuda', or 'cuda:<id>'.
95
+ nprobe : int, optional
96
+ Number of IVF cells to probe during search. Higher values increase recall
97
+ at the cost of speed. Defaults to DEFAULT_FAISS_NPROBE (16).
98
+ cache_key : str, optional
99
+ If provided, the built FAISS index is cached in-memory under this key and
100
+ reused on subsequent calls with matching (key, metric, n_ref, d). Up to
101
+ MAX_FAISS_INDEX_CACHE (16) indices are retained (LRU eviction).
102
+
103
+ Returns
104
+ -------
105
+ indices : np.ndarray, shape (n_query, n_neighbors), dtype int64
106
+ Indices into ref for each neighbor. Entries are -1 (set to NaN in distances)
107
+ when FAISS cannot fill all k neighbors (rare with Flat indices).
108
+ distances : np.ndarray, shape (n_query, n_neighbors), dtype float32
109
+ Euclidean distances or cosine-distance proxies (1 - cosine_similarity).
110
+ meta : dict
111
+ Diagnostic info: backend_used, device_used, index_type, nlist, nprobe,
112
+ cache_key_used, cache_hit.
113
+ """
62
114
  try:
63
115
  import faiss # type: ignore
64
116
  except Exception as e:
@@ -233,21 +285,47 @@ def knn_search(
233
285
  cache_key: str | None = None,
234
286
  ) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
235
287
  """
236
- Unified kNN search for ZMAP API.
237
-
238
- backend:
239
- - auto: try faiss first, fallback to sklearn
240
- - faiss: require faiss, fallback to sklearn with warning
241
- - sklearn: force sklearn
242
- device:
243
- - auto | cpu | cuda | cuda:<id>
244
- - used only by faiss backend
245
- nprobe:
246
- - IVF probe count for FAISS (None -> default)
247
- - used only by faiss backend
248
- cache_key:
249
- - optional key to reuse FAISS index build across repeated calls
250
- - used only by faiss backend
288
+ Unified kNN search supporting FAISS and scikit-learn backends.
289
+
290
+ Tries FAISS first (fast, approximate) and falls back to sklearn (exact) if
291
+ FAISS is unavailable or raises. Both backends return consistent output shapes
292
+ and distance semantics.
293
+
294
+ Parameters
295
+ ----------
296
+ ref : np.ndarray, shape (n_ref, d)
297
+ Reference embedding matrix.
298
+ query : np.ndarray, shape (n_query, d)
299
+ Query embedding matrix.
300
+ n_neighbors : int
301
+ Number of nearest neighbors to return per query point.
302
+ metric : {'cosine', 'euclidean'}, default 'cosine'
303
+ Distance metric. Cosine is computed as 1 - cosine_similarity.
304
+ backend : {'auto', 'faiss', 'sklearn'}, default 'auto'
305
+ Search backend. 'auto' tries FAISS first and falls back to sklearn.
306
+ 'faiss' also falls back to sklearn with a warning if FAISS fails.
307
+ 'sklearn' skips FAISS entirely.
308
+ device : {'auto', 'cpu', 'cuda', 'cuda:<id>'}, default 'auto'
309
+ Device for FAISS index. Ignored by the sklearn backend. 'auto' uses
310
+ GPU if FAISS GPU support is available, otherwise CPU.
311
+ nprobe : int, optional
312
+ Number of IVF partitions to probe during FAISS search. Higher values
313
+ improve recall at the cost of speed. Defaults to DEFAULT_FAISS_NPROBE.
314
+ Ignored by the sklearn backend and FAISS Flat indices.
315
+ cache_key : str, optional
316
+ If provided, the FAISS index built for `ref` is cached in-memory under
317
+ this key and reused on subsequent calls with the same (key, metric,
318
+ n_ref, d). Useful when the same reference is queried repeatedly.
319
+
320
+ Returns
321
+ -------
322
+ indices : np.ndarray, shape (n_query, n_neighbors), dtype int64
323
+ Row indices into `ref` for each neighbor, ordered by ascending distance.
324
+ distances : np.ndarray, shape (n_query, n_neighbors), dtype float32
325
+ Corresponding distances. NaN where FAISS returns invalid indices (-1).
326
+ meta : dict
327
+ Diagnostic info including backend_used, device_used, index_type, and
328
+ cache_hit. Useful for verifying which backend and device were actually used.
251
329
  """
252
330
  metric = str(metric).lower()
253
331
  backend_req = str(backend).lower()
@@ -232,6 +232,7 @@ def preprocess_adata_query(
232
232
  adata.X = np.log1p(X_tpm)
233
233
 
234
234
  # ---- 5. bookkeeping ----
235
+ adata.uns["log1p"] = {"base": None}
235
236
  adata.uns.setdefault("ZMAP_preprocessing", {})
236
237
  adata.uns["ZMAP_preprocessing"]["query"] = {
237
238
  "counts_source": counts_source,
@@ -991,7 +992,7 @@ def predict_labels_kNN(
991
992
  cache.get('query_basis') == query_basis and
992
993
  cache.get('metric') == metric and
993
994
  cache.get('n_neighbors') == n_neighbors and
994
- cache.get('mask_digest') == _mask_digest and
995
+ list(cache.get('mask_digest', [])) == list(_mask_digest) and
995
996
  cache.get('tissue_mode', 'none') == expected_cache_mode and
996
997
  cache.get('knn_backend_requested', 'auto') == knn_backend and
997
998
  cache.get('knn_device_requested', 'auto') == knn_device and
@@ -2229,6 +2230,17 @@ def aggregate_by_cluster(
2229
2230
 
2230
2231
  df = pd.DataFrame(records)
2231
2232
 
2233
+ # Ensure object columns are h5ad-serializable in uns:
2234
+ # - numeric columns demoted to object by pd.NA → coerce back to float (pd.NA → np.nan)
2235
+ # - string columns with pd.NA → fillna("") and cast to str
2236
+ _numeric_cols = {"top_fraction", "mean_prob", "margin", "second_fraction"}
2237
+ for col in df.columns:
2238
+ if df[col].dtype == object:
2239
+ if col in _numeric_cols:
2240
+ df[col] = pd.to_numeric(df[col], errors="coerce")
2241
+ else:
2242
+ df[col] = df[col].fillna("").astype(str)
2243
+
2232
2244
  if save_csv:
2233
2245
  os.makedirs(output_dir, exist_ok=True)
2234
2246
  out_path = os.path.join(output_dir, f"{label_space}_cluster_summary.csv")
@@ -2564,6 +2576,7 @@ def plot_embedding_with_ondata_labels(
2564
2576
  # ---- data/keys ----
2565
2577
  color_key: str = "ZMAP_Tissue_predicted",
2566
2578
  basis: str = "X_umap",
2579
+ ref_basis: str | None = None,
2567
2580
  filter_na: bool = True,
2568
2581
 
2569
2582
  # ---- palette handling ----
@@ -2579,12 +2592,14 @@ def plot_embedding_with_ondata_labels(
2579
2592
  # ---- figure style ----
2580
2593
  figsize: tuple[float, float] = (6, 6),
2581
2594
  dpi: int = 200,
2595
+ title_fontsize: float = 10,
2582
2596
 
2583
2597
  # ---- point style ----
2584
2598
  ref_size: float = 2,
2585
2599
  ref_alpha: float = 0.3,
2586
2600
  test_size: float = 2,
2587
2601
  test_alpha: float = 1.0,
2602
+ show_ref: bool = True,
2588
2603
  cmap: str = "jet",
2589
2604
  frameon: bool = False,
2590
2605
  sort_order: bool = True,
@@ -2625,9 +2640,10 @@ def plot_embedding_with_ondata_labels(
2625
2640
  Plot a query dataset overlaid on the reference embedding, with on-data labels
2626
2641
  and an optional vertical time distribution strip.
2627
2642
 
2628
- Renders two layers: (1) the full reference embedding as a faint grey
2629
- background for spatial context, and (2) the query cells colored by a
2630
- predicted label column. Labels are drawn directly on the embedding using
2643
+ Renders up to two layers: (1) optionally, the full reference embedding
2644
+ as a faint grey background for spatial context (controlled by
2645
+ ``show_ref``), and (2) the query cells colored by a predicted label
2646
+ column. Labels are drawn directly on the embedding using
2631
2647
  ``adjustText`` to minimize overlap. A vertical colorbar histogram of
2632
2648
  predicted developmental time (``ZMAP_time_id``) can optionally be added
2633
2649
  as a strip on the right side of the figure.
@@ -2642,7 +2658,12 @@ def plot_embedding_with_ondata_labels(
2642
2658
  Column in ``adata_test.obs`` containing the categorical labels to color
2643
2659
  and annotate. Typically a ``_predicted`` column from ``predict_labels_kNN``.
2644
2660
  basis : str, default ``"X_umap"``
2645
- ``obsm`` key used for the 2D embedding coordinates in both datasets.
2661
+ ``obsm`` key used for the 2D embedding coordinates in the query dataset.
2662
+ ref_basis : str or None, default ``None``
2663
+ ``obsm`` key for the reference background embedding. When ``None``,
2664
+ falls back to ``basis``. Useful when the query embedding lives in a
2665
+ different key than the reference (e.g. ``basis="X_umap_zmap"`` for
2666
+ the query while the reference uses ``"X_umap"``).
2646
2667
  filter_na : bool, default ``True``
2647
2668
  Drop query cells with ``NaN`` in ``color_key`` before plotting.
2648
2669
  palette : dict or None, default ``None``
@@ -2674,6 +2695,11 @@ def plot_embedding_with_ondata_labels(
2674
2695
  Scatter point size for query (projected) cells.
2675
2696
  test_alpha : float, default ``1.0``
2676
2697
  Opacity of query overlay points.
2698
+ show_ref : bool, default ``True``
2699
+ If ``True``, draw the reference embedding as a faint grey background
2700
+ for spatial context. If ``False``, only the query cells are plotted
2701
+ (useful when ``do_ingest=False``). When ``False``, ``adata_ref``
2702
+ may be ``None``.
2677
2703
  cmap : str, default ``"jet"``
2678
2704
  Colormap used for the reference background scatter.
2679
2705
  legend_loc : str, default ``"on data"``
@@ -2730,7 +2756,7 @@ def plot_embedding_with_ondata_labels(
2730
2756
  # Only attempt sync if there's no _color_map dict already available
2731
2757
  # (annotate_with_zmap populates _color_map from reference colormaps)
2732
2758
  cmap_dict_key = f"{base_obs}_color_map"
2733
- if cmap_dict_key not in adata_test_plot.uns:
2759
+ if cmap_dict_key not in adata_test_plot.uns and adata_ref is not None:
2734
2760
  try:
2735
2761
  sync_zmap_colors(adata_ref, obs_key=base_obs)
2736
2762
  sync_zmap_colors(adata_test_plot, obs_key=color_key, ref_adata=adata_ref, ref_obs_key=base_obs)
@@ -2795,6 +2821,9 @@ def plot_embedding_with_ondata_labels(
2795
2821
  ax_umap = None
2796
2822
  ax_strip = None
2797
2823
 
2824
+ # ---- Resolve ref_basis: defaults to basis if not provided ----
2825
+ ref_basis_use = ref_basis if ref_basis is not None else basis
2826
+
2798
2827
  # ---- figure + axes layout ----
2799
2828
  with plt.rc_context({'figure.figsize': figsize, 'figure.dpi': dpi}):
2800
2829
  if has_time:
@@ -2812,15 +2841,18 @@ def plot_embedding_with_ondata_labels(
2812
2841
  fig, ax_umap = plt.subplots()
2813
2842
  ax_strip = None
2814
2843
 
2815
- # ---- reference embedding in UMAP axis ----
2816
- ax = sc.pl.embedding(
2817
- adata_ref,
2818
- basis=basis,
2819
- show=False,
2820
- s=ref_size,
2821
- ax=ax_umap,
2822
- **ref_kwargs,
2823
- )
2844
+ # ---- reference embedding in UMAP axis (optional) ----
2845
+ if show_ref:
2846
+ ax = sc.pl.embedding(
2847
+ adata_ref,
2848
+ basis=ref_basis_use,
2849
+ show=False,
2850
+ s=ref_size,
2851
+ ax=ax_umap,
2852
+ **ref_kwargs,
2853
+ )
2854
+ else:
2855
+ ax = ax_umap
2824
2856
 
2825
2857
  # ---- query overlay in UMAP axis ----
2826
2858
  ax = sc.pl.embedding(
@@ -2840,6 +2872,7 @@ def plot_embedding_with_ondata_labels(
2840
2872
  palette=palette,
2841
2873
  **test_kwargs,
2842
2874
  )
2875
+ ax.title.set_fontsize(title_fontsize)
2843
2876
 
2844
2877
  # --- on-data label styling (only when show_labels=True) ---
2845
2878
  if show_labels:
@@ -3117,20 +3150,26 @@ def map_query_labels(
3117
3150
  fig = None
3118
3151
  if show_plot or save_plots:
3119
3152
  plt.rcParams["axes.grid"] = False
3120
- fig, ax = plt.subplots(figsize=(figsize, figsize))
3153
+ n_rows, n_cols = plot_table.shape
3154
+ tick_fontsize = 8
3155
+ cell_size = 0.25 # inches per cell — fits 8pt labels without crowding
3156
+ fig_w = min(28, max(figsize, n_cols * cell_size))
3157
+ fig_h = min(28, max(figsize, n_rows * cell_size))
3158
+ cb_shrink = min(0.8, 3.0 / fig_h) # keep colorbar ~3 inches tall
3159
+ fig, ax = plt.subplots(figsize=(fig_w, fig_h))
3121
3160
 
3122
3161
  im = ax.imshow(plot_table.to_numpy(), cmap=cmap, vmin=vmin, vmax=vmax)
3123
3162
 
3124
3163
  ax.set_aspect("equal")
3125
3164
  ax.set_xticks(np.arange(plot_table.shape[1]))
3126
3165
  ax.set_yticks(np.arange(plot_table.shape[0]))
3127
- ax.set_xticklabels(plot_table.columns, rotation=90)
3128
- ax.set_yticklabels(plot_table.index)
3166
+ ax.set_xticklabels(plot_table.columns, rotation=90, fontsize=tick_fontsize)
3167
+ ax.set_yticklabels(plot_table.index, fontsize=tick_fontsize)
3129
3168
  ax.set_title(title)
3130
3169
  ax.set_xlabel(obs_A)
3131
3170
  ax.set_ylabel(obs_B)
3132
3171
 
3133
- cb = fig.colorbar(im, ax=ax, shrink=0.5)
3172
+ cb = fig.colorbar(im, ax=ax, shrink=cb_shrink)
3134
3173
  cb.ax.set_ylabel(colorbar_label)
3135
3174
 
3136
3175
  if overlay_values:
@@ -3228,7 +3267,7 @@ def annotate_with_zmap(
3228
3267
  predict_kwargs: Mapping[str, Any] | None = None,
3229
3268
 
3230
3269
  # --- output controls ---
3231
- verbosity: int = 2, # 0=silent, 1=progress, 2=summary+UMAP+QC, 3=full
3270
+ verbosity: int = 3, # 0=silent, 1=progress, 2=summary+UMAP+QC, 3=full
3232
3271
  debug: bool = False, # if True, re-raise exceptions instead of catching
3233
3272
  print_summary: bool | None = None, # deprecated → use verbosity
3234
3273
  show_plots: bool | None = None, # deprecated → use verbosity
@@ -3324,7 +3363,7 @@ def annotate_with_zmap(
3324
3363
  Extra keyword arguments forwarded to ``predict_labels_kNN``.
3325
3364
  For common options, prefer the top-level ``tissue_aware`` and
3326
3365
  ``evaluate`` parameters instead of passing dicts manually.
3327
- verbosity : int, default ``2``
3366
+ verbosity : int, default ``3``
3328
3367
  Controls how much output is printed and displayed inline:
3329
3368
 
3330
3369
  - ``0`` — silent (no print, no inline plots).
@@ -3354,7 +3393,7 @@ def annotate_with_zmap(
3354
3393
  - ``.obs[f"{label_space}_predicted"]`` — transferred cell labels.
3355
3394
  - ``.obs[f"{label_space}_prob"]`` — label confidence (0–1).
3356
3395
  - ``.obs["ZMAP_time_id_predicted"]`` — predicted time (hpf).
3357
- - ``.obsm["X_umap"]`` — UMAP coordinates (if ingested).
3396
+ - ``.obsm["X_umap_zmap"]`` — projected UMAP coordinates (if ingested).
3358
3397
  - ``.uns['zmap_labels']['_last_space']`` — most recent label_space.
3359
3398
  - ``.uns['zmap_labels'][label_space]['_run_config']`` — stored run parameters
3360
3399
  for zero-arg on-demand plot accessors.
@@ -3517,9 +3556,29 @@ def annotate_with_zmap(
3517
3556
  if do_ingest:
3518
3557
  if v >= 1:
3519
3558
  _zlog("Ingesting query into reference UMAP...")
3559
+
3560
+ # Stash pre-existing X_umap so symphony doesn't clobber it
3561
+ _had_existing_umap = "X_umap" in adata_query.obsm
3562
+ if _had_existing_umap:
3563
+ _existing_umap = adata_query.obsm["X_umap"].copy()
3564
+
3520
3565
  with warnings.catch_warnings():
3521
3566
  warnings.simplefilter("ignore", FutureWarning)
3522
3567
  sp.tl.ingest(adata_query=adata_query, adata_ref=adata_ref)
3568
+
3569
+ # Move ingested coords to X_umap_zmap (non-destructive)
3570
+ adata_query.obsm["X_umap_zmap"] = adata_query.obsm["X_umap"].copy()
3571
+
3572
+ # Restore original X_umap if one existed, otherwise remove
3573
+ if _had_existing_umap:
3574
+ adata_query.obsm["X_umap"] = _existing_umap
3575
+ if v >= 1:
3576
+ _zlog("Restored pre-existing X_umap; ingested coords in X_umap_zmap.")
3577
+ else:
3578
+ del adata_query.obsm["X_umap"]
3579
+ if v >= 1:
3580
+ _zlog("Ingested coords stored in X_umap_zmap.")
3581
+
3523
3582
  if v >= 1:
3524
3583
  _zlog("Ingestion complete.")
3525
3584
 
@@ -3593,15 +3652,26 @@ def annotate_with_zmap(
3593
3652
  adata_query.uns.setdefault("zmap_labels", {})
3594
3653
  adata_query.uns["zmap_labels"]["_last_space"] = space
3595
3654
  adata_query.uns["zmap_labels"].setdefault(space, {})
3655
+ # Determine embedding basis and whether to show reference background
3656
+ if do_ingest:
3657
+ _plot_basis = "X_umap_zmap"
3658
+ _show_ref = True
3659
+ else:
3660
+ # No ingest → query lives in its own UMAP space
3661
+ _plot_basis = "X_umap" if "X_umap" in adata_query.obsm else None
3662
+ _show_ref = False
3663
+
3596
3664
  adata_query.uns["zmap_labels"][space]["_run_config"] = {
3597
3665
  "label_space": space,
3598
3666
  "ref_label_col": ref_label_col,
3599
3667
  "query_label_col": query_label_col,
3600
3668
  "time_col": time_col_actual,
3601
3669
  "output_dir": space_dir,
3670
+ "basis": _plot_basis,
3671
+ "show_ref": _show_ref,
3602
3672
  }
3603
- # Store reference UMAP for on-demand plot_embedding (avoids needing adata_ref)
3604
- if "X_umap" in adata_ref.obsm:
3673
+ # Store reference UMAP for on-demand plot_embedding (only when ingested)
3674
+ if do_ingest and "X_umap" in adata_ref.obsm:
3605
3675
  adata_query.uns["zmap_labels"][space]["_ref_umap"] = (
3606
3676
  np.asarray(adata_ref.obsm["X_umap"]).copy()
3607
3677
  )
@@ -3668,6 +3738,7 @@ def annotate_with_zmap(
3668
3738
 
3669
3739
  # ------------------------------------------------------------------
3670
3740
  # 8. Copy colormap dict from reference → query (order-independent)
3741
+ # Also copy reference color palettes and order lists
3671
3742
  # ------------------------------------------------------------------
3672
3743
  cmap_src_key = _COLORMAP_UNS_KEY.get(ref_label_col)
3673
3744
  if cmap_src_key and cmap_src_key in adata_ref.uns:
@@ -3687,19 +3758,35 @@ def annotate_with_zmap(
3687
3758
  "arrays — palette may be incorrect if query categories differ from reference.",
3688
3759
  stacklevel=2,
3689
3760
  )
3761
+
3762
+ # Standard palettes
3763
+ keys = ["ZMAP_CellType_order","ZMAP_Tissue_order","ZMAP_colormap_C12","ZMAP_colormap_C79",
3764
+ "ZMAP_colormap_G7","ZMAP_colormap_T28","ZMAP_colormap_T7"]
3765
+ for k in keys:
3766
+ if k in adata_ref.uns:
3767
+ adata_query.uns[k] = adata_ref.uns[k]
3690
3768
 
3691
3769
  # ------------------------------------------------------------------
3692
3770
  # 9. UMAP overlay figure (v >= 2)
3693
3771
  # ------------------------------------------------------------------
3694
- if v >= 2:
3772
+ if _plot_basis is None:
3773
+ if v >= 1:
3774
+ _zlog(
3775
+ "Skipping UMAP overlay: no embedding found "
3776
+ "(do_ingest=False and no pre-existing X_umap)."
3777
+ )
3778
+ elif v >= 2:
3695
3779
  try:
3696
3780
  if v >= 1:
3697
3781
  _zlog("Plotting UMAP overlay with predicted labels...")
3698
3782
  plot_embedding_with_ondata_labels(
3699
- adata_ref,
3783
+ adata_ref if _show_ref else None,
3700
3784
  adata_query,
3701
3785
  color_key=f"{space}_predicted",
3702
3786
  time_key=time_col_actual,
3787
+ basis=_plot_basis,
3788
+ ref_basis="X_umap",
3789
+ show_ref=_show_ref,
3703
3790
  show=True,
3704
3791
  save=save_outputs,
3705
3792
  output_dir=space_dir,
@@ -3714,10 +3801,13 @@ def annotate_with_zmap(
3714
3801
  # Still save the figure even if not showing it
3715
3802
  try:
3716
3803
  plot_embedding_with_ondata_labels(
3717
- adata_ref,
3804
+ adata_ref if _show_ref else None,
3718
3805
  adata_query,
3719
3806
  color_key=f"{space}_predicted",
3720
3807
  time_key=time_col_actual,
3808
+ basis=_plot_basis,
3809
+ ref_basis="X_umap",
3810
+ show_ref=_show_ref,
3721
3811
  show=False,
3722
3812
  save=True,
3723
3813
  output_dir=space_dir,
@@ -3985,6 +4075,7 @@ def validate_markers(
3985
4075
  groupby=groupby,
3986
4076
  method=method,
3987
4077
  corr_method=corr_method,
4078
+ use_raw=False,
3988
4079
  )
3989
4080
 
3990
4081
  # ---- Extract and filter DE results per group ----
@@ -4271,19 +4362,30 @@ def plot_embedding(
4271
4362
  label_space, config = _resolve_config(adata_query, label_space)
4272
4363
  space_store = adata_query.uns.get("zmap_labels", {}).get(label_space, {})
4273
4364
 
4274
- ref_umap = space_store.get("_ref_umap")
4275
- if ref_umap is None:
4365
+ # Resolve basis and show_ref from stored config
4366
+ plot_basis = config.get("basis", "X_umap_zmap")
4367
+ show_ref = config.get("show_ref", True)
4368
+
4369
+ if plot_basis is None:
4276
4370
  raise KeyError(
4277
- f"No stored reference UMAP found at "
4278
- f"adata.uns['zmap_labels']['{label_space}']['_ref_umap']. "
4279
- "Run annotate_with_zmap first."
4371
+ f"No embedding basis stored for label_space='{label_space}'. "
4372
+ "The original run had do_ingest=False and no pre-existing X_umap."
4280
4373
  )
4281
4374
 
4282
- # Build a minimal AnnData shell with just the reference UMAP
4283
- adata_ref_mini = ad.AnnData(
4284
- obs=pd.DataFrame(index=[f"ref_{i}" for i in range(ref_umap.shape[0])]),
4285
- )
4286
- adata_ref_mini.obsm["X_umap"] = ref_umap
4375
+ # Build reference shell only when needed
4376
+ adata_ref_mini = None
4377
+ if show_ref:
4378
+ ref_umap = space_store.get("_ref_umap")
4379
+ if ref_umap is None:
4380
+ raise KeyError(
4381
+ f"No stored reference UMAP found at "
4382
+ f"adata.uns['zmap_labels']['{label_space}']['_ref_umap']. "
4383
+ "Run annotate_with_zmap first."
4384
+ )
4385
+ adata_ref_mini = ad.AnnData(
4386
+ obs=pd.DataFrame(index=[f"ref_{i}" for i in range(ref_umap.shape[0])]),
4387
+ )
4388
+ adata_ref_mini.obsm["X_umap"] = ref_umap
4287
4389
 
4288
4390
  color_key = f"{label_space}_predicted"
4289
4391
  time_key = config.get("time_col", "ZMAP_time_id_predicted")
@@ -4294,6 +4396,9 @@ def plot_embedding(
4294
4396
  adata_query,
4295
4397
  color_key=color_key,
4296
4398
  time_key=time_key,
4399
+ basis=plot_basis,
4400
+ ref_basis="X_umap",
4401
+ show_ref=show_ref,
4297
4402
  show=True,
4298
4403
  save=save,
4299
4404
  show_labels=show_labels,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zmap-tools
3
- Version: 0.2.1
3
+ Version: 0.2.2.2
4
4
  Summary: API for working with ZMAP (Zebrafish Multi-Atlas Project)
5
5
  Author-email: Daniel Wagner <Daniel.Wagner@ucsf.edu>
6
6
  License: BSD 3-Clause License
@@ -47,19 +47,23 @@ Classifier: Programming Language :: Python :: 3.12
47
47
  Requires-Python: >=3.10
48
48
  Description-Content-Type: text/markdown
49
49
  License-File: LICENSE
50
- Requires-Dist: anndata>=0.10
51
- Requires-Dist: scanpy>=1.9
52
- Requires-Dist: numpy>=1.24
53
- Requires-Dist: pandas>=2.0
54
- Requires-Dist: matplotlib>=3.7
50
+ Requires-Dist: anndata==0.12.18
51
+ Requires-Dist: scanpy==1.12.1
52
+ Requires-Dist: numpy==2.0.2
53
+ Requires-Dist: numba==0.60.0
54
+ Requires-Dist: llvmlite==0.43.0
55
+ Requires-Dist: pandas<3.0,>=2.0
56
+ Requires-Dist: matplotlib<4.0,>=3.7
55
57
  Requires-Dist: requests>=2.31
56
- Requires-Dist: scipy>=1.10
57
- Requires-Dist: scikit-learn>=1.3
58
- Requires-Dist: faiss-cpu>=1.8
59
- Requires-Dist: seaborn>=0.12
58
+ Requires-Dist: scipy==1.16.3
59
+ Requires-Dist: scikit-learn==1.6.1
60
+ Requires-Dist: faiss-cpu==1.14.3
61
+ Requires-Dist: seaborn==0.13.2
60
62
  Requires-Dist: tqdm>=4.66
61
63
  Requires-Dist: adjustText>=0.8
62
- Requires-Dist: symphonypy>=0.2
64
+ Requires-Dist: symphonypy==0.2.4
65
+ Requires-Dist: igraph==1.0.0
66
+ Requires-Dist: leidenalg==0.12.0
63
67
  Dynamic: license-file
64
68
 
65
69
  # zmap-tools
@@ -0,0 +1,17 @@
1
+ anndata==0.12.18
2
+ scanpy==1.12.1
3
+ numpy==2.0.2
4
+ numba==0.60.0
5
+ llvmlite==0.43.0
6
+ pandas<3.0,>=2.0
7
+ matplotlib<4.0,>=3.7
8
+ requests>=2.31
9
+ scipy==1.16.3
10
+ scikit-learn==1.6.1
11
+ faiss-cpu==1.14.3
12
+ seaborn==0.13.2
13
+ tqdm>=4.66
14
+ adjustText>=0.8
15
+ symphonypy==0.2.4
16
+ igraph==1.0.0
17
+ leidenalg==0.12.0
@@ -1,13 +0,0 @@
1
- anndata>=0.10
2
- scanpy>=1.9
3
- numpy>=1.24
4
- pandas>=2.0
5
- matplotlib>=3.7
6
- requests>=2.31
7
- scipy>=1.10
8
- scikit-learn>=1.3
9
- faiss-cpu>=1.8
10
- seaborn>=0.12
11
- tqdm>=4.66
12
- adjustText>=0.8
13
- symphonypy>=0.2
File without changes
File without changes
File without changes