Metabolic crosstalk on the Slide-TCR-seq human lung RCC dataset#

In this tutorial, we demonstrate how we can use Harreman to perform metabolic crosstalk both at the cell-type-agnostic and cell-type-specific levels. For this, we will build on the metabolic zonation tutorial to infer metabolite exchange events that are specific to the previously defined zones.

If you are unfamiliar with how Harreman can be used to delineate metabolic zones in the tissue, we recommend starting with the metabolic zonation tutorial. However, most of the functions that we will cover in this tutorial do not depend on the outputs generated in the previous tutorial. Therefore, if you are not interested in dividing the tissue into metabolic zones, the previous tutorial can be skipped. In this case, beware of the functions that make use of metabolic modules, as you won’t be able to successfully run them until the metabolic zonation results are stored in your AnnData.

Plan for this tutorial:

  1. Loading the data.

  2. Inferring cell-type-agnostic gene pair and metabolite crosstalk.

  3. Computing interacting gene pair and metabolite scores.

  4. Group spatially co-localized metabolites.

  5. Correlation between metabolite group scores and metabolic module scores.

Loading the dataset#

In this tutorial, we will work with the AnnData saved in the metabolic zonation tutorial.

import harreman
import os
import tempfile
import numpy as np
import pandas as pd
import scanpy as sc
import anndata as ad
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib as mpl
import itertools
from scipy.stats import pearsonr, wilcoxon, mannwhitneyu, ranksums, zscore
import random
from sklearn import linear_model
from scipy.stats import hypergeom, zscore
import scipy.stats as stats
from statsmodels.stats.multitest import multipletests
from plotnine import *
from matplotlib.patches import Patch
from scipy.cluster.hierarchy import fcluster
import math
from collections import Counter
import warnings
warnings.filterwarnings("ignore")
adata = harreman.read_h5ad('Slide_seq_lung_metabolic_zonation.h5ad')

Cell-type-agnostic metabolic crosstalk#

Firstly, we extract the metabolite and ligand-receptor interaction information for cellular crosstalk inference using the extract_interaction_db function. For this, we need to specify the organism the dataset corresponds to (with the species parameter), indicate if we want to load only transporter information from HarremanDB (database='transporter'), only ligand-receptor information from CellChatDB (database='LR'), or both of them (database='both'). In case we don’t want to restrict the interactions to the extracellular space, we can use the extracellular_only parameter with the False value.

Here, we load both databases from human and we restrict the interactions to the extracellular space.

harreman.pp.extract_interaction_db(adata, species='human', database='both', verbose=True)

We then compute the neighborhood graph using the compute_knn_graph function. For this, we need to specify the latent (or observed) space we are using to compute our cell metric with the compute_neighbors_on_key parameter. Further, only one of compute_neighbors_on_key or distances_obsp_key is needed in order to run the function. Similarly, either the neighborhood_radius or the n_neighbors parameter needs to be used (the former needs to contain information in micrometers). The sample_key parameter is optional and is only required when more than one sample are present in the AnnData and we want to avoid having neighbors across different samples.

Here, we set weighted_graph=False to just use binary, 0-1 weights and neighborhood_radius=100 to create a local neighborhood with a radius of 100 micrometers. Larger neighborhood sizes can result in more robust detection of correlations and autocorrelations at a cost of missing more fine-grained, smaller-scale, spatial patterns. Further, set sample_key='sample' to make sure there are no shared neighbors between samples.

harreman.tl.compute_knn_graph(adata, 
                        compute_neighbors_on_key="spatial", 
                        neighborhood_radius=100,
                        weighted_graph=False,
                        sample_key='sample',
                        verbose=True
                        )

Once the neighborhood graph is computed, genes can be filtered out to restrict the interactions to the most informative ones using the apply_gene_filtering function. For this, several options have been implemented. In our case, we use local autocorrelation (autocorrelation_filt = True parameter) with the Bernoulli distribution to model our data.

harreman.tl.apply_gene_filtering(adata, layer_key='counts', model='bernoulli', autocorrelation_filt = True, verbose=True)

To compute gene pairs, the compute_gene_pairs function is used. For this, cell type information can be used to compute cell-type-specific pairs. Here, as we are inferring crosstalk in a cell-type-agnostic way, ct_specific = False will be specified.

harreman.tl.compute_gene_pairs(adata, ct_specific = False, verbose=True)

To infer metabolic crosstalk, the compute_cell_communication function is used. To assess statistical significance, we can either use the parametric test (test = "parametric"), the non-parametric one (test = "non-parametric"), or both of them (test = "both"). If the parametric test needs to be computed, the model parameter needs to be specified, in addition to the raw counts layer (layer_key_p_test parameter). In this case, we use the Bernoulli distribution to model the count data. In case the non-parametric test is used, the number of permutations is specified through the M parameter (1000 by default), as well as the layer of the raw or normalized count data (layer_key_np_test parameter). In our case, we use the log-normalized counts to infer crosstalk and assess its significance through the non-parametric test.

harreman.tl.compute_cell_communication(adata, model='bernoulli', M = 1000, test = "both", layer_key_p_test='counts', layer_key_np_test='log_norm', verbose=True)

We then select statistically significant interactions (FDR < 0.05) from the non-parametric test (specified in the test parameter).

harreman.tl.select_significant_interactions(adata, test = "non-parametric", threshold = 0.05)

Computing gene pair and metabolite interacting scores#

To visualize the interactions at the gene pair and metabolite levels in the tissue, the compute_interacting_cell_scores is used. The test parameter is analogous to the compute_cell_communication function, but, instead of using it to compute statistical significance (for this only the non-parametric test is used), we use it to specify if we want to compute the values using the raw counts (used for the parametric test) or the log-normalized counts (used for the non-parametric test). Here, we consider both of them, but we will eventually focus on the scores computed using the log-normalized counts. Additionally, as assessing the statistical significance using the non-parametric test can take a long time in this case, only the parametric one is used. However, as the parametric test has not been presented in the manuscript for this particular statistic, we will not consider significance values and will select relevant spots for each metabolite using a different approach.

harreman.tl.compute_interacting_cell_scores(adata, test = "both", compute_significance='parametric', verbose=True)

