zmap-tools 0.2.1__tar.gz → 0.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.
@@ -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
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
@@ -50,7 +50,7 @@ License-File: LICENSE
50
50
  Requires-Dist: anndata>=0.10
51
51
  Requires-Dist: scanpy>=1.9
52
52
  Requires-Dist: numpy>=1.24
53
- Requires-Dist: pandas>=2.0
53
+ Requires-Dist: pandas<3.0,>=2.0
54
54
  Requires-Dist: matplotlib>=3.7
55
55
  Requires-Dist: requests>=2.31
56
56
  Requires-Dist: scipy>=1.10
@@ -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"
8
8
  description = "API for working with ZMAP (Zebrafish Multi-Atlas Project)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "anndata>=0.10",
31
31
  "scanpy>=1.9",
32
32
  "numpy>=1.24",
33
- "pandas>=2.0",
33
+ "pandas>=2.0,<3.0",
34
34
  "matplotlib>=3.7",
35
35
  "requests>=2.31",
36
36
  "scipy>=1.10",
@@ -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()
@@ -991,7 +991,7 @@ def predict_labels_kNN(
991
991
  cache.get('query_basis') == query_basis and
992
992
  cache.get('metric') == metric and
993
993
  cache.get('n_neighbors') == n_neighbors and
994
- cache.get('mask_digest') == _mask_digest and
994
+ list(cache.get('mask_digest', [])) == list(_mask_digest) and
995
995
  cache.get('tissue_mode', 'none') == expected_cache_mode and
996
996
  cache.get('knn_backend_requested', 'auto') == knn_backend and
997
997
  cache.get('knn_device_requested', 'auto') == knn_device and
@@ -2229,6 +2229,17 @@ def aggregate_by_cluster(
2229
2229
 
2230
2230
  df = pd.DataFrame(records)
2231
2231
 
2232
+ # Ensure object columns are h5ad-serializable in uns:
2233
+ # - numeric columns demoted to object by pd.NA → coerce back to float (pd.NA → np.nan)
2234
+ # - string columns with pd.NA → fillna("") and cast to str
2235
+ _numeric_cols = {"top_fraction", "mean_prob", "margin", "second_fraction"}
2236
+ for col in df.columns:
2237
+ if df[col].dtype == object:
2238
+ if col in _numeric_cols:
2239
+ df[col] = pd.to_numeric(df[col], errors="coerce")
2240
+ else:
2241
+ df[col] = df[col].fillna("").astype(str)
2242
+
2232
2243
  if save_csv:
2233
2244
  os.makedirs(output_dir, exist_ok=True)
2234
2245
  out_path = os.path.join(output_dir, f"{label_space}_cluster_summary.csv")
@@ -2564,6 +2575,7 @@ def plot_embedding_with_ondata_labels(
2564
2575
  # ---- data/keys ----
2565
2576
  color_key: str = "ZMAP_Tissue_predicted",
2566
2577
  basis: str = "X_umap",
2578
+ ref_basis: str | None = None,
2567
2579
  filter_na: bool = True,
2568
2580
 
2569
2581
  # ---- palette handling ----
@@ -2579,12 +2591,14 @@ def plot_embedding_with_ondata_labels(
2579
2591
  # ---- figure style ----
2580
2592
  figsize: tuple[float, float] = (6, 6),
2581
2593
  dpi: int = 200,
2594
+ title_fontsize: float = 10,
2582
2595
 
2583
2596
  # ---- point style ----
2584
2597
  ref_size: float = 2,
2585
2598
  ref_alpha: float = 0.3,
2586
2599
  test_size: float = 2,
2587
2600
  test_alpha: float = 1.0,
2601
+ show_ref: bool = True,
2588
2602
  cmap: str = "jet",
2589
2603
  frameon: bool = False,
2590
2604
  sort_order: bool = True,
@@ -2625,9 +2639,10 @@ def plot_embedding_with_ondata_labels(
2625
2639
  Plot a query dataset overlaid on the reference embedding, with on-data labels
2626
2640
  and an optional vertical time distribution strip.
2627
2641
 
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
2642
+ Renders up to two layers: (1) optionally, the full reference embedding
2643
+ as a faint grey background for spatial context (controlled by
2644
+ ``show_ref``), and (2) the query cells colored by a predicted label
2645
+ column. Labels are drawn directly on the embedding using
2631
2646
  ``adjustText`` to minimize overlap. A vertical colorbar histogram of
2632
2647
  predicted developmental time (``ZMAP_time_id``) can optionally be added
2633
2648
  as a strip on the right side of the figure.
@@ -2642,7 +2657,12 @@ def plot_embedding_with_ondata_labels(
2642
2657
  Column in ``adata_test.obs`` containing the categorical labels to color
2643
2658
  and annotate. Typically a ``_predicted`` column from ``predict_labels_kNN``.
2644
2659
  basis : str, default ``"X_umap"``
2645
- ``obsm`` key used for the 2D embedding coordinates in both datasets.
2660
+ ``obsm`` key used for the 2D embedding coordinates in the query dataset.
2661
+ ref_basis : str or None, default ``None``
2662
+ ``obsm`` key for the reference background embedding. When ``None``,
2663
+ falls back to ``basis``. Useful when the query embedding lives in a
2664
+ different key than the reference (e.g. ``basis="X_umap_zmap"`` for
2665
+ the query while the reference uses ``"X_umap"``).
2646
2666
  filter_na : bool, default ``True``
2647
2667
  Drop query cells with ``NaN`` in ``color_key`` before plotting.
2648
2668
  palette : dict or None, default ``None``
@@ -2674,6 +2694,11 @@ def plot_embedding_with_ondata_labels(
2674
2694
  Scatter point size for query (projected) cells.
2675
2695
  test_alpha : float, default ``1.0``
2676
2696
  Opacity of query overlay points.
2697
+ show_ref : bool, default ``True``
2698
+ If ``True``, draw the reference embedding as a faint grey background
2699
+ for spatial context. If ``False``, only the query cells are plotted
2700
+ (useful when ``do_ingest=False``). When ``False``, ``adata_ref``
2701
+ may be ``None``.
2677
2702
  cmap : str, default ``"jet"``
2678
2703
  Colormap used for the reference background scatter.
2679
2704
  legend_loc : str, default ``"on data"``
@@ -2730,7 +2755,7 @@ def plot_embedding_with_ondata_labels(
2730
2755
  # Only attempt sync if there's no _color_map dict already available
2731
2756
  # (annotate_with_zmap populates _color_map from reference colormaps)
2732
2757
  cmap_dict_key = f"{base_obs}_color_map"
2733
- if cmap_dict_key not in adata_test_plot.uns:
2758
+ if cmap_dict_key not in adata_test_plot.uns and adata_ref is not None:
2734
2759
  try:
2735
2760
  sync_zmap_colors(adata_ref, obs_key=base_obs)
2736
2761
  sync_zmap_colors(adata_test_plot, obs_key=color_key, ref_adata=adata_ref, ref_obs_key=base_obs)
@@ -2795,6 +2820,9 @@ def plot_embedding_with_ondata_labels(
2795
2820
  ax_umap = None
2796
2821
  ax_strip = None
2797
2822
 
2823
+ # ---- Resolve ref_basis: defaults to basis if not provided ----
2824
+ ref_basis_use = ref_basis if ref_basis is not None else basis
2825
+
2798
2826
  # ---- figure + axes layout ----
2799
2827
  with plt.rc_context({'figure.figsize': figsize, 'figure.dpi': dpi}):
2800
2828
  if has_time:
@@ -2812,15 +2840,18 @@ def plot_embedding_with_ondata_labels(
2812
2840
  fig, ax_umap = plt.subplots()
2813
2841
  ax_strip = None
2814
2842
 
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
- )
2843
+ # ---- reference embedding in UMAP axis (optional) ----
2844
+ if show_ref:
2845
+ ax = sc.pl.embedding(
2846
+ adata_ref,
2847
+ basis=ref_basis_use,
2848
+ show=False,
2849
+ s=ref_size,
2850
+ ax=ax_umap,
2851
+ **ref_kwargs,
2852
+ )
2853
+ else:
2854
+ ax = ax_umap
2824
2855
 
2825
2856
  # ---- query overlay in UMAP axis ----
2826
2857
  ax = sc.pl.embedding(
@@ -2840,6 +2871,7 @@ def plot_embedding_with_ondata_labels(
2840
2871
  palette=palette,
2841
2872
  **test_kwargs,
2842
2873
  )
2874
+ ax.title.set_fontsize(title_fontsize)
2843
2875
 
2844
2876
  # --- on-data label styling (only when show_labels=True) ---
2845
2877
  if show_labels:
@@ -3117,20 +3149,26 @@ def map_query_labels(
3117
3149
  fig = None
3118
3150
  if show_plot or save_plots:
3119
3151
  plt.rcParams["axes.grid"] = False
3120
- fig, ax = plt.subplots(figsize=(figsize, figsize))
3152
+ n_rows, n_cols = plot_table.shape
3153
+ tick_fontsize = 8
3154
+ cell_size = 0.25 # inches per cell — fits 8pt labels without crowding
3155
+ fig_w = min(28, max(figsize, n_cols * cell_size))
3156
+ fig_h = min(28, max(figsize, n_rows * cell_size))
3157
+ cb_shrink = min(0.8, 3.0 / fig_h) # keep colorbar ~3 inches tall
3158
+ fig, ax = plt.subplots(figsize=(fig_w, fig_h))
3121
3159
 
3122
3160
  im = ax.imshow(plot_table.to_numpy(), cmap=cmap, vmin=vmin, vmax=vmax)
3123
3161
 
3124
3162
  ax.set_aspect("equal")
3125
3163
  ax.set_xticks(np.arange(plot_table.shape[1]))
3126
3164
  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)
3165
+ ax.set_xticklabels(plot_table.columns, rotation=90, fontsize=tick_fontsize)
3166
+ ax.set_yticklabels(plot_table.index, fontsize=tick_fontsize)
3129
3167
  ax.set_title(title)
3130
3168
  ax.set_xlabel(obs_A)
3131
3169
  ax.set_ylabel(obs_B)
3132
3170
 
3133
- cb = fig.colorbar(im, ax=ax, shrink=0.5)
3171
+ cb = fig.colorbar(im, ax=ax, shrink=cb_shrink)
3134
3172
  cb.ax.set_ylabel(colorbar_label)
3135
3173
 
3136
3174
  if overlay_values:
@@ -3354,7 +3392,7 @@ def annotate_with_zmap(
3354
3392
  - ``.obs[f"{label_space}_predicted"]`` — transferred cell labels.
3355
3393
  - ``.obs[f"{label_space}_prob"]`` — label confidence (0–1).
3356
3394
  - ``.obs["ZMAP_time_id_predicted"]`` — predicted time (hpf).
3357
- - ``.obsm["X_umap"]`` — UMAP coordinates (if ingested).
3395
+ - ``.obsm["X_umap_zmap"]`` — projected UMAP coordinates (if ingested).
3358
3396
  - ``.uns['zmap_labels']['_last_space']`` — most recent label_space.
3359
3397
  - ``.uns['zmap_labels'][label_space]['_run_config']`` — stored run parameters
3360
3398
  for zero-arg on-demand plot accessors.
@@ -3517,9 +3555,29 @@ def annotate_with_zmap(
3517
3555
  if do_ingest:
3518
3556
  if v >= 1:
3519
3557
  _zlog("Ingesting query into reference UMAP...")
3558
+
3559
+ # Stash pre-existing X_umap so symphony doesn't clobber it
3560
+ _had_existing_umap = "X_umap" in adata_query.obsm
3561
+ if _had_existing_umap:
3562
+ _existing_umap = adata_query.obsm["X_umap"].copy()
3563
+
3520
3564
  with warnings.catch_warnings():
3521
3565
  warnings.simplefilter("ignore", FutureWarning)
3522
3566
  sp.tl.ingest(adata_query=adata_query, adata_ref=adata_ref)
3567
+
3568
+ # Move ingested coords to X_umap_zmap (non-destructive)
3569
+ adata_query.obsm["X_umap_zmap"] = adata_query.obsm["X_umap"].copy()
3570
+
3571
+ # Restore original X_umap if one existed, otherwise remove
3572
+ if _had_existing_umap:
3573
+ adata_query.obsm["X_umap"] = _existing_umap
3574
+ if v >= 1:
3575
+ _zlog("Restored pre-existing X_umap; ingested coords in X_umap_zmap.")
3576
+ else:
3577
+ del adata_query.obsm["X_umap"]
3578
+ if v >= 1:
3579
+ _zlog("Ingested coords stored in X_umap_zmap.")
3580
+
3523
3581
  if v >= 1:
3524
3582
  _zlog("Ingestion complete.")
3525
3583
 
@@ -3593,15 +3651,26 @@ def annotate_with_zmap(
3593
3651
  adata_query.uns.setdefault("zmap_labels", {})
3594
3652
  adata_query.uns["zmap_labels"]["_last_space"] = space
3595
3653
  adata_query.uns["zmap_labels"].setdefault(space, {})
3654
+ # Determine embedding basis and whether to show reference background
3655
+ if do_ingest:
3656
+ _plot_basis = "X_umap_zmap"
3657
+ _show_ref = True
3658
+ else:
3659
+ # No ingest → query lives in its own UMAP space
3660
+ _plot_basis = "X_umap" if "X_umap" in adata_query.obsm else None
3661
+ _show_ref = False
3662
+
3596
3663
  adata_query.uns["zmap_labels"][space]["_run_config"] = {
3597
3664
  "label_space": space,
3598
3665
  "ref_label_col": ref_label_col,
3599
3666
  "query_label_col": query_label_col,
3600
3667
  "time_col": time_col_actual,
3601
3668
  "output_dir": space_dir,
3669
+ "basis": _plot_basis,
3670
+ "show_ref": _show_ref,
3602
3671
  }
3603
- # Store reference UMAP for on-demand plot_embedding (avoids needing adata_ref)
3604
- if "X_umap" in adata_ref.obsm:
3672
+ # Store reference UMAP for on-demand plot_embedding (only when ingested)
3673
+ if do_ingest and "X_umap" in adata_ref.obsm:
3605
3674
  adata_query.uns["zmap_labels"][space]["_ref_umap"] = (
3606
3675
  np.asarray(adata_ref.obsm["X_umap"]).copy()
3607
3676
  )
@@ -3691,15 +3760,24 @@ def annotate_with_zmap(
3691
3760
  # ------------------------------------------------------------------
3692
3761
  # 9. UMAP overlay figure (v >= 2)
3693
3762
  # ------------------------------------------------------------------
3694
- if v >= 2:
3763
+ if _plot_basis is None:
3764
+ if v >= 1:
3765
+ _zlog(
3766
+ "Skipping UMAP overlay: no embedding found "
3767
+ "(do_ingest=False and no pre-existing X_umap)."
3768
+ )
3769
+ elif v >= 2:
3695
3770
  try:
3696
3771
  if v >= 1:
3697
3772
  _zlog("Plotting UMAP overlay with predicted labels...")
3698
3773
  plot_embedding_with_ondata_labels(
3699
- adata_ref,
3774
+ adata_ref if _show_ref else None,
3700
3775
  adata_query,
3701
3776
  color_key=f"{space}_predicted",
3702
3777
  time_key=time_col_actual,
3778
+ basis=_plot_basis,
3779
+ ref_basis="X_umap",
3780
+ show_ref=_show_ref,
3703
3781
  show=True,
3704
3782
  save=save_outputs,
3705
3783
  output_dir=space_dir,
@@ -3714,10 +3792,13 @@ def annotate_with_zmap(
3714
3792
  # Still save the figure even if not showing it
3715
3793
  try:
3716
3794
  plot_embedding_with_ondata_labels(
3717
- adata_ref,
3795
+ adata_ref if _show_ref else None,
3718
3796
  adata_query,
3719
3797
  color_key=f"{space}_predicted",
3720
3798
  time_key=time_col_actual,
3799
+ basis=_plot_basis,
3800
+ ref_basis="X_umap",
3801
+ show_ref=_show_ref,
3721
3802
  show=False,
3722
3803
  save=True,
3723
3804
  output_dir=space_dir,
@@ -3985,6 +4066,7 @@ def validate_markers(
3985
4066
  groupby=groupby,
3986
4067
  method=method,
3987
4068
  corr_method=corr_method,
4069
+ use_raw=False,
3988
4070
  )
3989
4071
 
3990
4072
  # ---- Extract and filter DE results per group ----
@@ -4271,19 +4353,30 @@ def plot_embedding(
4271
4353
  label_space, config = _resolve_config(adata_query, label_space)
4272
4354
  space_store = adata_query.uns.get("zmap_labels", {}).get(label_space, {})
4273
4355
 
4274
- ref_umap = space_store.get("_ref_umap")
4275
- if ref_umap is None:
4356
+ # Resolve basis and show_ref from stored config
4357
+ plot_basis = config.get("basis", "X_umap_zmap")
4358
+ show_ref = config.get("show_ref", True)
4359
+
4360
+ if plot_basis is None:
4276
4361
  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."
4362
+ f"No embedding basis stored for label_space='{label_space}'. "
4363
+ "The original run had do_ingest=False and no pre-existing X_umap."
4280
4364
  )
4281
4365
 
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
4366
+ # Build reference shell only when needed
4367
+ adata_ref_mini = None
4368
+ if show_ref:
4369
+ ref_umap = space_store.get("_ref_umap")
4370
+ if ref_umap is None:
4371
+ raise KeyError(
4372
+ f"No stored reference UMAP found at "
4373
+ f"adata.uns['zmap_labels']['{label_space}']['_ref_umap']. "
4374
+ "Run annotate_with_zmap first."
4375
+ )
4376
+ adata_ref_mini = ad.AnnData(
4377
+ obs=pd.DataFrame(index=[f"ref_{i}" for i in range(ref_umap.shape[0])]),
4378
+ )
4379
+ adata_ref_mini.obsm["X_umap"] = ref_umap
4287
4380
 
4288
4381
  color_key = f"{label_space}_predicted"
4289
4382
  time_key = config.get("time_col", "ZMAP_time_id_predicted")
@@ -4294,6 +4387,9 @@ def plot_embedding(
4294
4387
  adata_query,
4295
4388
  color_key=color_key,
4296
4389
  time_key=time_key,
4390
+ basis=plot_basis,
4391
+ ref_basis="X_umap",
4392
+ show_ref=show_ref,
4297
4393
  show=True,
4298
4394
  save=save,
4299
4395
  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
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
@@ -50,7 +50,7 @@ License-File: LICENSE
50
50
  Requires-Dist: anndata>=0.10
51
51
  Requires-Dist: scanpy>=1.9
52
52
  Requires-Dist: numpy>=1.24
53
- Requires-Dist: pandas>=2.0
53
+ Requires-Dist: pandas<3.0,>=2.0
54
54
  Requires-Dist: matplotlib>=3.7
55
55
  Requires-Dist: requests>=2.31
56
56
  Requires-Dist: scipy>=1.10
@@ -1,7 +1,7 @@
1
1
  anndata>=0.10
2
2
  scanpy>=1.9
3
3
  numpy>=1.24
4
- pandas>=2.0
4
+ pandas<3.0,>=2.0
5
5
  matplotlib>=3.7
6
6
  requests>=2.31
7
7
  scipy>=1.10
File without changes
File without changes
File without changes