Skip to content

Commit f52cb99

Browse files
Zethsonclaude
andauthored
perturbation_space: skip None-keyed layers in _combine (#1000)
sc.get.aggregate can leave a None-keyed layer on the AnnData it returns; PseudobulkSpace.compute strips it, but the _combine refactor in #994 added `key.endswith("_control_diff")` calls that crash on a stray None key (e.g. when callers hand-build the input). Filter to string keys before doing the string ops and the iteration that builds the new AnnData. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d498186 commit f52cb99

1 file changed

Lines changed: 19 additions & 8 deletions

File tree

pertpy/tools/_perturbation_space/_perturbation_space.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,28 @@ def _combine(
186186

187187
if ensure_consistency:
188188
adata = self.compute_control_diff(adata, copy=True, all_data=True, target_col=target_col)
189-
rename_back = {
190-
key: key.removesuffix("_control_diff")
191-
for key in [*adata.layers.keys(), *adata.obsm.keys()]
192-
if key.endswith("_control_diff")
193-
}
194189
else:
195190
warnings.warn(
196191
"Combining perturbations without `ensure_consistency=True` is only well-defined "
197192
"if the input was already differenced against control "
198193
"(otherwise perturbation - perturbation != control).",
199194
stacklevel=3,
200195
)
201-
rename_back = {}
196+
197+
# sc.get.aggregate can leave a `None`-keyed layer behind (the pre-aggregation .X);
198+
# PseudobulkSpace.compute strips it but defensively re-strip here so callers passing
199+
# a hand-built AnnData don't crash inside the string ops below.
200+
layer_keys = [k for k in adata.layers if isinstance(k, str)]
201+
obsm_keys = [k for k in adata.obsm if isinstance(k, str)]
202+
rename_back = (
203+
{
204+
key: key.removesuffix("_control_diff")
205+
for key in [*layer_keys, *obsm_keys]
206+
if key.endswith("_control_diff")
207+
}
208+
if ensure_consistency
209+
else {}
210+
)
202211

203212
def _running(values: np.ndarray) -> np.ndarray:
204213
result = values[adata.obs_names.get_loc(reference_key)].astype(float, copy=True)
@@ -207,13 +216,15 @@ def _running(values: np.ndarray) -> np.ndarray:
207216
return result
208217

209218
new_layers: dict[str, np.ndarray] = {}
210-
for layer_key, mat in adata.layers.items():
219+
for layer_key in layer_keys:
220+
mat = adata.layers[layer_key]
211221
new_layers[rename_back.get(layer_key, layer_key)] = np.concatenate(
212222
(np.asarray(mat), _running(np.asarray(mat))[None, :]), axis=0
213223
)
214224

215225
new_obsm: dict[str, np.ndarray] = {}
216-
for embedding_key, mat in adata.obsm.items():
226+
for embedding_key in obsm_keys:
227+
mat = adata.obsm[embedding_key]
217228
new_obsm[rename_back.get(embedding_key, embedding_key)] = np.concatenate(
218229
(np.asarray(mat), _running(np.asarray(mat))[None, :]), axis=0
219230
)

0 commit comments

Comments
 (0)