We then compute the correlation between the metabolite (or gene pair) scores and the metabolic module scores defined in the metabolic zonation tutorial. For this, the metabolite (or gene pair) scores computed in the non-parametric test (using the log-normalized counts) are considered. To specify whether we want to focus on gene pair or metabolite scores, the interaction_type parameter is used (with the metabolite or gene_pair value). Here, we run the function at the metabolite level.

harreman.tl.compute_interaction_module_correlation(adata, cor_method='pearson', interaction_type='metabolite', test='non-parametric')

Downstream analyses#

We then visualize the correlation between metabolite scores and module scores.

harreman.pl.plot_interaction_module_correlation(adata, x_rotation = 45, figsize = (5,6), threshold = 0.25)
../../_images/7854176dbb88d29b5ce70f6113507e0214ee2dba5f0e76b5e44f1e7eaab8b335.png

We select some metabolites to visualize them in the tissue.

metabolites = ['L-Lactate', 'L-Arginine', 'Sodium_calcium exchange', 'Adenosine diphosphate ribose']
harreman.pl.plot_interacting_cell_scores(adata, interactions=metabolites, test='non-parametric', coords_obsm_key='spatial', s=1, vmin='p1', vmax='p99', only_sig_values=False, normalize_values=True, cmap='Blues', sample_specific=True)
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
../../_images/a27aab7a1a182131a328a2334a30768731e838731c9770167cfe5b43652c58e4.png ../../_images/92e1923ffab1e9938a1155f50d0d9cc1aec51b0680ba6fc9f1fc0ca41d7e39b0.png ../../_images/bce7e651811f0f0e88a9b2520b4922587a5500caeeeb6fa596a7378fd9e1cfa0.png ../../_images/04502cd150b4f1db1262cccde919ddc84a2c87d10c4396939992bfe6d08831b5.png ../../_images/c2b4b4634beb318510dd4511a26a91fb8eb67cddbf3f6bd2256cde9a98cfee76.png ../../_images/186b05ff252fa0def51eff694371923b0a86e470ef03a3da6c04afc0fe90bd5b.png ../../_images/f16fb70f471ac91f7967b1ac1ba8ed36604d426e8ebbd10f9ddaa21c4dbec1cf.png ../../_images/528fbd12762b08025c6f80dea271d02c0c3ef6c8c43adfa35075591d0998e6c2.png ../../_images/4185a70fd3ac5476e212fe3fff6c599563720eac85ba988b85849b212d1b2f7a.png ../../_images/59a083ff76113c744f183d019fb871f9ee5121bfeb6fde6e713a73324bfcd967.png ../../_images/3c56b89a1ba4bb5e05bc10a7b3e66b59aed60703169a073f97ff274c1e25f20c.png ../../_images/f73d36042baa391e9ccd74f57a24cf4cf4ca96da325296e93e97792785dfb115.png ../../_images/df85519db42ab57cf17ea317f38331e5c12de34ff1bbe79347b4893277def262.png ../../_images/3312933fc54235b635d3aed8e0952a15ae952baae0a3633d0dae9413769a9f0f.png ../../_images/a83e00a8953dfc75f79ed17d938407900ad49e8f2756dd1270d8069162a816f7.png ../../_images/47541e8adef4399a8bc8f57ca0619a7301796279d4ef8f38eb908e42f9f6188a.png ../../_images/f44ba24d8609407cec6a7b11973fb8e0e82bd2dcfd29dfcf63dfef428d768474.png ../../_images/86e79322d9b28c704208ba43c8282f348a00df599716c1122fc035d44bb98fab.png ../../_images/253ef9f3f0670198411c5bd54abcf37ff82e8c16cd6fd9e886855b2b0869f484.png ../../_images/67b35676472e1ecf21016a4d22ceddeafd1a085a489060ae9b3d8a963dec833f.png ../../_images/523297aef364ec42ba0a7ea44de82a81fa4c71e96e74d5e8d681e630eac557db.png ../../_images/3c3e8ea31484fa7755c23e9b06944f2082c9f9161f95a6e58c8ce74248ad2910.png ../../_images/c84afd99b472b8ec034365539bb82b734d11f30fe49cd64811e771e294e8bee5.png ../../_images/a35c574e402ec5193b310339a1c8441b4c65790ea72f1d784b856c8f476a3dda.png ../../_images/8f20c7f65f3a44a8696ca25ef41e59d3eaa7628996e63b062f01fe8a733c0f16.png ../../_images/dc7030d5de93f9b827fdd16440b7dab5bef0d587997660800cc565ecb46955ef.png ../../_images/885cfcbd356d8fc29cad26933d53e9624cf798e74f9daf30ea05c880eb7cbc4b.png ../../_images/6bf3f3b189aa8ed8091722e862e8902f9a0a45ead4e5ef8a721b27da769ba691.png

We can also identify which gene pairs belonging to the metabolites of interest have a significant spatial co-expression.

cell_communication_df = adata.uns['ccc_results']['cell_com_df_gp_sig'].copy()
gene_pairs_per_metabolite = adata.uns['gene_pairs_per_metabolite']
    
def to_tuple(x):
    # Recursively convert lists to tuples
    if isinstance(x, list):
        return tuple(to_tuple(i) for i in x)
    return x

metabolite_gene_pair_df = pd.DataFrame.from_dict(gene_pairs_per_metabolite, orient="index").reset_index()
metabolite_gene_pair_df = metabolite_gene_pair_df.rename(columns={"index": "metabolite"})
metabolite_gene_pair_df['gene_pair'] = metabolite_gene_pair_df['gene_pair'].apply(
    lambda arr: [(to_tuple(gp[0]), to_tuple(gp[1])) for gp in arr]
)
metabolite_gene_pair_df['gene_type'] = metabolite_gene_pair_df['gene_type'].apply(
    lambda arr: [(to_tuple(gt[0]), to_tuple(gt[1])) for gt in arr]
)
metabolite_gene_pair_df = pd.concat([
    metabolite_gene_pair_df['metabolite'],
    metabolite_gene_pair_df.explode('gene_pair')['gene_pair'],
    metabolite_gene_pair_df.explode('gene_type')['gene_type'],
], axis=1).reset_index(drop=True)

