From 591afdcc058b4a833b311b5b16bab1bca0c9b4b8 Mon Sep 17 00:00:00 2001 From: luisabender Date: Thu, 13 Aug 2026 12:24:35 +0200 Subject: [PATCH 1/4] added moran's I metric --- src/metrics/morans_i/config.vsh.yaml | 97 ++++++++++++++++++++++++++++ src/metrics/morans_i/script.py | 78 ++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/metrics/morans_i/config.vsh.yaml create mode 100644 src/metrics/morans_i/script.py diff --git a/src/metrics/morans_i/config.vsh.yaml b/src/metrics/morans_i/config.vsh.yaml new file mode 100644 index 0000000..3ab8b03 --- /dev/null +++ b/src/metrics/morans_i/config.vsh.yaml @@ -0,0 +1,97 @@ +__merge__: ../../api/comp_metric.yaml + +name: morans_i + + + +# Metadata for your component +info: + metrics: + - name: morans_i + label: Moran's I + summary: "Measures the spatial autocorrelation of the inferred pseudotime across the spatial coordinates of the cells/spots." + description: | + Computes Moran's I statistic for the inferred pseudotime, using a k-nearest-neighbour + graph built on the spatial coordinates (`obsm['X_spatial']`) of the cells/spots. + + Moran's I quantifies how similar the pseudotime values of spatially neighbouring + cells/spots are. Values close to 1 indicate a spatially smooth trajectory in which + neighbouring cells receive similar pseudotime values, values around 0 indicate a + spatially random assignment, and negative values indicate that neighbouring cells + receive dissimilar pseudotime values. + + Note that this metric evaluates the spatial coherence of the prediction only and does + not compare it against the ground-truth pseudotime; a spatially smooth but incorrect + ordering can still score highly. + + references: + doi: + - 10.2307/2332142 + bibtex: | + @article{Moran_1950, + author = {Moran, P. A. P.}, + title = {Notes on Continuous Stochastic Phenomena}, + journal = {Biometrika}, + volume = {37}, + number = {1/2}, + pages = {17--23}, + year = {1950}, + doi = {10.2307/2332142} + } + + links: + + documentation: https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.metrics.morans_i.html + + repository: https://github.com/scverse/scanpy + # The minimum possible value for this metric (required) + min: -1 + # The maximum possible value for this metric (required) + max: 1 + # Whether a higher value represents a 'better' solution (required) + maximize: true + +# Component-specific parameters (optional) +arguments: + - name: "--n_neighbors" + type: "integer" + default: 6 + description: Number of spatial neighbours used to build the k-NN graph. + +# Resources required to run the component +resources: + # The script of your component (required) + - type: python_script + path: script.py + # Additional resources your script needs (optional) + # - type: file + # path: weights.pt + +engines: + # Specifications for the Docker image for this component. + - type: docker + image: python:3.11-slim + setup: + - type: apt + packages: + - procps + - git + - type: python + packages: + - anndata~=0.10.0 + - scanpy~=1.10.0 + - scipy + - pandas + - pyyaml + - requests + - jsonschema + github: + - "openproblems-bio/core#subdirectory=packages/python/openproblems" + +runners: + # This platform allows running the component natively + - type: executable + # Allows turning the component into a Nextflow module / pipeline. + - type: nextflow + directives: + label: [midtime,midmem,midcpu] diff --git a/src/metrics/morans_i/script.py b/src/metrics/morans_i/script.py new file mode 100644 index 0000000..c47e404 --- /dev/null +++ b/src/metrics/morans_i/script.py @@ -0,0 +1,78 @@ +import anndata as ad +import numpy as np +import pandas as pd +import scanpy as sc +from scanpy.metrics import morans_i + +## VIASH START +# Note: this section is auto-generated by viash at runtime. To edit it, make changes +# in config.vsh.yaml and then run `viash config inject config.vsh.yaml`. +par = { + 'input_solution': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/solution.h5ad', + 'input_prediction': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/prediction.h5ad', + 'output': 'output.h5ad', + 'n_neighbors': 6, +} +meta = { + 'name': 'morans_i' +} +## VIASH END + + +def calc_morans_i(adata, pt_col, coords_key, n_neighbors=6): + """Spatial autocorrelation of pseudotime via Moran's I.""" + try: + sc.pp.neighbors(adata, use_rep=coords_key, n_neighbors=n_neighbors, key_added="spatial_neighbors") + pt = pd.to_numeric(adata.obs[pt_col], errors="coerce").values + + return float(morans_i(adata.obsp["spatial_neighbors_connectivities"], pt)) + except Exception as e: + print(f"Moran's I skipped for {pt_col}: {e}") + return np.nan + +# read input data +print('Reading input files', flush=True) +input_solution = ad.read_h5ad(par['input_solution']) +input_prediction = ad.read_h5ad(par['input_prediction']) + +assert (input_prediction.obs_names == input_solution.obs_names).all(), "obs_names not the same in prediction and solution inputs" + +# inferred pseudotime and spatial coordinates +INFERRED_COL = "pseudotime_inferred" +COORDS_KEY = "X_spatial" + +# spatial coordinates in the solution, the pseudotime in the prediction +adata = ad.AnnData( + obs=pd.DataFrame( + {INFERRED_COL: input_prediction.obs[INFERRED_COL].values}, + index=input_solution.obs_names, + ), + obsm={COORDS_KEY: np.asarray(input_solution.obsm[COORDS_KEY])}, +) + +# generate results +print('Compute metrics', flush=True) +# metric_ids and metric_values can have length > 1 +# but should be of equal length + +score = calc_morans_i(adata, INFERRED_COL, COORDS_KEY, n_neighbors=par['n_neighbors']) + +uns_metric_ids = [ 'morans_i' ] +uns_metric_values = [ score ] + +# Write output data to file +print("Write output AnnData to file...", flush=True) + +output = ad.AnnData( + obs=pd.DataFrame(index=pd.Index(np.array([], dtype=str))), + var=pd.DataFrame(index=pd.Index(np.array([], dtype=str))), + uns={ + 'dataset_id': input_solution.uns.get('dataset_id', 'unknown'), + 'normalization_id': input_solution.uns.get('normalization_id', 'unknown'), + 'method_id': input_prediction.uns.get('method_id', 'unknown'), + 'metric_ids': uns_metric_ids, + 'metric_values': uns_metric_values, + } +) + +output.write_h5ad(par['output'], compression='gzip') From 3ecc194e51513a22937c3d954f01db3ffa297bbe Mon Sep 17 00:00:00 2001 From: luisabender Date: Thu, 13 Aug 2026 12:58:49 +0200 Subject: [PATCH 2/4] morans I config and changelog --- CHANGELOG.md | 1 + src/metrics/morans_i/config.vsh.yaml | 16 +--------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3d361..75de7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Added `spaTrack` method (PR #4). * Added `Spearman's correlation` metric (PR #5). +* Added `Moran's I`metric (PR #6) ## MAJOR CHANGES diff --git a/src/metrics/morans_i/config.vsh.yaml b/src/metrics/morans_i/config.vsh.yaml index 3ab8b03..8250796 100644 --- a/src/metrics/morans_i/config.vsh.yaml +++ b/src/metrics/morans_i/config.vsh.yaml @@ -2,9 +2,6 @@ __merge__: ../../api/comp_metric.yaml name: morans_i - - -# Metadata for your component info: metrics: - name: morans_i @@ -44,31 +41,22 @@ info: documentation: https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.metrics.morans_i.html repository: https://github.com/scverse/scanpy - # The minimum possible value for this metric (required) min: -1 - # The maximum possible value for this metric (required) max: 1 - # Whether a higher value represents a 'better' solution (required) maximize: true -# Component-specific parameters (optional) arguments: - name: "--n_neighbors" type: "integer" default: 6 description: Number of spatial neighbours used to build the k-NN graph. -# Resources required to run the component resources: - # The script of your component (required) - type: python_script path: script.py - # Additional resources your script needs (optional) - # - type: file - # path: weights.pt + engines: - # Specifications for the Docker image for this component. - type: docker image: python:3.11-slim setup: @@ -89,9 +77,7 @@ engines: - "openproblems-bio/core#subdirectory=packages/python/openproblems" runners: - # This platform allows running the component natively - type: executable - # Allows turning the component into a Nextflow module / pipeline. - type: nextflow directives: label: [midtime,midmem,midcpu] From 4e25469572ec9729e7116e8c94d1b26f8d456a7f Mon Sep 17 00:00:00 2001 From: luisabender Date: Thu, 13 Aug 2026 15:18:28 +0200 Subject: [PATCH 3/4] added package versions --- src/metrics/morans_i/config.vsh.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/metrics/morans_i/config.vsh.yaml b/src/metrics/morans_i/config.vsh.yaml index 8250796..f8cb4d4 100644 --- a/src/metrics/morans_i/config.vsh.yaml +++ b/src/metrics/morans_i/config.vsh.yaml @@ -66,13 +66,14 @@ engines: - git - type: python packages: - - anndata~=0.10.0 - - scanpy~=1.10.0 - - scipy - - pandas - - pyyaml - - requests - - jsonschema + - anndata~=0.10.9 + - scanpy~=1.10.4 + - numpy~=2.4.6 + - pandas~=3.0.5 + - scipy~=1.17.1 + - pyyaml~=6.0.3 + - requests~=2.34.2 + - jsonschema~=4.26.0 github: - "openproblems-bio/core#subdirectory=packages/python/openproblems" From 650048a659cfd07487fbd4eb6b7e530766259d0a Mon Sep 17 00:00:00 2001 From: luisabender Date: Thu, 13 Aug 2026 15:53:49 +0200 Subject: [PATCH 4/4] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75de7ae..942cbb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ * Added `spaTrack` method (PR #4). * Added `Spearman's correlation` metric (PR #5). -* Added `Moran's I`metric (PR #6) +* Added `Moran's I` metric (PR #6). ## MAJOR CHANGES @@ -16,6 +16,7 @@ ## MINOR CHANGES +* Added package versions to the moran's I config (PR #6). ## BUGFIXES