Current behavior
When working with AnnData.layers, applying a preprocessing transformation and storing the result in a new layer currently requires copying the source layer first and then modifying that copy in place.
For example, creating a normalized-counts layer requires:
adata.layers["normalized_counts"] = (
adata.layers["counts"].copy()
)
sc.pp.normalize_total(
adata,
layer="normalized_counts",
)
This works, but IMHO is excessively verbose and thus confusing.
Basically, the layer argument simultaneously identifies the input matrix and the matrix that will be modified. As a result, creating a new derived layer requires two separate operations:
- Copy the source layer manually.
- Run the transformation on the copied layer.
The current inplace=False behavior does not fully solve this ergonomically because it returns a dictionary rather than the transformed matrix itself:
result = sc.pp.normalize_total(
pca_adata,
layer="counts",
inplace=False,
)
pca_adata.layers["normalized_counts"] = result["X"]
I think this is still more cumbersome than necessary, and the "X" key is confusing when the input came from a named layer rather than from adata.X.
Suggested behavior
I think a more natural API would allow the transformed matrix to be returned directly:
# Preserve raw counts and create a normalized-expression layer
pca_adata.layers["normalized_counts"] = sc.pp.normalize_total(
pca_adata,
layer="counts",
)
or have an explicit output-layer argument:
sc.pp.normalize_total(
pca_adata,
layer="counts",
output_layer="normalized_counts",
)
This would make the data flow explicit:
counts layer → normalization → normalized_counts layer
What do you think? Am I overlooking an existing way to work exclusively with named layers, without relying on .X or making the code unnecessarily verbose?