if 'LR_database' in adata.uns:
    LR_database = adata.uns['LR_database']
    df_merged = pd.merge(metabolite_gene_pair_df, LR_database, left_on='metabolite', right_on='interaction_name', how='left')
    LR_df = df_merged.dropna(subset=['pathway_name'])
    metabolite_gene_pair_df['metabolite'][metabolite_gene_pair_df.metabolite.isin(LR_df.metabolite)] = LR_df['pathway_name']
metabolite_gene_pair_df = metabolite_gene_pair_df.set_index('metabolite')
def is_present(row, gene_pairs):
    gene1, gene2 = row['Gene 1'], row['Gene 2']
    return any(
        (gene1 in pair and gene2 in pair) if isinstance(pair, tuple) else False
        for pair in gene_pairs
    )

gene_pairs = metabolite_gene_pair_df.loc[metabolites]['gene_pair'].tolist()
cell_communication_df_filt = cell_communication_df[cell_communication_df.apply(lambda row: is_present(row, gene_pairs), axis=1)]
gene_pairs_filt = list(zip(cell_communication_df_filt["Gene 1"], cell_communication_df_filt["Gene 2"]))

We can also visualize the spatial co-expression of some interesting transporter pairs.

gene_pairs_filt_plot = list(cell_communication_df_filt["Gene 1"] + '_' + cell_communication_df_filt["Gene 2"])
gene_pairs_filt_plot = ['SLC16A1_SLC16A3', 'SLC8A1_SLC8A1', 'SLC7A1_SLC6A8', 'CD38_CD38']
harreman.pl.plot_interacting_cell_scores(adata, interactions=gene_pairs_filt_plot, test='non-parametric', coords_obsm_key='spatial', s=1, vmin='p1', vmax='p99', only_sig_values=False, normalize_values=True, cmap='Greens', sample_specific=True)
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
../../_images/7b371bcdcc38f84a3f9ca81b50ec5588130cc9e654aeeca6175f6a6cccf8b1e8.png ../../_images/92fe2a380568c291a46593074b188461237e849b42eb7427e06a0814df84b718.png ../../_images/4e2a0d5a791b5294d21e5568101dab02aa971de65027c45b81ff3fafd7956dab.png ../../_images/c5c73396e0007d2c3f3282ba825a4e09ea166f69ad1827a3137d0693461a0492.png ../../_images/59dd518548c55feab644cf619b01aa82edc4ca5a9ee259135584d2c502a20999.png ../../_images/04cfae67a1a86112c7147596baa4d0d46271d5c6523b6c5faf2f0fa50218440d.png ../../_images/c58d62dbe2b9d11dd1e09820fe89451569582e3aaed4a4d2664432e7df0c596e.png ../../_images/66dd8d48710a9b26e5816ede8282d7b766f83bf1be0c1cc9c3fbb6393ccdca66.png ../../_images/b2bcdae00eac91bd6fecbe420424eb864f125448e259b152473ffe7b89f7532f.png ../../_images/fda69a1dd4be0703c159d5582c63575147c54897f4318928e81cd54b94538d9c.png ../../_images/411aa2243da81a0de5945dfd4ce79ff651f48a66d7c9eac8cdc501175d89eddf.png ../../_images/8fdbb46b218cd72543ff34664bb6f2cfbb84a0266a305ff65e72b03650e65e43.png ../../_images/b320915f3f5ba8e5e71795767ea6a69969e76dc47908c641f2fba441790cbea4.png ../../_images/9be668573bb43696fce3cbd2973c533bba9cc9c329fdedf0dd629e54947c1d8a.png ../../_images/0700fd8bbe1762eead7a67069a25461027ad3a137470b0dfcf8d23ee742b8f64.png ../../_images/62b4a0302cd48202c0ebd7bc60d3c2dee8e9447368a95026207be8f2c555cde7.png ../../_images/469571e488e58c495b18d0287f5ffc220c1769821052e7f80788c6016ffddb89.png ../../_images/df883823d9d8ec8b5a25539cf838d0d29a9a1bdd240d1d28dc360ecb37232d6f.png ../../_images/6e28d3298749dc95c37f8b7a18c4db22c9c9fe0795e2e59f818bf71a052d61ef.png ../../_images/6a9d6ea3242cddf46208b25a246450f7ce101d0bfc718c150ee581dae2a39b46.png ../../_images/0b855259c0ddcc6c509049e9ce8cb8d7d2a3b1ce1b6a844c7b9da9fb2e28a626.png ../../_images/e23eb65dc59e40daeeda67ee2ce0e69245c836e13faeb2844bae0afcef4e5467.png ../../_images/83c8c43c6c3d6f6f325d05ef7f107aabe6166392dbe49561390f49d69b3849eb.png ../../_images/8d001a82f93aad0398d74fe9e09521e108814b6a6ee70f2a2a7e88a78c6b74c6.png ../../_images/eb048d2d78355c585ebc6e693fe2a9f4738698b68269fea0eb57bf439834c509.png ../../_images/9afd61a4dc7eae46b3345b1e4e8dea4e12335583711a712611571d35014f8028.png ../../_images/b685423aad28e4d57fb6c6b38b9b1dec6a9e2d71d5283bb00a9c0360a1d99cac.png ../../_images/f5afda957d9d2e9d89d55ada9098ef94df676d5ab90d255ed22b04d30845843a.png
gene_1 = ["_".join(pair[0]) if isinstance(pair[0], list) else pair[0] for pair in gene_pairs]
gene_2 = ["_".join(pair[1]) if isinstance(pair[1], list) else pair[1] for pair in gene_pairs]

def remove_duplicates(lst):
    seen = set()
    return [x for x in lst if not (x in seen or seen.add(x))]

gene_1 = remove_duplicates(gene_1)
gene_2 = remove_duplicates(gene_2)
cell_communication_df_filt['log10_FDR'] = -np.log10(cell_communication_df_filt['FDR_np'])
cell_communication_df_filt['log10_C'] = np.log10(cell_communication_df_filt['C_np'])
def convert_list_to_string(value):
    return '_'.join(value) if isinstance(value, list) else value

