From 63764e9961d8708f29ce3239f9750e9ea1bca22d Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 11:46:53 +0200 Subject: [PATCH 01/11] Add linear version of CellMapper --- src/methods/cellmapper_linear/config.vsh.yaml | 73 +++++++++++++++++++ src/methods/cellmapper_linear/script.py | 62 ++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/methods/cellmapper_linear/config.vsh.yaml create mode 100644 src/methods/cellmapper_linear/script.py diff --git a/src/methods/cellmapper_linear/config.vsh.yaml b/src/methods/cellmapper_linear/config.vsh.yaml new file mode 100644 index 0000000..bb18e26 --- /dev/null +++ b/src/methods/cellmapper_linear/config.vsh.yaml @@ -0,0 +1,73 @@ +__merge__: ../../api/comp_method.yaml +name: cellmapper_linear +label: CellMapper+PCA/CCA +summary: "Modality prediction in a PCA/CCA space using CellMapper" +description: | + CellMapper is a general framework for k-NN based mapping tasks in single-cell and spatial genomics. + This variant uses CellMapper to project modalities from a reference dataset (train) onto a query dataset (test) in a PCA/CCA latent space. +references: + doi: + - 10.5281/zenodo.15683594 +links: + documentation: https://cellmapper.readthedocs.io/en/latest/ + repository: https://github.com/quadbio/cellmapper +info: + preferred_normalization: log_cp10k + variants: + cellmapper-pca: + fallback_representation: joint_pca + mask_var: None + kernel_method: hnoca + cellmapper-pca-hvg: + fallback_representation: joint_pca + mask_var: "hvg" + kernel_method: hnoca + cellmapper-pca-hvg-gauss: + fallback_representation: joint_pca + mask_var: "hvg" + kernel_method: gauss + cellmapper-cca: + fallback_representation: fast_cca + mask_var: None + kernel_method: hnoca + cellmapper-cca-hvg: + fallback_representation: fast_cca + mask_var: "hvg" + kernel_method: hnoca + cellmapper-cca-hvg-gauss: + fallback_representation: fast_cca + mask_var: "hvg" + kernel_method: gauss +arguments: + - name: "--fallback_representation" + type: "string" + choices: ["joint_pca", "fast_cca"] + default: "fast_cca" + description: Fallback representation to use for k-NN mapping (computed if use_rep is None). + - name: "--mask_var" + type: "string" + description: Variable to mask for fallback representation. + - name: "--kernel_method" + type: "string" + choices: ["hnoca", "gauss"] + default: "hnoca" + description: Kernel function to compute k-NN edge weights. + - name: "--n_neighbors" + type: "integer" + default: 30 + description: Number of neighbors to consider for k-NN graph construction. +resources: + - type: python_script + path: script.py +engines: + - type: docker + image: openproblems/base_python:1 + setup: + - type: python + packages: + - cellmapper>=0.2.2 +runners: + - type: executable + - type: nextflow + directives: + label: [midtime,midmem,midcpu] diff --git a/src/methods/cellmapper_linear/script.py b/src/methods/cellmapper_linear/script.py new file mode 100644 index 0000000..9f61304 --- /dev/null +++ b/src/methods/cellmapper_linear/script.py @@ -0,0 +1,62 @@ +import anndata as ad +import cellmapper as cm +from scipy.sparse import csc_matrix + +## 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/test_mod1.h5ad', + 'output': 'output.h5ad', + 'fallback_representation': 'joint_pca', # or None for fallback_representation + 'n_neighbors': 30, + 'kernel_method': 'gauss', + 'mask_var': "hvg" # variable to mask for fallback representation +} +meta = { + 'name': 'cellmapper_linear', +} +## VIASH END + +print('Reading input files', flush=True) +input_train_mod1 = ad.read_h5ad(par['input_train_mod1']) +input_train_mod2 = ad.read_h5ad(par['input_train_mod2']) +input_test_mod1 = ad.read_h5ad(par['input_test_mod1']) + +print('Prepare the data', flush=True) +# Make sure we have normalized data in .X for mod1 +input_train_mod1.X = input_train_mod1.layers["normalized"].copy() +input_test_mod1.X = input_test_mod1.layers["normalized"].copy() + +# set up query and reference AnnData objects +query = input_test_mod1 +reference = input_train_mod1 +reference.obsm["mod2"] = input_train_mod2.layers["normalized"] # could use mudata here as well + +print("Set up and prepare Cellmapper", flush=True) +cmap = cm.CellMapper(query=query, reference=reference) +cmap.compute_neighbors( + use_rep=None, + fallback_representation=par['fallback_representation'], + n_neighbors=par['n_neighbors'], + fallback_kwargs={"mask_var": par['mask_var']}, + ) +cmap.compute_mapping_matrix(kernel_method=par['kernel_method']) + +print("Predict on test data", flush=True) +cmap.map_obsm(key="mod2", prediction_postfix="pred") +mod2_pred = csc_matrix(cmap.query.obsm["mod2_pred"]) + +print("Write output AnnData to file", flush=True) +output = ad.AnnData( + layers={"normalized": mod2_pred}, + obs=input_test_mod1.obs, + var=input_train_mod2.var, + uns={ + 'dataset_id': input_train_mod1.uns['dataset_id'], + 'method_id': meta["name"], + }, +) +output.write_h5ad(par['output'], compression='gzip') From 89062c3002dbe925fe74f933355b8a8c7e148517 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 13:42:16 +0200 Subject: [PATCH 02/11] Update object naming --- src/methods/cellmapper_linear/script.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/methods/cellmapper_linear/script.py b/src/methods/cellmapper_linear/script.py index 9f61304..b351efa 100644 --- a/src/methods/cellmapper_linear/script.py +++ b/src/methods/cellmapper_linear/script.py @@ -30,13 +30,11 @@ input_train_mod1.X = input_train_mod1.layers["normalized"].copy() input_test_mod1.X = input_test_mod1.layers["normalized"].copy() -# set up query and reference AnnData objects -query = input_test_mod1 -reference = input_train_mod1 -reference.obsm["mod2"] = input_train_mod2.layers["normalized"] # could use mudata here as well +# could use mudata here as well +input_train_mod1.obsm["mod2"] = input_train_mod2.layers["normalized"] print("Set up and prepare Cellmapper", flush=True) -cmap = cm.CellMapper(query=query, reference=reference) +cmap = cm.CellMapper(query=input_test_mod1, reference=input_train_mod1) cmap.compute_neighbors( use_rep=None, fallback_representation=par['fallback_representation'], From ce9b74a740960bbb87eb2dcd8ff1c765583e38e5 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 13:42:36 +0200 Subject: [PATCH 03/11] Add CellMapper scVI variant --- src/methods/cellmapper_scvi/config.vsh.yaml | 71 +++++++++++++++++ src/methods/cellmapper_scvi/script.py | 87 +++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/methods/cellmapper_scvi/config.vsh.yaml create mode 100644 src/methods/cellmapper_scvi/script.py diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml new file mode 100644 index 0000000..1e8f9c4 --- /dev/null +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -0,0 +1,71 @@ +__merge__: ../../api/comp_method.yaml +name: cellmapper_scvi +label: CellMapper+scVI +summary: "Modality prediction in an scVI latent space using CellMapper" +description: | + CellMapper is a general framework for k-NN based mapping tasks in single-cell and spatial genomics. + This variant uses CellMapper to project modalities from a reference dataset (train) onto a query dataset (test) in an scVI latent space. +references: + doi: + - 10.5281/zenodo.15683594 +links: + documentation: https://cellmapper.readthedocs.io/en/latest/ + repository: https://github.com/quadbio/cellmapper +info: + preferred_normalization: log_cp10k + variants: + cellmapper_hnoca_hvg: + kernel_method: hnoca + num_hvg: 2000 + cellmapper_hnoca_all_genes: + kernel_method: hnoca + num_hvg: None + cellmapper_gauss_hvg: + kernel_method: gauss + num_hvg: 2000 + cellmapper_gauss_all_genes: + kernel_method: gauss + num_hvg: None + +arguments: + - name: "--kernel_method" + type: "string" + choices: ["hnoca", "gauss"] + default: "hnoca" + description: Kernel function to compute k-NN edge weights (CellMapper parameter). + - name: "--n_neighbors" + type: "integer" + default: 30 + description: Number of neighbors to consider for k-NN graph construction (CellMapper parameter). + - name: "--num_hvg" + type: integer + description: "The number of HVG genes to subset to (Generic analysis parameter). " + - name: "--n_latent" + type: integer + default: 30 + description: Number of latent dimensions (scVI parameter). + - name: "--n_hidden" + type: integer + default: 128 + description: Number of hidden units (scVI parameter). + - name: "--n_layers" + type: integer + default: 2 + description: Number of layers (scVI parameter). +resources: + - type: python_script + path: script.py +engines: + - type: docker + image: openproblems/base_pytorch_nvidia:1.0.0 + setup: + - type: python + packages: + - cellmapper>=0.2.2 + - scvi-tools>=1.3.0 + +runners: + - type: executable + - type: nextflow + directives: + label: [midtime,midmem,midcpu,gpu] diff --git a/src/methods/cellmapper_scvi/script.py b/src/methods/cellmapper_scvi/script.py new file mode 100644 index 0000000..8bab200 --- /dev/null +++ b/src/methods/cellmapper_scvi/script.py @@ -0,0 +1,87 @@ +import anndata as ad +import scvi +import cellmapper as cm +from scipy.sparse import csc_matrix + +## 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/test_mod1.h5ad', + 'output': 'output.h5ad', + 'num_hvg': 2000, + 'n_latent': 30, + 'n_hidden': 128, + 'n_layers': 2, + 'n_neighbors': 30, + 'kernel_method': 'hnoca', + +} +meta = { + 'name': 'cellmapper_scvi', +} +## VIASH END + +print('Reading input files', flush=True) +input_train_mod1 = ad.read_h5ad(par['input_train_mod1']) +input_train_mod2 = ad.read_h5ad(par['input_train_mod2']) +input_test_mod1 = ad.read_h5ad(par['input_test_mod1']) + +print('Preprocess data', flush=True) +if par["num_hvg"]: + print("Subsetting to HVG", flush=True) + hvg_idx = input_train_mod1.var['hvg_score'].to_numpy().argsort()[:par["num_hvg"]] + input_train_mod1 = input_train_mod1[:,hvg_idx] + input_test_mod1 = input_test_mod1[:,hvg_idx] + +# could use mudata here as well +input_train_mod1.obsm["mod2"] = input_train_mod2.layers["normalized"] + +print("Concatenating train and test data", flush=True) +adata = ad.concat( + [input_train_mod1, input_test_mod1], merge = "same", label="split", keys=["train", "test"] + ) + +print('Create and train SCVI model', flush=True) +scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer="counts") + +model_kwargs = { + key: par[key] + for key in ["n_latent", "n_hidden", "n_layers"] + if par[key] is not None +} + +model = scvi.model.SCVI(adata, **model_kwargs) +model.train(early_stopping=True) +adata.obsm["X_scvi"] = model.get_latent_representation() + +# Place the representation back into individual objects +input_train_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "train"].obsm["X_scvi"].copy() +input_test_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "test"].obsm["X_scvi"].copy() + + +print('Setup and prepare Cellmapper', flush=True) +cmap = cm.CellMapper(query=input_test_mod1, reference=input_train_mod1) +cmap.compute_neighbors( + use_rep="X_scvi", + n_neighbors=par['n_neighbors'], + ) +cmap.compute_mapping_matrix(kernel_method=par['kernel_method']) + +print("Predict on test data", flush=True) +cmap.map_obsm(key="mod2", prediction_postfix="pred") +mod2_pred = csc_matrix(cmap.query.obsm["mod2_pred"]) + +print("Write output AnnData to file", flush=True) +output = ad.AnnData( + layers={"normalized": mod2_pred}, + obs=input_test_mod1.obs, + var=input_train_mod2.var, + uns={ + 'dataset_id': input_train_mod1.uns['dataset_id'], + 'method_id': meta["name"], + }, +) +output.write_h5ad(par['output'], compression='gzip') From ed80667e65dbf6698b12f8b289e30d407d775b24 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 15:35:07 +0200 Subject: [PATCH 04/11] Use modality-dependent scvi models --- src/methods/cellmapper_linear/script.py | 2 +- src/methods/cellmapper_scvi/config.vsh.yaml | 29 +++------ src/methods/cellmapper_scvi/script.py | 47 ++++++--------- src/methods/cellmapper_scvi/utils.py | 66 +++++++++++++++++++++ 4 files changed, 94 insertions(+), 50 deletions(-) create mode 100644 src/methods/cellmapper_scvi/utils.py diff --git a/src/methods/cellmapper_linear/script.py b/src/methods/cellmapper_linear/script.py index b351efa..8863a22 100644 --- a/src/methods/cellmapper_linear/script.py +++ b/src/methods/cellmapper_linear/script.py @@ -30,7 +30,7 @@ input_train_mod1.X = input_train_mod1.layers["normalized"].copy() input_test_mod1.X = input_test_mod1.layers["normalized"].copy() -# could use mudata here as well +# copy the normalized layer to obsm for mod2 input_train_mod1.obsm["mod2"] = input_train_mod2.layers["normalized"] print("Set up and prepare Cellmapper", flush=True) diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml index 1e8f9c4..2512b06 100644 --- a/src/methods/cellmapper_scvi/config.vsh.yaml +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -16,17 +16,17 @@ info: variants: cellmapper_hnoca_hvg: kernel_method: hnoca - num_hvg: 2000 + use_hvg: true cellmapper_hnoca_all_genes: kernel_method: hnoca - num_hvg: None + use_hvg: false cellmapper_gauss_hvg: kernel_method: gauss - num_hvg: 2000 + use_hvg: true cellmapper_gauss_all_genes: kernel_method: gauss - num_hvg: None - + use_hvg: false + arguments: - name: "--kernel_method" type: "string" @@ -37,21 +37,10 @@ arguments: type: "integer" default: 30 description: Number of neighbors to consider for k-NN graph construction (CellMapper parameter). - - name: "--num_hvg" - type: integer - description: "The number of HVG genes to subset to (Generic analysis parameter). " - - name: "--n_latent" - type: integer - default: 30 - description: Number of latent dimensions (scVI parameter). - - name: "--n_hidden" - type: integer - default: 128 - description: Number of hidden units (scVI parameter). - - name: "--n_layers" - type: integer - default: 2 - description: Number of layers (scVI parameter). + - name: "--use_hvg" + type: boolean + default: true + description: Whether to use highly variable genes (HVG) for the mapping (Generic analysis parameter). resources: - type: python_script path: script.py diff --git a/src/methods/cellmapper_scvi/script.py b/src/methods/cellmapper_scvi/script.py index 8bab200..7c40fcf 100644 --- a/src/methods/cellmapper_scvi/script.py +++ b/src/methods/cellmapper_scvi/script.py @@ -1,5 +1,5 @@ +import sys import anndata as ad -import scvi import cellmapper as cm from scipy.sparse import csc_matrix @@ -7,60 +7,49 @@ # 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod1.h5ad', - 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod2.h5ad', - 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/test_mod1.h5ad', + 'input_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad', 'output': 'output.h5ad', - 'num_hvg': 2000, - 'n_latent': 30, - 'n_hidden': 128, - 'n_layers': 2, 'n_neighbors': 30, 'kernel_method': 'hnoca', + 'use_hvg': False, } meta = { 'name': 'cellmapper_scvi', + 'resources_dir': 'target/executable/methods/cellmapper_scvi', } ## VIASH END +sys.path.append(meta['resources_dir']) +from utils import get_representation + print('Reading input files', flush=True) input_train_mod1 = ad.read_h5ad(par['input_train_mod1']) input_train_mod2 = ad.read_h5ad(par['input_train_mod2']) input_test_mod1 = ad.read_h5ad(par['input_test_mod1']) -print('Preprocess data', flush=True) -if par["num_hvg"]: - print("Subsetting to HVG", flush=True) - hvg_idx = input_train_mod1.var['hvg_score'].to_numpy().argsort()[:par["num_hvg"]] - input_train_mod1 = input_train_mod1[:,hvg_idx] - input_test_mod1 = input_test_mod1[:,hvg_idx] - -# could use mudata here as well -input_train_mod1.obsm["mod2"] = input_train_mod2.layers["normalized"] +mod1 = input_train_mod1.uns['modality'] +mod2 = input_train_mod2.uns['modality'] +print(f"Modality 1: {mod1}, n_features: {input_train_mod1.n_vars}", flush=True) +print(f"Modality 2: {mod2}, n_features: {input_train_mod2.n_vars}", flush=True) print("Concatenating train and test data", flush=True) adata = ad.concat( [input_train_mod1, input_test_mod1], merge = "same", label="split", keys=["train", "test"] ) -print('Create and train SCVI model', flush=True) -scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer="counts") - -model_kwargs = { - key: par[key] - for key in ["n_latent", "n_hidden", "n_layers"] - if par[key] is not None -} - -model = scvi.model.SCVI(adata, **model_kwargs) -model.train(early_stopping=True) -adata.obsm["X_scvi"] = model.get_latent_representation() +# Compute a latent representation using an appropriate model based on the modality +print("Get latent representation", flush=True) +adata = get_representation(adata=adata, modality=mod1, use_hvg=par['use_hvg']) # Place the representation back into individual objects input_train_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "train"].obsm["X_scvi"].copy() input_test_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "test"].obsm["X_scvi"].copy() +# copy the normalized layer to obsm for mod2 +input_train_mod1.obsm["mod2"] = input_train_mod2.layers["normalized"] print('Setup and prepare Cellmapper', flush=True) cmap = cm.CellMapper(query=input_test_mod1, reference=input_train_mod1) diff --git a/src/methods/cellmapper_scvi/utils.py b/src/methods/cellmapper_scvi/utils.py new file mode 100644 index 0000000..185fed6 --- /dev/null +++ b/src/methods/cellmapper_scvi/utils.py @@ -0,0 +1,66 @@ +from typing import Literal +import anndata as ad +import scvi +from scipy.sparse import issparse + + +def get_representation(adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC"], use_hvg: bool = True) -> ad.AnnData: + """ + Get a joint latent space representation of the data based on the modality. + + Parameters + ---------- + adata + AnnData object containing concateneted train and test data from the same modality. + modality + The modality of the data, one of "GEX", "ADT", or "ATAC". Depeding on the modality, we fit the following models: + + - "GEX": scVI model for gene expression data with ZINB likelihood on raw counts. + - "ADT": scVI model for ADT data (surface proteins) with Gaussian likelihood on normalized data. + - "ATAC": PeakVI model for ATAC data with Bernoulli likelihood on binarized count data. + + We assume that regardless of the modality, the raw data will be stored in the `counts` layer + (e.g. UMI counts for GEX and peak counts for ATAC), and the normalized data in the `normalized` layer. + use_hvg + Whether to subset the data to highly variable genes (HVGs) before training the model + + Returns + ------- + ad.AnnData + AnnData object with the latent representation in `obsm["X_scvi"]`, regardless of the modality. + """ + # Subset to highly variable features + if "hvg" in adata.var.columns and use_hvg: + n_hvg = adata.var["hvg"].sum() + print(f"Subsetting to {n_hvg} highly variable features ({n_hvg/adata.n_vars:.2%})", flush=True) + adata = adata[:, adata.var["hvg"]].copy() + else: + print("Training on all available features", flush=True) + + # Setup the AnnData object for scVI + if modality == "GEX": + layer = "counts" + scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) + model = scvi.model.SCVI(adata, gene_likelihood="nb", n_layers=2, n_latent=30) + elif modality == "ADT": + layer = "normalized" + scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) + model = scvi.model.SCVI(adata, gene_likelihood="normal", n_layers=1, n_latent=10) + elif modality == "ATAC": + layer = "counts" + scvi.model.PEAKVI.setup_anndata(adata, batch_key="batch", layer=layer) + model = scvi.model.PEAKVI(adata) + else: + raise ValueError(f"Unknown modality: {modality}") + + example_data = adata.layers[layer].data if issparse(adata.layers[layer]) else adata.layers[layer] + print(f"Set up AnnData for modality: {modality}, layer={layer}", flush=True) + print(f"Data looks like this: {example_data}", flush=True) + + # Train the model + model.train(early_stopping=True) + + # Get the latent representation + adata.obsm["X_scvi"] = model.get_latent_representation() + + return adata \ No newline at end of file From e2bc9dedaabac3b918637b32907696a30d0fbfc4 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 15:48:27 +0200 Subject: [PATCH 05/11] Improve logging and docs --- .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 3920 bytes src/methods/cellmapper_scvi/config.vsh.yaml | 6 +++++- src/methods/cellmapper_scvi/script.py | 8 ++++---- src/methods/cellmapper_scvi/utils.py | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc diff --git a/src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc b/src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95fa333bb0b0a4872094dd1cf81116cda1a6a107 GIT binary patch literal 3920 zcmd5k~WJ_cG)GOSy;sKFNC&h#-3+;%y`Cn zGlpO}5?i&}q?aXofmO6xdZQwT9O-G7IL~QsfxK0@!H;z>vOhuXn5M@hNg!;u>A!C z#WPE3+lP@^z*^Eq0 z&{5xxurOL<4Vplm4OR{=_Av~eluz8@%m!D>s{))2yR+3k+gz1h32--*xQC6~m1wZc zztMZr-C6BHM@Vjx!w*kxL-a_y!)rSwRz6ih+x&lqwnc80Pai`o0opSZ?f8GdZHwb& z?el-aZELMYM=mkjtRn%ZA@hv8LT)Ru516k!?(DBwlOz{ykb`Gkx!RXFvcO%B+h4aT z+gaV`N?Pc3JMQ>u^0u8|lh2kocy`oRO3k{?l8^XsJj<)|rTQA_Q zkpb_Lspy)hr$jS@L~{O#3CTdR2!mwAJqx2$9eqqlSjr(dKMneN@u<;PdWKSo*dNlg zVWLw{WKq%-lR6kwG(}4jF-ZV{B3U#sjEa&55;9B&MhfdW3c&ztRU9JK?ZP@TG+l$W z)|-BC1d)MmILW*F#cnvuq0=!)aG-Zb#bK03GC@@5alg2X#FS!E3@Kez^<}aOEF;wz z2p(C!=%^kLjpXbNXIe~w{xtX|qT7UKgHTMlT~^GDIDcdOeNj~wk*Z{Lol=dZWniSB znMTxckdOzO3ECfNhCxSLpHaXm63sl;O{8c>M5G2rqypHKv^pc2j&tx~D+1vJj1yAV z5FukgoIujzF)s*7HPGKjSknuts?@1YD4K++X6l8bU#yaJ6N!>x6d;ghMlvD$(h`ss%{t6@;sV^`U4wD$aa#qrxSP1H_$X zRvuid9Vebe`kkZ$opt0KnW~Og%sI4G?g2+xa5IAd85T`lGztj=%8me~KEO~$NoUj* zaS1q*5-Os(VTgS{pB;gaB@m|7xNCHIZ@rw!LusDTbz~k0ynyi^Yx~d6+^H+>a1A zS@cHV?y=d(G~BiLlv*${X6)F zF}bc5Pt9nHn!cqDb39GsQZ0=kZ|jxo9A*uDxg z99_oK9JJfh$OOs=QSswBV{;^7B7RJ+qS!|sge#lZ$fX#Qs~Lv79b-43+fpvCqBzlO zkWi9y2HHP9j?EbG>4@c2NlT;H1lGxy!HB`K#W)<6vXV68E}bz0Cu8uf$>_2XOCnXx zN%=g&G+ko_Q&Ej5_w)`$gK$?Fe*UiCr=Nve z$;d`eIV>K8&zHmJ_rh=9<)6dS&_-b+ZS}mj+qzr$Q|a4Md1%U-zG=NTXU*NRLi1K4 zwJ(tUUq~CRU$=kRzImnG9Do`+l3gZG2$P4}BN`A6oiWKB-5 zHSL9G?(xq8C)Z}41kOGScm7vOZa?m_X6J~KTaJ?1CxMPyBnLu!S!h2HddouZ7Qb`m zF>eXIPlfRc7KgRXp1p7%xD{%%IxcQyw&tz&t9zk$Ea9CWVn5sAziIem!|qJ^>gdzh z*xHr#!TW=oe7X6}gXXuMG{0p{-aMH6q&)eF6_?ACXm#v47qA*GY+c>DU|k;Go!K3E ze8~!aWX;^#=jQ2L_vYYc)CykOY1!wl!N}d7HFKZq1Sa|5XQQ8vZkVg1Pr38(-r04~ zF;MOp*y|Wvz5dyUpMH4vcA0DY%Zc#n*h@$P_{%pP3EDn$g;W?O{}OD!TN@rp#=T^u z!AL6km8kP)&?ygp*xiVR*dovMl>O zbM`x?mkUm>Y6t34SzY=&SwgM+ E3oJk89smFU literal 0 HcmV?d00001 diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml index 2512b06..f1f6cfc 100644 --- a/src/methods/cellmapper_scvi/config.vsh.yaml +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -4,7 +4,11 @@ label: CellMapper+scVI summary: "Modality prediction in an scVI latent space using CellMapper" description: | CellMapper is a general framework for k-NN based mapping tasks in single-cell and spatial genomics. - This variant uses CellMapper to project modalities from a reference dataset (train) onto a query dataset (test) in an scVI latent space. + This variant uses CellMapper to project modalities from a reference dataset (train) onto a query dataset + (test) in a modality-specific latent space computed with suitable scvi-tools models. For gene expression data, + we use the scVI model on raw counts (nb likelihood), for ADT data, we use the scVI models on normalized counts + (gaussian likelihood), and for ATAC data, we use the PeakVI model on raw counts. The actual CellMapper pipeline is + modality-agnostic. references: doi: - 10.5281/zenodo.15683594 diff --git a/src/methods/cellmapper_scvi/script.py b/src/methods/cellmapper_scvi/script.py index 7c40fcf..e955125 100644 --- a/src/methods/cellmapper_scvi/script.py +++ b/src/methods/cellmapper_scvi/script.py @@ -7,13 +7,13 @@ # 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod1.h5ad', - 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad', - 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad', + 'input_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/test_mod1.h5ad', 'output': 'output.h5ad', 'n_neighbors': 30, 'kernel_method': 'hnoca', - 'use_hvg': False, + 'use_hvg': True, } meta = { diff --git a/src/methods/cellmapper_scvi/utils.py b/src/methods/cellmapper_scvi/utils.py index 185fed6..cfb638a 100644 --- a/src/methods/cellmapper_scvi/utils.py +++ b/src/methods/cellmapper_scvi/utils.py @@ -54,8 +54,9 @@ def get_representation(adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC" raise ValueError(f"Unknown modality: {modality}") example_data = adata.layers[layer].data if issparse(adata.layers[layer]) else adata.layers[layer] - print(f"Set up AnnData for modality: {modality}, layer={layer}", flush=True) + print(f"Set up AnnData for modality: '{modality}' using layer: '{layer}'", flush=True) print(f"Data looks like this: {example_data}", flush=True) + print(model, flush=True) # Train the model model.train(early_stopping=True) From 82a41b4d3330085b56c3920a90602f700884f354 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 16:05:51 +0200 Subject: [PATCH 06/11] Add cellmapper to workflow config files --- src/workflows/run_benchmark/config.vsh.yaml | 2 ++ src/workflows/run_benchmark/main.nf | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/workflows/run_benchmark/config.vsh.yaml b/src/workflows/run_benchmark/config.vsh.yaml index 75a7429..ea4396c 100644 --- a/src/workflows/run_benchmark/config.vsh.yaml +++ b/src/workflows/run_benchmark/config.vsh.yaml @@ -68,6 +68,8 @@ dependencies: - name: control_methods/solution - name: methods/knnr_py - name: methods/knnr_r + - name: methods/cellmapper_linear + - name: methods/cellmapper_scvi - name: methods/lm - name: methods/guanlab_dengkw_pm - name: methods/novel diff --git a/src/workflows/run_benchmark/main.nf b/src/workflows/run_benchmark/main.nf index a032696..6a7989d 100644 --- a/src/workflows/run_benchmark/main.nf +++ b/src/workflows/run_benchmark/main.nf @@ -13,6 +13,8 @@ methods = [ solution, knnr_py, knnr_r, + cellmapper_linear, + cellmapper_scvi, lm, guanlab_dengkw_pm, novel, From 0d503dc39d1ccd96747b06ec28aa9da73bd0c347 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 30 Jul 2025 16:09:31 +0200 Subject: [PATCH 07/11] Update the Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9aec7f..f056dc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## NEW FUNCTIONALITY +* Added CellMapper method (two variants: simple PCA/CCA fallback and modality-specific scvi-tools models for joint mod1 representation) (PR #10) + * Added Novel method (PR #2). * Added Simple MLP method (PR #3). From 17ef5c6add049415e9304777f56dfd8e953d04cf Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Thu, 31 Jul 2025 08:44:39 +0200 Subject: [PATCH 08/11] Add clr normalization for adt counts --- src/methods/cellmapper_scvi/config.vsh.yaml | 14 +++++++++++++ src/methods/cellmapper_scvi/script.py | 9 +++++---- src/methods/cellmapper_scvi/utils.py | 22 ++++++++++++++++++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml index f1f6cfc..cd6e620 100644 --- a/src/methods/cellmapper_scvi/config.vsh.yaml +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -21,15 +21,23 @@ info: cellmapper_hnoca_hvg: kernel_method: hnoca use_hvg: true + adt_normalization: clr cellmapper_hnoca_all_genes: kernel_method: hnoca use_hvg: false + adt_normalization: clr cellmapper_gauss_hvg: kernel_method: gauss use_hvg: true + adt_normalization: clr + cellmapper_gauss_hvg_log_cp10k: + kernel_method: gauss + use_hvg: true + adt_normalization: log_cp10k cellmapper_gauss_all_genes: kernel_method: gauss use_hvg: false + adt_normalization: clr arguments: - name: "--kernel_method" @@ -45,6 +53,11 @@ arguments: type: boolean default: true description: Whether to use highly variable genes (HVG) for the mapping (Generic analysis parameter). + - name: "--adt_normalization" + type: "string" + choices: ["clr", "log_cp10k"] + default: "clr" + description: Normalization method for ADT data, clr = centered log ratio. resources: - type: python_script path: script.py @@ -56,6 +69,7 @@ engines: packages: - cellmapper>=0.2.2 - scvi-tools>=1.3.0 + - muon>=0.1.6 runners: - type: executable diff --git a/src/methods/cellmapper_scvi/script.py b/src/methods/cellmapper_scvi/script.py index e955125..f874103 100644 --- a/src/methods/cellmapper_scvi/script.py +++ b/src/methods/cellmapper_scvi/script.py @@ -7,13 +7,14 @@ # 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/train_mod1.h5ad', - 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/train_mod2.h5ad', - 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/normal/test_mod1.h5ad', + 'input_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad', 'output': 'output.h5ad', 'n_neighbors': 30, 'kernel_method': 'hnoca', 'use_hvg': True, + 'adt_normalization': 'clr', } meta = { @@ -42,7 +43,7 @@ # Compute a latent representation using an appropriate model based on the modality print("Get latent representation", flush=True) -adata = get_representation(adata=adata, modality=mod1, use_hvg=par['use_hvg']) +adata = get_representation(adata=adata, modality=mod1, use_hvg=par['use_hvg'], adt_normalization=par['adt_normalization']) # Place the representation back into individual objects input_train_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "train"].obsm["X_scvi"].copy() diff --git a/src/methods/cellmapper_scvi/utils.py b/src/methods/cellmapper_scvi/utils.py index cfb638a..1fda425 100644 --- a/src/methods/cellmapper_scvi/utils.py +++ b/src/methods/cellmapper_scvi/utils.py @@ -1,10 +1,12 @@ from typing import Literal import anndata as ad import scvi -from scipy.sparse import issparse +from scipy.sparse import issparse, csr_matrix, csc_matrix +import muon -def get_representation(adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC"], use_hvg: bool = True) -> ad.AnnData: +def get_representation( + adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC"], use_hvg: bool = True, adt_normalization: Literal["clr", "log_cp10k"] = "clr") -> ad.AnnData: """ Get a joint latent space representation of the data based on the modality. @@ -23,6 +25,10 @@ def get_representation(adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC" (e.g. UMI counts for GEX and peak counts for ATAC), and the normalized data in the `normalized` layer. use_hvg Whether to subset the data to highly variable genes (HVGs) before training the model + adt_normalization + Normalization method for ADT data. Options are: + - "clr" (centered log-ratio transformation) + - "log_cp10k" (normalization to 10k counts per cell and logarithm transformation) Returns ------- @@ -43,7 +49,17 @@ def get_representation(adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC" scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) model = scvi.model.SCVI(adata, gene_likelihood="nb", n_layers=2, n_latent=30) elif modality == "ADT": - layer = "normalized" + print(f"Normalizing the ADT data using method '{adt_normalization}'") + if adt_normalization == "clr": + adata.X = csc_matrix(adata.layers["counts"]) # Use raw counts for ADT + muon.prot.pp.clr(adata) + adata.layers["adt_normalized"] = csr_matrix(adata.X) + elif adt_normalization == "log_cp10k": + adata.layers["adt_normalized"] = adata.layers["normalized"] + else: + raise ValueError(f"Unknown ADT normalization method: {adt_normalization}") + + layer = "adt_normalized" scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) model = scvi.model.SCVI(adata, gene_likelihood="normal", n_layers=1, n_latent=10) elif modality == "ATAC": From 5918c28ba0258d9258a6632ea9ea32d001d8b30f Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Thu, 31 Jul 2025 10:17:38 +0200 Subject: [PATCH 09/11] Add CLR normalization for ADT data --- src/methods/cellmapper_scvi/config.vsh.yaml | 4 +++ src/methods/cellmapper_scvi/script.py | 15 +++++++----- src/methods/cellmapper_scvi/utils.py | 27 +++++++++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml index cd6e620..b3f9783 100644 --- a/src/methods/cellmapper_scvi/config.vsh.yaml +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -58,6 +58,10 @@ arguments: choices: ["clr", "log_cp10k"] default: "clr" description: Normalization method for ADT data, clr = centered log ratio. + - name: "--plot_umap" + type: boolean + default: false + description: Whether to plot the UMAP embedding of the latent space (for diagnoscic purposes) resources: - type: python_script path: script.py diff --git a/src/methods/cellmapper_scvi/script.py b/src/methods/cellmapper_scvi/script.py index f874103..1f9e74b 100644 --- a/src/methods/cellmapper_scvi/script.py +++ b/src/methods/cellmapper_scvi/script.py @@ -7,14 +7,15 @@ # 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_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod1.h5ad', - 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad', - 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad', + 'input_train_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/swap/train_mod1.h5ad', + 'input_train_mod2': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/swap/train_mod2.h5ad', + 'input_test_mod1': 'resources_test/task_predict_modality/openproblems_neurips2021/bmmc_multiome/swap/test_mod1.h5ad', 'output': 'output.h5ad', 'n_neighbors': 30, 'kernel_method': 'hnoca', - 'use_hvg': True, - 'adt_normalization': 'clr', + 'use_hvg': False, + 'adt_normalization': 'clr', # Normalization method for ADT data + 'plot_umap': True, } meta = { @@ -43,7 +44,9 @@ # Compute a latent representation using an appropriate model based on the modality print("Get latent representation", flush=True) -adata = get_representation(adata=adata, modality=mod1, use_hvg=par['use_hvg'], adt_normalization=par['adt_normalization']) +adata = get_representation( + adata=adata, modality=mod1, use_hvg=par['use_hvg'], adt_normalization=par['adt_normalization'], plot_umap=par['plot_umap'] + ) # Place the representation back into individual objects input_train_mod1.obsm["X_scvi"] = adata[adata.obs["split"] == "train"].obsm["X_scvi"].copy() diff --git a/src/methods/cellmapper_scvi/utils.py b/src/methods/cellmapper_scvi/utils.py index 1fda425..f059877 100644 --- a/src/methods/cellmapper_scvi/utils.py +++ b/src/methods/cellmapper_scvi/utils.py @@ -3,10 +3,16 @@ import scvi from scipy.sparse import issparse, csr_matrix, csc_matrix import muon +import scanpy as sc def get_representation( - adata: ad.AnnData, modality: Literal["GEX", "ADT", "ATAC"], use_hvg: bool = True, adt_normalization: Literal["clr", "log_cp10k"] = "clr") -> ad.AnnData: + adata: ad.AnnData, + modality: Literal["GEX", "ADT", "ATAC"], + use_hvg: bool = True, + adt_normalization: Literal["clr", "log_cp10k"] = "clr", + plot_umap: bool = False, + ) -> ad.AnnData: """ Get a joint latent space representation of the data based on the modality. @@ -29,6 +35,9 @@ def get_representation( Normalization method for ADT data. Options are: - "clr" (centered log-ratio transformation) - "log_cp10k" (normalization to 10k counts per cell and logarithm transformation) + plot_umap + Purely for diagnostic purposes, to see whether the data integration looks ok, this optionally computes + a UMAP in shared latent space and stores a plot. Returns ------- @@ -46,8 +55,9 @@ def get_representation( # Setup the AnnData object for scVI if modality == "GEX": layer = "counts" - scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) - model = scvi.model.SCVI(adata, gene_likelihood="nb", n_layers=2, n_latent=30) + scvi.model.SCVI.setup_anndata(adata, layer=layer, categorical_covariate_keys=["split", "batch"]) + model = scvi.model.SCVI(adata) + elif modality == "ADT": print(f"Normalizing the ADT data using method '{adt_normalization}'") if adt_normalization == "clr": @@ -60,11 +70,11 @@ def get_representation( raise ValueError(f"Unknown ADT normalization method: {adt_normalization}") layer = "adt_normalized" - scvi.model.SCVI.setup_anndata(adata, batch_key="batch", layer=layer) + scvi.model.SCVI.setup_anndata(adata, layer=layer, categorical_covariate_keys=["split", "batch"]) model = scvi.model.SCVI(adata, gene_likelihood="normal", n_layers=1, n_latent=10) elif modality == "ATAC": layer = "counts" - scvi.model.PEAKVI.setup_anndata(adata, batch_key="batch", layer=layer) + scvi.model.PEAKVI.setup_anndata(adata, layer=layer, categorical_covariate_keys=["split", "batch"]) model = scvi.model.PEAKVI(adata) else: raise ValueError(f"Unknown modality: {modality}") @@ -80,4 +90,11 @@ def get_representation( # Get the latent representation adata.obsm["X_scvi"] = model.get_latent_representation() + if plot_umap: + sc.pp.neighbors(adata, use_rep="X_scvi") + sc.tl.umap(adata) + + plot_name = f"_{modality}_{adt_normalization}_use_hvg_{use_hvg}.png" + sc.pl.embedding(adata, basis="umap", color=["batch", "split"], show=False, save=plot_name) + return adata \ No newline at end of file From 7c7b8b5e65b4c09ebbba448c5dc23b2f40584bd2 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 6 Aug 2025 10:44:53 +0200 Subject: [PATCH 10/11] Correctly set the path to the utils file --- src/methods/cellmapper_scvi/config.vsh.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/methods/cellmapper_scvi/config.vsh.yaml b/src/methods/cellmapper_scvi/config.vsh.yaml index b3f9783..359579a 100644 --- a/src/methods/cellmapper_scvi/config.vsh.yaml +++ b/src/methods/cellmapper_scvi/config.vsh.yaml @@ -65,6 +65,8 @@ arguments: resources: - type: python_script path: script.py + - path: utils.py + dest: utils.py engines: - type: docker image: openproblems/base_pytorch_nvidia:1.0.0 From 47364fc36de823a64c3affc4e67e5a395abe3044 Mon Sep 17 00:00:00 2001 From: Marius Lange Date: Wed, 6 Aug 2025 10:49:17 +0200 Subject: [PATCH 11/11] Remove accidentally committed __pycache__ file and improve .gitignore --- .gitignore | 5 ++++- .../__pycache__/utils.cpython-312.pyc | Bin 3920 -> 0 bytes 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 309271d..b88f162 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ /output trace-* .ipynb_checkpoints -/temp \ No newline at end of file +/temp +__pycache__/ +*.pyc +*.pyo \ No newline at end of file diff --git a/src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc b/src/methods/cellmapper_scvi/__pycache__/utils.cpython-312.pyc deleted file mode 100644 index 95fa333bb0b0a4872094dd1cf81116cda1a6a107..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3920 zcmd5k~WJ_cG)GOSy;sKFNC&h#-3+;%y`Cn zGlpO}5?i&}q?aXofmO6xdZQwT9O-G7IL~QsfxK0@!H;z>vOhuXn5M@hNg!;u>A!C z#WPE3+lP@^z*^Eq0 z&{5xxurOL<4Vplm4OR{=_Av~eluz8@%m!D>s{))2yR+3k+gz1h32--*xQC6~m1wZc zztMZr-C6BHM@Vjx!w*kxL-a_y!)rSwRz6ih+x&lqwnc80Pai`o0opSZ?f8GdZHwb& z?el-aZELMYM=mkjtRn%ZA@hv8LT)Ru516k!?(DBwlOz{ykb`Gkx!RXFvcO%B+h4aT z+gaV`N?Pc3JMQ>u^0u8|lh2kocy`oRO3k{?l8^XsJj<)|rTQA_Q zkpb_Lspy)hr$jS@L~{O#3CTdR2!mwAJqx2$9eqqlSjr(dKMneN@u<;PdWKSo*dNlg zVWLw{WKq%-lR6kwG(}4jF-ZV{B3U#sjEa&55;9B&MhfdW3c&ztRU9JK?ZP@TG+l$W z)|-BC1d)MmILW*F#cnvuq0=!)aG-Zb#bK03GC@@5alg2X#FS!E3@Kez^<}aOEF;wz z2p(C!=%^kLjpXbNXIe~w{xtX|qT7UKgHTMlT~^GDIDcdOeNj~wk*Z{Lol=dZWniSB znMTxckdOzO3ECfNhCxSLpHaXm63sl;O{8c>M5G2rqypHKv^pc2j&tx~D+1vJj1yAV z5FukgoIujzF)s*7HPGKjSknuts?@1YD4K++X6l8bU#yaJ6N!>x6d;ghMlvD$(h`ss%{t6@;sV^`U4wD$aa#qrxSP1H_$X zRvuid9Vebe`kkZ$opt0KnW~Og%sI4G?g2+xa5IAd85T`lGztj=%8me~KEO~$NoUj* zaS1q*5-Os(VTgS{pB;gaB@m|7xNCHIZ@rw!LusDTbz~k0ynyi^Yx~d6+^H+>a1A zS@cHV?y=d(G~BiLlv*${X6)F zF}bc5Pt9nHn!cqDb39GsQZ0=kZ|jxo9A*uDxg z99_oK9JJfh$OOs=QSswBV{;^7B7RJ+qS!|sge#lZ$fX#Qs~Lv79b-43+fpvCqBzlO zkWi9y2HHP9j?EbG>4@c2NlT;H1lGxy!HB`K#W)<6vXV68E}bz0Cu8uf$>_2XOCnXx zN%=g&G+ko_Q&Ej5_w)`$gK$?Fe*UiCr=Nve z$;d`eIV>K8&zHmJ_rh=9<)6dS&_-b+ZS}mj+qzr$Q|a4Md1%U-zG=NTXU*NRLi1K4 zwJ(tUUq~CRU$=kRzImnG9Do`+l3gZG2$P4}BN`A6oiWKB-5 zHSL9G?(xq8C)Z}41kOGScm7vOZa?m_X6J~KTaJ?1CxMPyBnLu!S!h2HddouZ7Qb`m zF>eXIPlfRc7KgRXp1p7%xD{%%IxcQyw&tz&t9zk$Ea9CWVn5sAziIem!|qJ^>gdzh z*xHr#!TW=oe7X6}gXXuMG{0p{-aMH6q&)eF6_?ACXm#v47qA*GY+c>DU|k;Go!K3E ze8~!aWX;^#=jQ2L_vYYc)CykOY1!wl!N}d7HFKZq1Sa|5XQQ8vZkVg1Pr38(-r04~ zF;MOp*y|Wvz5dyUpMH4vcA0DY%Zc#n*h@$P_{%pP3EDn$g;W?O{}OD!TN@rp#=T^u z!AL6km8kP)&?ygp*xiVR*dovMl>O zbM`x?mkUm>Y6t34SzY=&SwgM+ E3oJk89smFU