Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ def _validate_against_anndata(dataset_id: str, fov_name: str, tracks: list[dict]
ds_meta = DATASETS[dataset_id]
emb_path = ds_meta["embedding_zarr"]
adata = ad.read_zarr(emb_path)
adata.obs_names_make_unique()
obs = adata.obs
fov_obs = obs[obs["fov_name"].astype(str) == fov_name]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ def _select_productive_from_zarr(
return pd.DataFrame(columns=OUTPUT_COLUMNS)

adata = ad.read_zarr(matches[0])
adata.obs_names_make_unique()
if pred_column not in adata.obs.columns:
_logger.warning(f"[{dataset_id}] {pred_column} not in {matches[0].name}; productive empty")
return pd.DataFrame(columns=OUTPUT_COLUMNS)
Expand Down Expand Up @@ -296,7 +295,6 @@ def _select_mock_from_zarr(
return pd.DataFrame(columns=OUTPUT_COLUMNS)

adata = ad.read_zarr(matches[0])
adata.obs_names_make_unique()
obs = adata.obs.copy()
obs = obs[obs["fov_name"].astype(str).str.contains(fov_pattern, regex=False)]
if obs.empty:
Expand Down Expand Up @@ -413,7 +411,6 @@ def _load_lc_predictions(
)

adata = ad.read_zarr(matches[0])
adata.obs_names_make_unique()
if pred_column not in adata.obs.columns:
_logger.warning(f"{pred_column} not in {matches[0]} .obs; LC fallback")
return pd.DataFrame()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ def _load_query_embeddings(
zarr_path = find_embedding_zarr(ds_cfg["pred_dir"], prefix + embedding_pattern)

adata = ad.read_zarr(zarr_path)
adata.obs_names_make_unique()

# FOV restriction from the dataset config (e.g. "C/2") — keeps us
# out of control wells unless the user explicitly wants them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ def _load_lc_predictions(
f"{[m.name for m in matches]}; using first"
)
adata = ad.read_zarr(matches[0])
adata.obs_names_make_unique()
if pred_column not in adata.obs.columns:
_logger.warning(f"{pred_column} not in {matches[0]} .obs")
return pd.DataFrame(columns=["fov_name", "track_id", "t", pred_column])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def load_organelle_embeddings(
_logger.warning(f"[{ds_id}] no embedding zarr matched {prefix + embedding_pattern}: {exc}")
continue
adata = ad.read_zarr(zarr_path)
adata.obs_names_make_unique()
out[ds_id] = adata
_logger.info(f"[{ds_id}] loaded {Path(zarr_path).name} ({adata.n_obs} cells)")
return out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def run_linear_classifiers(
raise FileNotFoundError(f"No .zarr files found in {embeddings_path}")
parts = [ad.read_zarr(p) for p in zarr_paths]
adata = ad.concat(parts, join="outer")
adata.obs_names_make_unique()
click.echo(f" Loaded {len(zarr_paths)} per-experiment zarrs")
else:
adata = ad.read_zarr(embeddings_path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,6 @@ def run_mmd_pooled(config: MMDPooledConfig) -> pd.DataFrame:

adatas = [ad.read_zarr(p) for p in config.input_paths]
combined = ad.concat(adatas, join="outer", label="source_experiment")
combined.obs_names_make_unique()

if config.obs_filter:
mask = pd.Series([True] * len(combined), index=combined.obs.index)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Callback for writing embeddings to zarr store."""

import logging
import uuid
from pathlib import Path
from typing import Any, Dict, Literal, Optional, Sequence

Expand Down Expand Up @@ -166,6 +167,11 @@ def write_embedding_dataset(
elif hasattr(s, "cat") and isinstance(s.cat.categories.dtype, pd.StringDtype):
ultrack_indices[col] = s.cat.rename_categories(s.cat.categories.astype(object))

# obs_names are an opaque, unique handle — never referenced directly (cell identity
# lives in the obs columns). Random UUIDs stay unique within a store and across any
# number of concatenated stores, so consumers never need obs_names_make_unique().
ultrack_indices.index = [str(uuid.uuid4()) for _ in range(len(ultrack_indices))]

if embedding_key == "projections":
if projections is None:
raise ValueError("embedding_key='projections' requires projections to be provided.")
Expand Down Expand Up @@ -273,7 +279,7 @@ def write_on_epoch_end(
"""Write predictions and dimensionality reductions to a zarr store."""
features = _move_and_stack_embeddings(predictions, "features")
projections = _move_and_stack_embeddings(predictions, "projections")
ultrack_indices = pd.concat([pd.DataFrame(p["index"]) for p in predictions])
ultrack_indices = pd.concat([pd.DataFrame(p["index"]) for p in predictions], ignore_index=True)

write_embedding_dataset(
output_path=self.output_path,
Expand Down
45 changes: 45 additions & 0 deletions packages/viscy-utils/tests/test_embedding_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import anndata as ad
import numpy as np
import pandas as pd

from viscy_utils.callbacks.embedding_writer import write_embedding_dataset


def _make_index_df(n: int, fov: str) -> pd.DataFrame:
# Mimic EmbeddingWriter.write_on_epoch_end: per-batch frames concatenated, so the
# positional index restarts each batch and the result has duplicate labels.
half = n // 2
batches = [
pd.DataFrame({"fov_name": [fov] * half, "track_id": range(half), "t": range(half)}),
pd.DataFrame(
{"fov_name": [fov] * (n - half), "track_id": range(n - half), "t": range(n - half)}
),
]
df = pd.concat(batches)
assert df.index.has_duplicates # precondition: the input that used to leak into obs_names
return df


def test_obs_names_unique_within_store(tmp_path):
n, d = 6, 4
features = np.random.default_rng(0).standard_normal((n, d)).astype(np.float32)
out = tmp_path / "store.zarr"

write_embedding_dataset(output_path=out, features=features, index_df=_make_index_df(n, "A/1"))

adata = ad.read_zarr(out)
assert adata.obs_names.is_unique


def test_obs_names_unique_across_concatenated_stores(tmp_path):
rng = np.random.default_rng(0)
n, d = 6, 4
paths = []
for i, fov in enumerate(["A/1", "B/2"]):
out = tmp_path / f"store_{i}.zarr"
features = rng.standard_normal((n, d)).astype(np.float32)
write_embedding_dataset(output_path=out, features=features, index_df=_make_index_df(n, fov))
paths.append(out)

combined = ad.concat([ad.read_zarr(p) for p in paths])
assert combined.obs_names.is_unique
Loading