# Apply conversion to both columns
cell_communication_df_filt['Gene 1'] = cell_communication_df_filt['Gene 1'].apply(convert_list_to_string)
cell_communication_df_filt['Gene 2'] = cell_communication_df_filt['Gene 2'].apply(convert_list_to_string)
gene_1 = [gene for gene in gene_1 if gene in cell_communication_df_filt['Gene 1'].unique()]
gene_2 = [gene for gene in gene_2 if gene in cell_communication_df_filt['Gene 2'].unique()]
cell_communication_df_filt['Gene 1'] = cell_communication_df_filt['Gene 1'].astype('category')
cell_communication_df_filt['Gene 1'] = cell_communication_df_filt['Gene 1'].cat.reorder_categories(gene_1[::-1])

cell_communication_df_filt['Gene 2'] = cell_communication_df_filt['Gene 2'].astype('category')
cell_communication_df_filt['Gene 2'] = cell_communication_df_filt['Gene 2'].cat.reorder_categories(gene_2)
fig = (
ggplot(data=cell_communication_df_filt, mapping=aes(x='Gene 2', y='Gene 1', color='log10_C', size='log10_FDR')) 
+ geom_point()
+ scale_color_gradient(low = "#FEE08B", high = "#5E4FA2")
+ theme_classic() 
+ theme(plot_title = element_text(hjust = 0.5,
                                margin={"t": 0, "b": 5, "l": 0, "r": 0},
                                size = 14,
                                face='bold'),
        # legend_position = "none",
        axis_title_x = element_text(size = 11),
        axis_title_y = element_text(size = 11),
        axis_text_x = element_text(margin={"t": 0, "b": 0, "l": 0, "r": 10}, size = 9, colour='black', rotation=90),
        axis_text_y = element_text(margin={"t": 0, "b": 0, "l": 0, "r": 10}, size = 9, colour='black'),
        panel_border = element_rect(color='black'),
        panel_background = element_rect(colour = "black",
                                        linewidth = 1),
        figure_size=(5, 4)) + ylab('Transporter / Ligand') + xlab('Transporter / Receptor')
)
fig.show()

Group spatially co-localized metabolites#

We now group spatially co-localized metabolites and assess if their presence is correlated with the predefined metabolic zones. For this, we will binarize the metabolite scores computed using the compute_interacting_cell_scores function, such that those spots with a value higher than 1 standard deviation above the mean will be assigned a value of 1, and 0 otherwise.

interacting_cell_scores_m = adata.uns['interacting_cell_results']['np']['m']['cs'].copy()
n=1
means = np.nanmean(interacting_cell_scores_m, axis=0)
stds = np.nanstd(interacting_cell_scores_m, axis=0)
thresholds = means + n*stds
interacting_cell_scores_m[interacting_cell_scores_m < thresholds] = 0
interacting_cell_scores_m = pd.DataFrame(interacting_cell_scores_m, index=adata.obs_names, columns=adata.uns['metabolites'])
interacting_cell_scores_m[interacting_cell_scores_m != 0] = 1

We create a new AnnData with the binarized metabolite scores to run the metabolic zonation pipeline.

metab_scores_adata = ad.AnnData(interacting_cell_scores_m)
metab_scores_adata.obs_names = adata.obs_names
metab_scores_adata.var_names = adata.uns['metabolites']

metab_scores_adata.obs['sample'] = adata.obs['sample']
metab_scores_adata.obsm['spatial'] = adata.obsm['spatial']

Here we compute the spatial proximity graph using the same parameters as before.

harreman.tl.compute_knn_graph(metab_scores_adata, 
                           compute_neighbors_on_key="spatial", 
                           neighborhood_radius=100,
                           weighted_graph=False,
                           sample_key='sample')

The code below is only run to filter out metabolites with all-zero values in at least one of the samples for subsequent pairwise correlation. We don’t select autocorrelated metabolites through this approach as they have already been selected using the select_significant_interactions function.

Here, the Bernoulli will be used, as the metabolite scores have been binarized and this is the best distribution to model the data.

harreman.hs.compute_local_autocorrelation(metab_scores_adata, model='bernoulli')
gene_autocorrelation_results = metab_scores_adata.uns['gene_autocorrelation_results']
metabolites = gene_autocorrelation_results.index

Pairwise correlation between binarized metabolite scores is also evaluated.

harreman.hs.compute_local_correlation(metab_scores_adata, genes=metabolites)

Then, metabolite groups are created.

harreman.hs.create_modules(metab_scores_adata, min_gene_threshold=10)
harreman.pl.local_correlation_plot(metab_scores_adata, mod_cmap='Set1')
../../_images/804623861599a84faf835d3685b5373fabfe8f1b9d8d594dd9dff0ccc47d7e0d.png

We also compute metabolite group scores to eventually correlate them with the previously computed metabolic module scores.

harreman.hs.calculate_module_scores(metab_scores_adata)
modules = metab_scores_adata.obsm['module_scores'].columns
metab_scores_adata.obs[modules] = metab_scores_adata.obsm['module_scores']
for sample in metab_scores_adata.obs['sample'].unique():
    print(sample)
    sample_adata = metab_scores_adata[metab_scores_adata.obs['sample'] == sample].copy()
    sc.pl.embedding(sample_adata, basis='spatial', color=modules, frameon=False, vmin="p1", vmax="p99", ncols=4)
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
../../_images/ff5966ecfae799f7204b8e877ba177aa3eb4fdf751ef0806fd92a567ea4b139e.png ../../_images/30e96296c763b6324aa810c3067eba2da5220c49f40314d2006a8386b7c9a824.png ../../_images/cbed218799f870c41735f3458877d729af80b6997d7f1eb5dee7405340c28150.png ../../_images/22c8c274346973f82b04e8973b2846915e4d6f76be4f265663efcae11946dda4.png ../../_images/619136d65d66b3d6382996041f422f61fc0659110ab7cbe8fc98adae961b8c77.png ../../_images/319580fc5cac94bfde11cbf18adc4086bcbec1815b3cb2fa034937a118382f16.png ../../_images/f38694e4cd74e7f1b1f4fceda9dc0ef9407fa39cfdf57d46fc756b6888dc41e0.png

Once metabolite groups have been computed, we might want to group similar ones together or ignore unspecific or noisy groups. For this, we can visualize the average pairwise Z-scores between groups.

harreman.pl.average_local_correlation_plot(metab_scores_adata, col_cluster=False, row_cluster=False, mod_cmap='Set1', show=False)
../../_images/a9a65c21e8574e8fa914c4d649d501d09eb1eebd4c728ebdf672bf73eb664f6e.png

Additionally, we can also visualize the correlations between metabolite group scores to identify groups with a similar spatial pattern.

harreman.pl.module_score_correlation_plot(metab_scores_adata, col_cluster=False, row_cluster=False, mod_cmap='Set1', show=False)
../../_images/cbc176eda7319500166af0e1f318ed40defb19cfc468776c35da403c84f0e52b.png

Eventually, metabolite modules can also be grouped together into super-groups and we recompute the scores using the same approach as in the metabolic zonation pipeline.

super_module_dict = {
    -1: [8, 9],
    1: [1, 2, 4],
    2: [3],
    3: [5, 6],
    4: [7],
}
harreman.hs.calculate_super_module_scores(metab_scores_adata, super_module_dict=super_module_dict)
Finished computing super-module scores in 6.580 seconds

We can then visualize the pairwise correlation plot at the super-group level.

harreman.pl.local_correlation_plot(metab_scores_adata, use_super_modules=True, show=False)
../../_images/984d10670bd580bfdb1c64da06fcfb39fdad6a2a0cef5d3cd918990388b54bd5.png

And super-group scores are computed.

super_modules = metab_scores_adata.obsm['super_module_scores'].columns
metab_scores_adata.obs[super_modules] = metab_scores_adata.obsm['super_module_scores']
for sample in metab_scores_adata.obs['sample'].unique():
    print(sample)
    sample_adata = metab_scores_adata[metab_scores_adata.obs['sample'] == sample].copy()
    sc.pl.embedding(sample_adata, basis='spatial', color=super_modules, frameon=False, vmin="p1", vmax="p99", ncols=4)
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
../../_images/fdcb9c5f644e41213c76c34e3c80e16812d9c2527d130922fec6af6b82b4ce49.png ../../_images/34a3b6394514b28305547b35b4dbb9bea2fbf2a3923fb0284c1eedd0272d9f88.png ../../_images/b7d340ffca35d6afdca9347dcfa8b2d28fae9702d121d194cffd274443c13909.png ../../_images/ab7a8fc5e5cc27e78d6d2184dfd48db83519493fe52052f6283e790ec68faf8e.png ../../_images/42f09b12672f33dbf0a212e8eb223a5facfb533d4b2d682bc9bf326c2fa6e6ba.png ../../_images/dbb6ea0d10aba8b2c50d1491d69be93fe6af9d70fabc74b38e5f594693e44f92.png ../../_images/1e1701bc48232e9edb0a03e2b28fd70c719639b4a2ae1508544a1c472cbf9638.png

Module / metabolite group correlation analysis#

We compute the correlation between metabolic module scores and metabolite group scores.

common_cells = adata.obsm['module_scores'].index.intersection(metab_scores_adata.obsm['super_module_scores'].index)
df1a = adata.obsm['module_scores'].loc[common_cells]
df2a = metab_scores_adata.obsm['super_module_scores'].loc[common_cells]

corr = pd.DataFrame(index=df1a.columns, columns=df2a.columns, dtype=float)
for m1 in df1a.columns:
    for m2 in df2a.columns:
        corr.at[m1, m2] = df1a[m1].corr(df2a[m2], method='spearman')
colors = ["#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999"]
palette_m = {mod: colors[int(mod.split(' ')[1])-1] for mod in adata.obs['top_module'].dropna().unique()}
palette = {
    'Module -1': "#BDBDBD",
    'Module 1': "#9E9AC8",
    'Module 2': "#ABDDA4",
    'Module 3': "#9ECAE1",
    'Module 4': "#F1B6DA",
}

And we then can visualize the correlation results.

vmin=-0.8
vmax=0.8
cmap=sns.color_palette("PRGn", as_cmap=True)
yticklabels=False

row_colors1 = pd.Series(
    [palette_m[i] for i in corr.index],
    index=corr.index,
)

row_colors = pd.DataFrame({
    "Modules": row_colors1,
})

col_colors1 = pd.Series(
    [palette[i] for i in corr.columns],
    index=corr.columns,
)

col_colors = pd.DataFrame({
    "Metabolite Modules": col_colors1,
})

cm = sns.clustermap(
    corr,
    vmin=vmin,
    vmax=vmax,
    cmap=cmap,
    xticklabels=False,
    yticklabels=yticklabels,
    row_colors=row_colors,
    col_colors=col_colors,
    rasterized=True,
)

fig = plt.gcf()
plt.sca(cm.ax_heatmap)
plt.ylabel("")
plt.xlabel("")

cm.ax_row_dendrogram.remove()
cm.ax_col_dendrogram.remove()

plt.sca(cm.ax_row_colors)

# Find the colorbar 'child' and modify
min_delta = 1e99
min_aa = None
for aa in fig.get_children():
    try:
        bbox = aa.get_position()
        delta = (0-bbox.xmin)**2 + (1-bbox.ymax)**2
        if delta < min_delta:
            delta = min_delta
            min_aa = aa
    except AttributeError:
        pass

min_aa.set_ylabel('Spearman R')
min_aa.yaxis.set_label_position("left")
../../_images/9a4c00d16832c3cd02c311ec2e6ebf0d3c53c4942fde00f7118d4492359e8735.png

Cell-type-specific metabolic crosstalk#

In the final step of the Harreman pipeline, we will perform cell-type-specific metabolic crosstalk inference by focusing on a few metabolites of interest and their corresponding significant gene pairs.

To assign cell types to spots, we can do different things. On the one hand, if the dataset contains single cells instead of spots (not in this case), we could directly annotate the cells and then run the pipeline directly. On the other hand, we could run DestVI (Lopez et al., Nature biotechnology, 2022) or another cell type deconvolution algorithm (such as cell2location; Kleshchevnikov et al., Nature biotechnology, 2022) to infer the cell type proportions in every spot and then either (1) assign the cell type with the highest (z-normalized) proportion to every spot, or (2) use the imputed cell-type-specific gene expression counts to achieve a higher resolution (cell types per spot).

Here, we will use the DestVI deconvolution results but, instead of running the algorithm on the imputed cell-type-specific counts, we will assign a given cell type (the one with the highest z-normalized proportion) to every spot.

temp_dir_obj = tempfile.TemporaryDirectory()
st_adata_path = os.path.join(temp_dir_obj.name, "Liu_et_al_human_lung_DestVI_v2.h5ad")
st_adata = sc.read(st_adata_path, backup_url='https://figshare.com/ndownloader/files/60231869')
adata = adata[adata.obs_names.isin(st_adata.obs_names)].copy()
cell_types = [ct for ct in st_adata.obsm['proportions'].columns if 'additional' not in ct]
df_z = st_adata.obsm['proportions'][cell_types].apply(zscore, axis=0)
assigned_cell_types = df_z.idxmax(axis=1)
adata.obs['cell_type'] = assigned_cell_types

We load the cell-type-agnostic crosstalk results to select the statistically significant gene pairs of the metabolites of interest.

metabolites = ['L-Lactate', 'L-Arginine', 'Sodium_calcium exchange', 'Adenosine diphosphate ribose']
cell_communication_df = adata.uns['ccc_results']['cell_com_df_gp_sig'].copy()
gene_pairs_per_metabolite = adata.uns['gene_pairs_per_metabolite']
    
def to_tuple(x):
    # Recursively convert lists to tuples
    if isinstance(x, list):
        return tuple(to_tuple(i) for i in x)
    return x

metabolite_gene_pair_df = pd.DataFrame.from_dict(gene_pairs_per_metabolite, orient="index").reset_index()
metabolite_gene_pair_df = metabolite_gene_pair_df.rename(columns={"index": "metabolite"})
metabolite_gene_pair_df['gene_pair'] = metabolite_gene_pair_df['gene_pair'].apply(
    lambda arr: [(to_tuple(gp[0]), to_tuple(gp[1])) for gp in arr]
)
metabolite_gene_pair_df['gene_type'] = metabolite_gene_pair_df['gene_type'].apply(
    lambda arr: [(to_tuple(gt[0]), to_tuple(gt[1])) for gt in arr]
)
metabolite_gene_pair_df = pd.concat([
    metabolite_gene_pair_df['metabolite'],
    metabolite_gene_pair_df.explode('gene_pair')['gene_pair'],
    metabolite_gene_pair_df.explode('gene_type')['gene_type'],
], axis=1).reset_index(drop=True)

if 'LR_database' in adata.uns:
    LR_database = adata.uns['LR_database']
    df_merged = pd.merge(metabolite_gene_pair_df, LR_database, left_on='metabolite', right_on='interaction_name', how='left')
    LR_df = df_merged.dropna(subset=['pathway_name'])
    metabolite_gene_pair_df['metabolite'][metabolite_gene_pair_df.metabolite.isin(LR_df.metabolite)] = LR_df['pathway_name']
metabolite_gene_pair_df = metabolite_gene_pair_df.set_index('metabolite')
def is_present(row, gene_pairs):
    gene1, gene2 = row['Gene 1'], row['Gene 2']
    return any(
        (gene1 in pair and gene2 in pair) if isinstance(pair, tuple) else False
        for pair in gene_pairs
    )

gene_pairs = metabolite_gene_pair_df.loc[metabolites]['gene_pair'].tolist()
cell_communication_df_filt = cell_communication_df[cell_communication_df.apply(lambda row: is_present(row, gene_pairs), axis=1)]

We finally select the significant gene pairs that belong to the metabolites of interest that we will use in this analysis.

gene_pairs_filt = list(zip(cell_communication_df_filt["Gene 1"], cell_communication_df_filt["Gene 2"]))

First of all, we can optionally rerun the code below to extract the interaction database and compute the spatial proximity graph. Given that this was already done in the cell-type-agnostic analysis, it is not necessary to do it again.

harreman.pp.extract_interaction_db(adata, species='human', database='both', verbose=True)

harreman.tl.compute_knn_graph(adata,
                        compute_neighbors_on_key="spatial", 
                        neighborhood_radius=100,
                        weighted_graph=False,
                        sample_key='sample',
                        verbose=True)

To compute cell-type-specific gene pairs, unlike the previous time, the compute_gene_pairs function with the cell_type_key = 'cell_type' parameter is used.

harreman.tl.compute_gene_pairs(adata, cell_type_key='cell_type', verbose=True)

To infer cell-type-specific metabolic crosstalk, the compute_ct_cell_communication function is used. Cell type informatin is specified in the cell_type_key = 'cell_type' parameter. To assess statistical significance, we can either use the parametric test (test = "parametric"), the non-parametric one (test = "non-parametric"), or both of them (test = "both"). If the parametric test needs to be computed, the model parameter needs to be specified, in addition to the raw counts layer (layer_key_p_test parameter). In this case, we use the Bernoulli distribution to model the count data. In case the non-parametric test is used, the number of permutations is specified through the M parameter (1000 by default), as well as the layer of the raw or normalized count data (layer_key_np_test parameter). In our case, we use the log-normalized counts to infer crosstalk and assess its significance through the non-parametric test.

Additionally, the metabolites of interest as well as the significant gene pairs (from the cell-type-agnostic statistic) associated with them are used.

fix_gp = False #True or False

gene_pairs_filt_tmp = [(x, list(y.split(' - ')) if ' - ' in y else y) for x, y in gene_pairs_filt]
gene_pairs_filt_new = [(list(x.split(' - ')) if ' - ' in x else x, y) for x, y in gene_pairs_filt_tmp]

harreman.tl.compute_ct_cell_communication(adata, model='bernoulli', cell_type_key='cell_type', M = 1000, test = "both", layer_key_p_test='counts', layer_key_np_test='log_norm', 
                                          subset_gene_pairs=gene_pairs_filt_new, subset_metabolites=metabolites, fix_gp=fix_gp, verbose=True)

harreman.tl.select_significant_interactions(adata, test = "non-parametric", ct_aware = True, threshold = 0.05)

harreman.tl.compute_ct_interacting_cell_scores(adata, test = "both", verbose=True, device='cpu')
filename = 'Slide_seq_lung_ct_Harreman_fix_gp.h5ad' if fix_gp else 'Slide_seq_lung_ct_Harreman.h5ad'
harreman.write_h5ad(adata, filename = filename)

Communication heat map#

To visualize the results, we will compute a communication heat map that shows the results of both hypotheses at the same time. For this, we will use the AnnData files saved previously.

gp_adata = harreman.read_h5ad('Slide_seq_lung_ct_Harreman_fix_gp.h5ad')
adata = harreman.read_h5ad('Slide_seq_lung_ct_Harreman.h5ad')
harreman.tl.select_significant_interactions(gp_adata, test = "non-parametric", ct_aware = True, threshold = 0.05)
harreman.tl.select_significant_interactions(adata, test = "non-parametric", ct_aware = True, threshold = 0.01)
cell_com_df_m_sig_gp = gp_adata.uns['ct_ccc_results']['cell_com_df_m'].copy()
cell_com_df_m_sig = adata.uns['ct_ccc_results']['cell_com_df_m'].copy()
cell_types = ['B cell', 'CD8+ T cell', 'DC', 'Endothelial', 'Fibroblast',
       'Macrophage', 'Mast cell', 'Monocyte', 'NK cell', 
       'Plasma cell', 'T-Helper', 'Tumor', 'Undetermined']
cell_com_df_m_sig_gp[['Cell Type 1', 'Cell Type 2']] = cell_com_df_m_sig_gp[['Cell Type 1', 'Cell Type 2']].replace('TAM', 'Macrophage').replace('NK', 'NK cell').replace('Misc/Undetermined', 'Undetermined')
cell_com_df_m_sig[['Cell Type 1', 'Cell Type 2']] = cell_com_df_m_sig[['Cell Type 1', 'Cell Type 2']].replace('TAM', 'Macrophage').replace('NK', 'NK cell').replace('Misc/Undetermined', 'Undetermined')
cell_com_df_m_sig_gp = cell_com_df_m_sig_gp[(cell_com_df_m_sig_gp['Cell Type 1'].isin(cell_types)) & (cell_com_df_m_sig_gp['Cell Type 2'].isin(cell_types))]
cell_com_df_m_sig = cell_com_df_m_sig[(cell_com_df_m_sig['Cell Type 1'].isin(cell_types)) & (cell_com_df_m_sig['Cell Type 2'].isin(cell_types))]
cell_com_df_m_sig_full_gp = pd.concat([
    cell_com_df_m_sig_gp,
    cell_com_df_m_sig_gp.rename(columns={'Cell Type 1': 'Cell Type 2', 'Cell Type 2': 'Cell Type 1'})
], ignore_index=True).drop_duplicates()

cell_com_df_m_sig_full = pd.concat([
    cell_com_df_m_sig,
    cell_com_df_m_sig.rename(columns={'Cell Type 1': 'Cell Type 2', 'Cell Type 2': 'Cell Type 1'})
], ignore_index=True).drop_duplicates()
cell_com_df_m_sig_full_metab_gp = cell_com_df_m_sig_full_gp[cell_com_df_m_sig_full_gp['metabolite'] == 'L-Lactate'].copy()
cell_com_df_m_sig_full_metab = cell_com_df_m_sig_full[cell_com_df_m_sig_full['metabolite'] == 'L-Lactate'].copy()

diag_lines = cell_com_df_m_sig_full_metab_gp[cell_com_df_m_sig_full_metab_gp['selected']].copy()

# For plotting diagonal lines, define tile corners
diag_lines['x'] = diag_lines['Cell Type 2']
diag_lines['y'] = diag_lines['Cell Type 1']
diag_lines['xend'] = diag_lines['Cell Type 2']
diag_lines['yend'] = diag_lines['Cell Type 1']

# Map to numeric (for line coordinates)
val_sum = 0.5
x_order = {k: i+1 for i, k in enumerate(cell_types)}
diag_lines['x0'] = diag_lines['x'].map(x_order) - val_sum
diag_lines['x1'] = diag_lines['x'].map(x_order) + val_sum
diag_lines['y0'] = diag_lines['y'].map(x_order) - val_sum
diag_lines['y1'] = diag_lines['y'].map(x_order) + val_sum

p = (
    ggplot(cell_com_df_m_sig_full_metab, aes(x='Cell Type 2', y='Cell Type 1')) +
    # Base tiles
    geom_tile(aes(fill='selected'), color='white', show_legend=False) +
    scale_fill_manual(values={True: '#C6DBEF', False: 'white'}) +

    # Diagonal lines for second condition
    geom_segment(
        diag_lines,
        aes(x='x1', y='y0', xend='x0', yend='y1'),
        color='black',
        size=1
    ) +

    # Aesthetics
    theme_classic() +
    theme(axis_text_x=element_text(rotation=45, ha='right', size=12),
            axis_text_y=element_text(size=12),
            axis_title_x=element_text(size=14),
            axis_title_y=element_text(size=14),
            figure_size=(5, 5)) +
    labs(x='Cell Type 2', y='Cell Type 1', fill='-log10(FDR)', title='L-Lactate')
)
p
cell_com_df_m_sig_full_metab_gp = cell_com_df_m_sig_full_gp[cell_com_df_m_sig_full_gp['metabolite'] == 'Sodium_calcium exchange'].copy()
cell_com_df_m_sig_full_metab = cell_com_df_m_sig_full[cell_com_df_m_sig_full['metabolite'] == 'Sodium_calcium exchange'].copy()

diag_lines = cell_com_df_m_sig_full_metab_gp[cell_com_df_m_sig_full_metab_gp['selected']].copy()

# For plotting diagonal lines, define tile corners
diag_lines['x'] = diag_lines['Cell Type 2']
diag_lines['y'] = diag_lines['Cell Type 1']
diag_lines['xend'] = diag_lines['Cell Type 2']
diag_lines['yend'] = diag_lines['Cell Type 1']

# Map to numeric (for line coordinates)
val_sum = 0.5
x_order = {k: i+1 for i, k in enumerate(cell_types)}
diag_lines['x0'] = diag_lines['x'].map(x_order) - val_sum
diag_lines['x1'] = diag_lines['x'].map(x_order) + val_sum
diag_lines['y0'] = diag_lines['y'].map(x_order) - val_sum
diag_lines['y1'] = diag_lines['y'].map(x_order) + val_sum

p = (
    ggplot(cell_com_df_m_sig_full_metab, aes(x='Cell Type 2', y='Cell Type 1')) +
    # Base tiles
    geom_tile(aes(fill='selected'), color='white', show_legend=False) +
    scale_fill_manual(values={True: '#ABDDA4', False: 'white'}) +

    # Diagonal lines for second condition
    geom_segment(
        diag_lines,
        aes(x='x1', y='y0', xend='x0', yend='y1'),
        color='black',
        size=1
    ) +

    # Aesthetics
    theme_classic() +
    theme(axis_text_x=element_text(rotation=45, ha='right', size=12),
            axis_text_y=element_text(size=12),
            axis_title_x=element_text(size=14),
            axis_title_y=element_text(size=14),
            figure_size=(5, 5)) +
    labs(x='Cell Type 2', y='Cell Type 1', fill='-log10(FDR)', title='Sodium/calcium exchange')
)
p

Cell-type-specific interacting cell scores#

Finally, we visualize the cell-type-specific interacting cell scores with the plot_ct_interacting_cell_scores function. For this, we can select the metabolites or gene pairs of interest through the interactions parameter and the cell types of interest through the cell_type_pair parameter. The latter accepts either a list of cell types (which visualizes every pair a given cell type is part of) or a list of cell type pairs (tuples), where we only visualize the interacting scores corresponding to the specified cell type pairs. Here, we also use the agg_only = True parameter, which aggregates for the cell types of interest the interaction scores in which a given cell type is present and visualizes only these scores.

harreman.pl.plot_ct_interacting_cell_scores(adata, interactions=['L-Lactate', 'Sodium_calcium exchange'], cell_type_pair=['Tumor', 'TAM', 'Endothelial'], agg_only=True, 
                                            test='non-parametric', coords_obsm_key='spatial', s=1, vmin=0, vmax='p99.9', normalize_values=True, cmap='Blues', sample_specific=True)
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
Puck_220408_20
Puck_220408_13
Puck_220408_15
Puck_200727_09
Puck_200727_10
Puck_200727_08
Puck_220408_14
../../_images/2c008f671886725abf2ea383476d517ff5f942432ee25ad5738fdeb3e9270982.png ../../_images/a12ff44c82510ba321b33a867799e874cc2790dd83d48d31444e02e85806a567.png ../../_images/e423500e75068fc1477e8848a79808f28037711ec42e3c5518da9dca4491d8e2.png ../../_images/12286fb470e5bbf296819f8b396c632a8b506680a000aeec3017aa08875ee335.png ../../_images/86641d709c8753ebe3b640e0025b973a6f4e279f51dd9a64ac318d24c0d8ba88.png ../../_images/8476be39b86df8bc47c9eb2ce909fcf44796c2fd04a091f87cfadc479708c725.png ../../_images/7ea6cf6d9569efba2721cd1e5167fd8278c4769e52958e8df9afc8406c554ae9.png ../../_images/c828e074c585af10a76aed458312aceb4c5aec4b8b0f62d1e009448b9cd47ed5.png ../../_images/ce6134e703a299704d1609bb46d2289269408ddbe1715e7ffea0a224d89a5ad8.png ../../_images/4eceb6f728a13276b8ed774606b447edd6c72ce4b123e55cbaae97a112a73f5a.png ../../_images/cd726c10309b9e7bb5abb08f5455f0bb626e1ea229b5594ec301ff56e6ea5eaa.png ../../_images/8080e0db52af3468dbdd929e6c47e6af9d914640cf1fe8e2b9df9beea45cb584.png ../../_images/584d850dc2f06a8071849868315d306e85bb608b7c7f4f022116ca19e57d248b.png ../../_images/264bd88906bcc94780a03dbb7520db80e1e4f032cced249c0a23ce73e640b5d2.png ../../_images/2cb8f38084f4b1192d755cbdca60528e6b068638a1df0d7eea2a09430bfb891a.png ../../_images/fbd4bf62f9f2bfa0b4fc557cb53b6aac542065dc3602f22a2076108cace0533a.png ../../_images/40b8e629c466cb6ed8ef17e4e91766940bba64af41e76d3a9d2d3f8998ee56b9.png ../../_images/3ebecce11c37021a129288b70595b89b86d1d87381afb014da6d9b44b940e59e.png ../../_images/1704db00b6c63ad386b3a30b5d9827bb8314c955b0c594f22c835ec426b8a643.png ../../_images/f526c0c7dd6a2df9ad9a5e026374f034d1414f3584c9e6c245dfdd0bf6be503e.png ../../_images/208d529a2b7bf731b1d9008f96e02ae94cf28679c2f3be9bbcfd4cd0ddb6f28d.png ../../_images/35e5cc6d000953e89623ddec38631608a59175375a8ac6d4c85e925139ed8c49.png ../../_images/3a33c60cd38b540e4025a0bed0769df9a962601be259f42dbd68ea1b9c7e9eac.png ../../_images/012945943e2c47ef6b6f87ef97a6f869a89390a70b90dba88fd26939fcf94ff8.png ../../_images/10acb5aa009f83dc43c98de0b2cddb8a16ff63bd903ea4c973103959dfea6dba.png ../../_images/41100dff2432ef8e8de11dacb5aafff61c22d492c01e4c8e618ca197d8289cfb.png ../../_images/6ccaab87b41a62b2be3f58ba13b7704a7589760eb01d6e6a34af7e712d6f029c.png ../../_images/db02a25911b7fe7e8d827a8021e25616d63967bb5ca06ec4dbbb3197ec039cbe.png ../../_images/d2b4a6f7ab4f4d56f3e80db80c1f59f3cb2dc2b124193dd3ec12ea5f221220c5.png ../../_images/59df20046b9dcb7ba7c14411c0504636377e3033f4786556c4ca824976ca161b.png ../../_images/05c7f52b3b036709d6994ad7ea568eeaf1ce7b8d99eff69af9f4dfd2247bf954.png ../../_images/80e5dce791a605ec7f675a879b20b49c7f1653660618fc29afd569de3be24e93.png ../../_images/daae19b06cbb62a98d5d89d9bdad5d63d3dae2129f118e380b7840175b403c5e.png ../../_images/a31a077debf1f61450f29fe6cf3066862d8a6b40b0c04397390ac9d807c6b595.png ../../_images/6d48460b7c14a827711d004198aefb01bdd473c8f020774c0146c200c198c9db.png