From 571b16f8c7d6df2730c9c4864fc4593d77659a6f Mon Sep 17 00:00:00 2001 From: r-trimbour Date: Sun, 14 Jul 2024 19:45:25 +0200 Subject: [PATCH 01/18] update to use multiple random on only one shared multilayer object --- R/explore_network.R | 2 +- hummuspy/src/hummuspy/config.py | 158 ++- hummuspy/src/hummuspy/core_grn.py | 1009 ++++++++++++++++++++ hummuspy/src/hummuspy/create_multilayer.py | 395 ++++---- hummuspy/src/hummuspy/explore_network.py | 967 ++----------------- 5 files changed, 1431 insertions(+), 1100 deletions(-) create mode 100644 hummuspy/src/hummuspy/core_grn.py diff --git a/R/explore_network.R b/R/explore_network.R index 439535d..61cacbe 100644 --- a/R/explore_network.R +++ b/R/explore_network.R @@ -511,7 +511,7 @@ define_output <- function( suffix_bipartites = suffix_bipartites) # define target_genes with hummuspy function - output <- hummuspy$explore_network$get_output_from_dicts( + output <- hummuspy$core_grn$get_output_from_dicts( output_request = output_type, multilayer_f = multilayer_f, multiplexes_list = multiplexes_dictionary, diff --git a/hummuspy/src/hummuspy/config.py b/hummuspy/src/hummuspy/config.py index dd21bc1..190d91d 100644 --- a/hummuspy/src/hummuspy/config.py +++ b/hummuspy/src/hummuspy/config.py @@ -2,9 +2,10 @@ import yaml import os +import numpy +import pandas + import matplotlib.pyplot as plt -import numpy as np -import pandas as pd import networkx as nx @@ -284,13 +285,13 @@ def open_config(filename): config_dic = yaml.load(file, Loader=yaml.BaseLoader) if "lamb" in config_dic.keys(): - config_dic["lamb"] = pd.DataFrame( + config_dic["lamb"] = pandas.DataFrame( config_dic["lamb"], index=list(config_dic["multiplex"].keys()), columns=list(config_dic["multiplex"].keys()) ).astype(float) if "eta" in config_dic.keys(): - config_dic["eta"] = pd.Series( + config_dic["eta"] = pandas.Series( config_dic["eta"], index=list(config_dic["multiplex"].keys()) ).astype(float) @@ -298,10 +299,85 @@ def open_config(filename): return config_dic +def process_config( + config, + multilayer_folder='multilayer' +): + """ + Process config file to be used to create CreateMultilayer object. + + Parameters + ---------- + config : dict + Configuration dictionary. + Contains the following keys: + - multiplex : dict + - graph_type : list + - layers : list + - bipartite : dict + - edge_list_df : str + - graph_type : str + - source : str + - target : str + - eta : pandas.Series + - lamb : pandas.DataFrame + - seed : str + multilayer_folder : str, optional + + Returns + ------- + dict + Processed configuration dictionary. + Contains the following keys: + - multiplex : dict + - names : list + - graph_type : list + - layers : list + - bipartite : dict + - edge_list_df : str + - graph_type : str + - source : str + - target : str + - eta : list + - lamb : list[list] + - seeds : str + """ + + config = dict(config) + for multiplex in config["multiplex"]: + new_multiplex = { + "names": [multiplex+"_"+str(i+1) for i in range( + len(config["multiplex"][multiplex]['layers']))], + "graph_type": config["multiplex"][multiplex]['graph_type'], + "layers": [os.path.join(multilayer_folder, path) for path + in config["multiplex"][multiplex]["layers"]] + } + config['multiplex'][multiplex] = new_multiplex + + for bipartite in config['bipartite']: + config['bipartite'][bipartite]['edge_list_df'] = os.path.join( + multilayer_folder, bipartite) + + config['eta'] = config['eta'].to_list() + config['lamb'] = numpy.array(config['lamb']).tolist() + seeds_path = os.path.join(multilayer_folder, config['seed']) + if os.path.exists(seeds_path): + config['seeds'] = pandas.read_csv( + seeds_path, header=None).iloc[:, 0].values.tolist() + else: + config['seeds'] = [] + del config['seed'] + if 'r' in config.keys(): + config['restart_proba'] = config['r'] + del config['r'] + + return config + + def old_setup_proba_config( config: dict, - eta: Union[list[float], pd.Series, np.ndarray], - lamb: pd.DataFrame): + eta: Union[list[float], pandas.Series, numpy.ndarray], + lamb: pandas.DataFrame): """ Setup the RWR probability for the exploration of hummus networks with the given eta and lambda values. The lambda values are normalised (per rows) to sum to 1. @@ -316,7 +392,7 @@ def old_setup_proba_config( lamb: list[list[float]] The lambda values for the RWR probability. - e.g.: lamb = pd.DataFrame(np.ones((3,3)), + e.g.: lamb = pandas.DataFrame(numpy.ones((3,3)), index = ['TF', 'Gene', 'Peak''], columns = ['TF', 'Gene', 'Peak']) lamb.loc[i, j] corresponds to the probability @@ -359,9 +435,9 @@ def old_setup_proba_config( assert check_lamb(lamb, config),\ "lamb is not a valid probability matrix according to bipartites" # Transform eta to list if it's a pandas Series or a numpy array - if type(eta) == pd.Series: + if type(eta) == pandas.Series: eta = eta.values.tolist() - elif type(eta) == np.ndarray: + elif type(eta) == numpy.ndarray: eta = eta.tolist() # Add eta and lamb to config @@ -373,8 +449,8 @@ def old_setup_proba_config( def setup_proba_config( config: dict, - eta: Union[pd.Series, np.ndarray, list[float]], - lamb: Union[pd.DataFrame, np.ndarray] + eta: Union[pandas.Series, numpy.ndarray, list[float]], + lamb: Union[pandas.DataFrame, numpy.ndarray] ): """ Setup the RWR probability for the exploration of hummus networks with the given eta and lambda values. @@ -384,16 +460,16 @@ def setup_proba_config( ---------- config: dict The config dictionary. - eta: Union[pd.Series, np.array] + eta: Union[pandas.Series, numpy.array] The eta values for the RWR probability, must sum up to 1. - e.g.: pd.Series([0.5, 0.5, 0], index = ['TF', 'peaks', 'RNA']) + e.g.: pandas.Series([0.5, 0.5, 0], index = ['TF', 'peaks', 'RNA']) If a pandas Series is provided, the index must be the layer names. If a numpy array is provided, the order of the values must be the same as the order of the layers in the config. - lamb: pd.DataFrame + lamb: pandas.DataFrame The lambda values for the RWR probability. - e.g.: lamb = pd.DataFrame(np.ones((3,3)), + e.g.: lamb = pandas.DataFrame(numpy.ones((3,3)), index = ['TF', 'Gene', 'Peak''], columns = ['TF', 'Gene', 'Peak']) lamb.loc[i, j] corresponds to the probability @@ -424,14 +500,14 @@ def setup_proba_config( [0.5, 0, 0.5], [0, 0.5, 0.5]]} """ - if type(eta) == np.array or type(eta) == list: + if type(eta) == numpy.array or type(eta) == list: # Check that the length of eta is equal to the number of layers assert len(config['multiplex']) == len(eta),\ "eta (length : {}) should be as long as the number of layers ({})"\ .format(len(eta), len(config['multiplex'])) # Transform eta to pandas Series - eta = pd.Series(eta, index=config['multiplex'].keys()) - elif type(eta) == pd.Series: + eta = pandas.Series(eta, index=config['multiplex'].keys()) + elif type(eta) == pandas.Series: # Check that the index of eta are the layer names assert eta.index.tolist().sort() == list( config['multiplex'].keys()).sort(),\ @@ -440,7 +516,7 @@ def setup_proba_config( else: raise TypeError("eta should be a numpy array or a pandas Series") - if type(lamb) == np.array: + if type(lamb) == numpy.array: # Check that lamb is a square matrix of size len(config['multiplex']) assert lamb.shape[0] == lamb.shape[1],\ "lamb should be a square matrix" @@ -448,9 +524,9 @@ def setup_proba_config( "lamb should be a square matrix of size {}".format( len(config['multiplex'])) # Transform lamb to pandas DataFrame - lamb = pd.DataFrame(lamb, index=config['multiplex'].keys(), + lamb = pandas.DataFrame(lamb, index=config['multiplex'].keys(), columns=config['multiplex'].keys()) - elif type(lamb) == pd.DataFrame: + elif type(lamb) == pandas.DataFrame: # Check that lamb index and columns are the layer names assert lamb.index.tolist().sort() == list( config['multiplex'].keys()).sort(),\ @@ -477,9 +553,9 @@ def setup_proba_config( lamb = lamb.loc[config['multiplex'].keys(), config['multiplex'].keys()] # Transform eta to list if it's a pandas Series or a numpy array - if type(eta) == pd.Series: + if type(eta) == pandas.Series: eta = eta.values.tolist() - elif type(eta) == np.ndarray: + elif type(eta) == numpy.ndarray: eta = eta.tolist() # Add eta and lamb to config @@ -503,13 +579,13 @@ def initialise_lamb(config, ordered_multiplexes = config['multiplex'].keys() if value == 1: - array = np.ones((len(ordered_multiplexes), len(ordered_multiplexes))) + array = numpy.ones((len(ordered_multiplexes), len(ordered_multiplexes))) elif value == 0: - array = np.zeros((len(ordered_multiplexes), len(ordered_multiplexes))) + array = numpy.zeros((len(ordered_multiplexes), len(ordered_multiplexes))) else: raise ValueError('value param of initialise_lamb should be 1 or 0') - lamb = pd.DataFrame(array, + lamb = pandas.DataFrame(array, index=ordered_multiplexes, columns=ordered_multiplexes) return lamb @@ -521,7 +597,7 @@ def get_single_layer_eta(config, starting_multiplex='RNA'): assert starting_multiplex in ordered_multiplex,\ "It seems starting_multiplex not in config['multiplex']" - eta = pd.Series([0 for k in ordered_multiplex], + eta = pandas.Series([0 for k in ordered_multiplex], index=ordered_multiplex) eta[starting_multiplex] = 1 return eta @@ -701,7 +777,7 @@ def get_max_lamb(config, directed=True, draw=False, figsize=(7, 7)): max_lamb: pandas.DataFrame The maximum lamb matrix according to bipartites. Structure: - pd.DataFrame([[0.5, 0.5, 0], + pandas.DataFrame([[0.5, 0.5, 0], [1/3, 1/3, 1/3], [0, 0.5, 0.5]], index = ['TF', 'Peak', 'RNA'], @@ -731,7 +807,7 @@ def get_max_lamb(config, directed=True, draw=False, figsize=(7, 7)): # Create an empty dataframe with layer names as index and columns # that we'll fill where it's possible according to the bipartites - max_lamb = pd.DataFrame(np.zeros((len(config['multiplex']), + max_lamb = pandas.DataFrame(numpy.zeros((len(config['multiplex']), len(config['multiplex']))), index=config['multiplex'].keys(), columns=config['multiplex'].keys()) @@ -746,7 +822,7 @@ def get_max_lamb(config, directed=True, draw=False, figsize=(7, 7)): else: max_lamb += max_lamb.transpose() # filling the diagonal to allow intra-layer exploration - max_lamb += np.eye(len(config['multiplex'])).astype(int) + max_lamb += numpy.eye(len(config['multiplex'])).astype(int) # normalise per rows max_lamb = max_lamb.div(max_lamb.sum(axis=0), axis=1) @@ -780,7 +856,7 @@ def check_lamb(lamb, config, directed=True, draw=False): # Count number of locations where lamb has no non-zero value # and max_lamb is zero - X = np.sum(np.sum((max_lamb - lamb) < 0, axis=0), axis=0) + X = numpy.sum(numpy.sum((max_lamb - lamb) < 0, axis=0), axis=0) # If X > 0, lamb is not a valid probability matrix according to bipartites. if X > 0: @@ -824,7 +900,7 @@ def draw_lamb(df, figsize=(7, 7), node_color='blue'): df: pandas.DataFrame The lamb matrix. Structure: - pd.DataFrame([[0.5, 0.5, 0], + pandas.DataFrame([[0.5, 0.5, 0], [1/3, 1/3, 1/3], [0, 0.5, 0.5]], index = ['TF', 'Peak', 'RNA'], @@ -844,7 +920,7 @@ def draw_lamb(df, figsize=(7, 7), node_color='blue'): Examples -------- - >>> lamb = pd.DataFrame([[0.5, 0.5, 0], + >>> lamb = pandas.DataFrame([[0.5, 0.5, 0], [1/3, 1/3, 1/3], [0, 0.5, 0.5]], index = ['TF', 'Peak', 'RNA'], @@ -861,7 +937,7 @@ def draw_lamb(df, figsize=(7, 7), node_color='blue'): # Create a directed graph from the lamb matrix G = nx.from_pandas_adjacency(df.transpose(), create_using=nx.DiGraph()) - pos = {list(G.nodes)[-i-1]: np.array((0, i)) + pos = {list(G.nodes)[-i-1]: numpy.array((0, i)) for i in range(-len(G.nodes), 0)} # Draw the graph without edge labels @@ -894,7 +970,7 @@ def draw_lamb(df, figsize=(7, 7), node_color='blue'): # Draw self edges labels # We add 0.3 to the y coordinate to avoid overlapping with the node - pos_self_labels = {k: np.array([0, pos[k][1]+0.30]) for k in pos} + pos_self_labels = {k: numpy.array([0, pos[k][1]+0.30]) for k in pos} my_draw_networkx_edge_labels(G, pos_self_labels, edge_labels=self_edges, @@ -1027,11 +1103,11 @@ def my_draw_networkx_edge_labels( x1 * label_pos + x2 * (1.0 - label_pos), y1 * label_pos + y2 * (1.0 - label_pos), ) - pos_1 = ax.transData.transform(np.array(pos[n1])) - pos_2 = ax.transData.transform(np.array(pos[n2])) + pos_1 = ax.transData.transform(numpy.array(pos[n1])) + pos_2 = ax.transData.transform(numpy.array(pos[n2])) linear_mid = 0.5*pos_1 + 0.5*pos_2 d_pos = pos_2 - pos_1 - rotation_matrix = np.array([(0, 1), + rotation_matrix = numpy.array([(0, 1), (-1, 0)]) ctrl_1 = linear_mid + rad*rotation_matrix@d_pos ctrl_mid_1 = 0.5*pos_1 + 0.5*ctrl_1 @@ -1041,16 +1117,16 @@ def my_draw_networkx_edge_labels( if rotate: # in degrees - angle = np.arctan2(y2 - y1, x2 - x1) / (2.0 * np.pi) * 360 + angle = numpy.arctan2(y2 - y1, x2 - x1) / (2.0 * numpy.pi) * 360 # make label orientation "right-side-up" if angle > 90: angle -= 180 if angle < -90: angle += 180 # transform data coordinate angle to screen coordinate angle - xy = np.array((x, y)) + xy = numpy.array((x, y)) trans_angle = ax.transData.transform_angles( - np.array((angle,)), xy.reshape((1, 2)) + numpy.array((angle,)), xy.reshape((1, 2)) )[0] else: trans_angle = 0.0 diff --git a/hummuspy/src/hummuspy/core_grn.py b/hummuspy/src/hummuspy/core_grn.py new file mode 100644 index 0000000..bb33295 --- /dev/null +++ b/hummuspy/src/hummuspy/core_grn.py @@ -0,0 +1,1009 @@ +import numpy +from typing import Union +import pandas +import hummuspy.config +import hummuspy.explore_network + + +def format_multilayer( + TF_layer: Union[list, pandas.DataFrame], + ATAC_layer: Union[list, pandas.DataFrame], + RNA_layer: Union[list, pandas.DataFrame], + TF_ATAC_bipartite: pandas.DataFrame, + ATAC_RNA_bipartite: pandas.DataFrame, + TF_layer_graph_type: Union[str, list] = '00', + ATAC_layer_graph_type: Union[str, list] = '01', + RNA_layer_graph_type: Union[str, list] = '01', + TF_ATAC_bipartite_graph_type: str = '00', + ATAC_RNA_bipartite_graph_type: str = '00' +): + + """ Format layers and bipartites data as needed for HuMMuS/MultiXrank + + ! The DataFrame should have 2-3 columns : + ['source', 'target', 'weight'], or ['source', 'target'] ! + #TODO : explain graph_type !! + Args: + TF_layer (Union[list, pandas.DataFrame]): TF layer(s) edge list + ATAC_layer (Union[list, pandas.DataFrame]): ATAC layer(s) edge list + RNA_layer (Union[list, pandas.DataFrame]): RNA layer(s) edge list + TF_ATAC_bipartite (pandas.DataFrame): TF-ATAC bipartite edge list + ATAC_RNA_bipartite (pandas.DataFrame): ATAC-RNA bipartite edge list + TF_layer_graph_type (Union[str, list], optional): + Graph type for TF layer(s). + Defaults to '00'. + ATAC_layer_graph_type (Union[str, list], optional): + Graph type for ATAC layer(s). + Defaults to '01'. + RNA_layer_graph_type (Union[str, list], optional): + Graph type for RNA layer(s). + Defaults to '01'. + TF_ATAC_bipartite_graph_type (str, optional): + Graph type for TF-ATAC bipartite. + Defaults to '00'. + ATAC_RNA_bipartite_graph_type (str, optional): + Graph type for ATAC-RNA bipartite. + Defaults to '00'. + + Returns: + dict: Formatted data as input for MultiXrank-HuMMuS class + """ + if type(TF_layer) is pandas.DataFrame: + TF_layer = [TF_layer] + if type(ATAC_layer) is pandas.DataFrame: + ATAC_layer = [ATAC_layer] + if type(RNA_layer) is pandas.DataFrame: + RNA_layer = [RNA_layer] + + if type(TF_layer_graph_type) is str: + TF_layer_graph_type = [TF_layer_graph_type]*len(TF_layer) + if type(ATAC_layer_graph_type) is str: + ATAC_layer_graph_type = [ATAC_layer_graph_type]*len(ATAC_layer) + if type(RNA_layer_graph_type) is str: + RNA_layer_graph_type = [RNA_layer_graph_type]*len(RNA_layer) + + assert len(TF_layer) == len(TF_layer_graph_type), \ + "TF_layer and TF_layer_graph_type must have the same length" + assert len(ATAC_layer) == len(ATAC_layer_graph_type), \ + "ATAC_layer and ATAC_layer_graph_type must have the same length" + assert len(RNA_layer) == len(RNA_layer_graph_type), \ + "RNA_layer and RNA_layer_graph_type must have the same length" + + keys_data = ['names', 'graph_type', 'layers'] + # Multiplex + multiplex = { + 'TF': {key: [] for key in keys_data}, + 'ATAC': {key: [] for key in keys_data}, + 'RNA': {key: [] for key in keys_data} + } + + for i, layer in enumerate(TF_layer): + multiplex['TF']['names'].append(f'TF_{i}') + multiplex['TF']['graph_type'].append(TF_layer_graph_type[i]) + multiplex['TF']['layers'].append(layer) + + for i, layer in enumerate(ATAC_layer): + multiplex['ATAC']['names'].append(f'ATAC_{i}') + multiplex['ATAC']['graph_type'].append(ATAC_layer_graph_type[i]) + multiplex['ATAC']['layers'].append(layer) + + for i, layer in enumerate(RNA_layer): + multiplex['RNA']['names'].append(f'RNA_{i}') + multiplex['RNA']['graph_type'].append(RNA_layer_graph_type[i]) + multiplex['RNA']['layers'].append(layer) + + # Bipartite + bipartite = { + 'TF_ATAC': { + 'source': 'TF', + 'target': 'ATAC', + 'edge_list_df': TF_ATAC_bipartite, + 'graph_type': TF_ATAC_bipartite_graph_type}, + + 'ATAC_RNA': { + 'source': 'ATAC', + 'target': 'RNA', + 'edge_list_df': ATAC_RNA_bipartite, + 'graph_type': ATAC_RNA_bipartite_graph_type} + } + + return multiplex, bipartite + + +def init_GRN( + TF_layer: Union[list, pandas.DataFrame], + ATAC_layer: Union[list, pandas.DataFrame], + RNA_layer: Union[list, pandas.DataFrame], + TF_ATAC_bipartite: pandas.DataFrame, + ATAC_RNA_bipartite: pandas.DataFrame, + TF_layer_graph_type: Union[str, list] = '00', + ATAC_layer_graph_type: Union[str, list] = '10', + RNA_layer_graph_type: Union[str, list] = '10', + TF_ATAC_bipartite_graph_type: str = '00', + ATAC_RNA_bipartite_graph_type: str = '00' +): + + multiplex, bipartite = format_multilayer( + TF_layer=TF_layer, + ATAC_layer=ATAC_layer, + RNA_layer=RNA_layer, + TF_ATAC_bipartite=TF_ATAC_bipartite, + ATAC_RNA_bipartite=ATAC_RNA_bipartite, + TF_layer_graph_type=TF_layer_graph_type, + ATAC_layer_graph_type=ATAC_layer_graph_type, + RNA_layer_graph_type=RNA_layer_graph_type, + TF_ATAC_bipartite_graph_type=TF_ATAC_bipartite_graph_type, + ATAC_RNA_bipartite_graph_type=ATAC_RNA_bipartite_graph_type + ) + + params = { + 'multiplex': multiplex, + 'bipartite': bipartite + } + + lamb = hummuspy.config.get_grn_lamb( + params, + tf_multiplex='TF', + peak_multiplex='ATAC', + rna_multiplex='RNA') + eta = hummuspy.config.get_single_layer_eta( + params, + 'TF') + + return multiplex, bipartite, eta, lamb + + +def get_GRN( + TF_layer: Union[list, pandas.DataFrame], + ATAC_layer: Union[list, pandas.DataFrame], + RNA_layer: Union[list, pandas.DataFrame], + TF_ATAC_bipartite: pandas.DataFrame, + ATAC_RNA_bipartite: pandas.DataFrame, + seeds: Union[list, str], + TF_layer_graph_type: Union[str, list] = '00', + ATAC_layer_graph_type: Union[str, list] = '01', + RNA_layer_graph_type: Union[str, list] = '01', + TF_ATAC_bipartite_graph_type: str = '00', + ATAC_RNA_bipartite_graph_type: str = '00', + n_jobs=1 +): + + multiplex, bipartite, eta, lamb = init_GRN( + TF_layer=TF_layer, + ATAC_layer=ATAC_layer, + RNA_layer=RNA_layer, + TF_ATAC_bipartite=TF_ATAC_bipartite, + ATAC_RNA_bipartite=ATAC_RNA_bipartite, + TF_layer_graph_type=TF_layer_graph_type, + ATAC_layer_graph_type=ATAC_layer_graph_type, + RNA_layer_graph_type=RNA_layer_graph_type, + TF_ATAC_bipartite_graph_type=TF_ATAC_bipartite_graph_type, + ATAC_RNA_bipartite_graph_type=ATAC_RNA_bipartite_graph_type + ) + print("initialization done.") + + if type(seeds) is str: + seeds = [seeds] + print(seeds) + + ranking = hummuspy.explore_network.compute_multiple_RandomWalk( + multiplex, + bipartite, + eta=eta.tolist(), + lamb=numpy.array(lamb).tolist(), + seeds=seeds, + save=False, + spec_layer_result_saved='RNA', + n_jobs=n_jobs) + + return ranking + + +############################################# +# 1/4 Define GRN config and compute results # +############################################# +def define_grn_from_config( + multilayer_f, + config, + gene_list=None, + tf_list=None, + config_name='grn_config.yml', + config_folder='config', + tf_multiplex: str = 'TF', + peak_multiplex: str = 'peaks', + rna_multiplex: str = 'RNA', + update_config=True, + save=False, + return_df=True, + output_f=None, + njobs=1): + """Define a GRN from a multilayer network and a config file. + Random walks are computed for each gene in the gene list and we keep + the probability to reach each TF in the TF list. + You can provide a list of genes and TFs to restrict the GRN. + The gene_list is used as individual seed for computing the random walks. + The list of TFs is used after the random walks, filtering the results to + only the TFs of interest. + You can choose to save the result in a file and/or return it. + + Parameters + ---------- + multilayer_f : str + Path to the multilayer folder. + config : dict + Config dictionnary. + gene_list : list, optional + List of genes. The default is 'all'. + tf_list : list, optional + List of TFs. The default is 'all'. + config_name : str, optional + Name of the config file that will be saved. + The default is 'grn_config.yml'. + config_folder : str, optional + Name of the config folder where the config will be save. + ! For each seed (sometimes thousands), a file should be created in this + folder. The default is 'config'. + tf_multiplex : str, optional + Name of the TF multiplex. The default is 'TF'. + peak_multiplex : str, optional + Name of the peak multiplex. The default is 'peaks'. + rna_multiplex : str, optional + Name of the RNA multiplex. The default is 'RNA'. + update_config : bool, optional + Update the config file. The default is True ; if False, the config + file won't be updated for the values of eta and lamb. + save : bool, optional + Save the result. The default is False. If True, you need to provide + an output_f name to save the GRN result. + return_df : bool, optional + Return the result. The default is True. + output_f : str, optional + Name of the output file. The default is None. Only used if save=True. + njobs : int, optional + Number of jobs. The default is 1. If >1, the seeds will be saved in + different files (in the multilayer subfolder 'seed') and the random + walks will be parallelised. + Returns + ------- + df : pandas.DataFrame + Dataframe containing the random walks's results that defines the GRN. + Columns: + layer : str + Name of the target layer. + path_layer : str + Name of the layer of the path. + score : float + Score of the random walk. + gene : str + Name of the gene-seed. + tf : str + Name of the TF-target. + + """ + # store mutliplex already because it will be when saving yaml file, + # while eta and lambda won't. + config['multiplex'] = {k: config['multiplex'][k] + for k in sorted(config['multiplex'].keys())} + + if update_config: + eta = hummuspy.config.get_single_layer_eta(config, + rna_multiplex) + + lamb = hummuspy.config.get_grn_lamb(config, + tf_multiplex, + peak_multiplex, + rna_multiplex, + draw=False) + config['eta'] = eta + config['lamb'] = lamb + # config = hummuspy.config.setup_proba_config(config, eta, lamb) + + if gene_list is None: + gene_list = [] + for layer in config['multiplex'][rna_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + gene_list = numpy.unique(numpy.concatenate([gene_list, layer_nodes] + )) + + # process the config to the right format to + # compute the random walks without local saving + config = hummuspy.config.process_config(config, multilayer_f) + + df = hummuspy.explore_network.compute_multiple_RandomWalk( + **config, + output_f=output_f, + seeds=gene_list, + save=False, + return_df=return_df, + spec_layer_result_saved=tf_multiplex, + njobs=njobs) + + df['gene'] = df['seed'] + df['tf'] = df['target'] + del df['target'] + del df['seed'] + + if tf_list is None: + tf_list = [] + for layer in config['multiplex'][tf_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + tf_list = numpy.unique(numpy.concatenate([tf_list, layer_nodes])) + tf_list = tf_list[tf_list != 'fake_node'] + + # Add normalisation ? + df = df[df['tf'].isin(tf_list)] + + if save is True: + assert output_f is not None, 'You need to provide an output_f name ' +\ + 'to save the GRN result' + df.sort_values(by='score', ascending=False).to_csv(output_f, + sep='\t', + index=False, + header=True) + if return_df: + return df + + +############################################################################### +############################################# +# 2/4 Define enhancers config and compute results # +############################################# +def define_enhancers_from_config( + multilayer_f, + config, + gene_list=None, + peak_list=None, + config_name='enhancers_config.yml', + config_folder='config', + tf_multiplex: str = 'TF', + peak_multiplex: str = 'peaks', + rna_multiplex: str = 'RNA', + update_config=True, + save=False, + return_df=True, + output_f=None, + njobs=1): + """Return enhancers prediction from a multilayer network and a config file. + Random walks are computed for each gene in the gene list and we keep + the probability to reach each peak in the peak list. + You can provide a peak_list and a gene_list to restrict the predictions. + The gene_list is used as individual seed for computing the random walks. + The list of peaks is used after the random walks, filtering the results to + only the peaks of interest. + You can choose to save the result in a file and/or return it. + + Parameters + ---------- + multilayer_f : str + Path to the multilayer folder. + config : dict + Config dictionnary. + gene_list : list, optional + List of genes. The default is 'all'. + peak_list : list, optional + List of peaks. The default is 'all'. + config_name : str, optional + Name of the config file that will be saved. + The default is 'enhancers_config.yml'. + config_folder : str, optional + Name of the config folder where the config will be save. + ! For each seed (sometimes thousands), a file should be created in this + folder. The default is 'config'. + tf_multiplex : str, optional + Name of the TF multiplex. The default is 'TF'. + peak_multiplex : str, optional + Name of the peak multiplex. The default is 'peaks'. + rna_multiplex : str, optional + Name of the RNA multiplex. The default is 'RNA'. + update_config : bool, optional + Update the config file. The default is True ; if False, the config + file won't be updated for the values of eta and lamb. + save : bool, optional + Save the result. The default is False. If True, you need to provide + an output_f name to save the predictions. + return_df : bool, optional + Return the result. The default is True. + output_f : str, optional + Name of the output file. The default is None. Only used if save=True. + njobs : int, optional + Number of jobs. The default is 1. If >1, the seeds will be saved in + different files (in the multilayer subfolder 'seed') and the random + walks will be parallelised. + Returns + ------- + df : pandas.DataFrame + Dataframe of the random walks's results that defines the predictions. + Columns: + layer : str + Name of the target layer. + path_layer : str + Name of the layer of the path. + score : float + Score of the random walk. + gene : str + Name of the gene-seed. + peak : str + Name of the peak-target. + """ + # store mutliplex already because it will be when saving yaml file, + # while eta and lambda won't. + config['multiplex'] = {k: config['multiplex'][k] + for k in sorted(config['multiplex'].keys())} + + if update_config: + # Indicate layer where to start the random walks : rna_multiplex + eta = hummuspy.config.get_single_layer_eta(config, + rna_multiplex) + + # Define proba matrix to jump between layer : rna <--> peaks + lamb = hummuspy.config.get_enhancers_lamb(config, + tf_multiplex, + peak_multiplex, + rna_multiplex, + draw=False) + + config['eta'] = eta + config['lamb'] = lamb + # config = hummuspy.config.setup_proba_config(config, eta, lamb) + + config_path = multilayer_f+'/'+config_folder+'/'+config_name + hummuspy.config.save_config(config, config_path) + + if gene_list is None: + gene_list = [] + for layer in config['multiplex'][rna_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + gene_list = numpy.unique(numpy.concatenate([gene_list, layer_nodes] + )) + + df = hummuspy.explore_network.compute_multiple_RandomWalk( + multilayer_f, + config_name=config_name, + output_f=output_f, + list_seeds=gene_list, + config_folder=config_folder, + save=False, + return_df=return_df, + spec_layer_result_saved=peak_multiplex, + # save only peaks proba + njobs=njobs) + + df['gene'] = df['seed'] + df['peak'] = df['target'] + del df['target'] + del df['seed'] + + if peak_list is None: + peak_list = [] + for layer in config['multiplex'][peak_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + peak_list = numpy.unique(numpy.concatenate([peak_list, layer_nodes] + )) + + # Add normalisation ? + df = df[df['peak'].isin(peak_list)] + + if save is True: + assert output_f is not None, 'You need to provide an output_f name ' +\ + 'to save the enhancers prediction result.' + df.sort_values(by='score', ascending=False).to_csv(output_f, + sep='\t', + index=False, + header=True) + if return_df: + return df + + +######################################################### +# 3/4 Define binding regions config and compute results # +######################################################### +def define_binding_regions_from_config( + multilayer_f, + config, + tf_list=None, + peak_list=None, + config_name='binding_regions_config.yml', + config_folder='config', + tf_multiplex: str = 'TF', + peak_multiplex: str = 'peaks', + rna_multiplex: str = 'RNA', + update_config=True, + save=False, + return_df=True, + output_f=None, + njobs=1): + """Return binding regions prediction from a multilayer network and a config + file. Random walks are computed for each TF in the TF list and we keep the + probability to reach each peak in the peak list. + You can provide a list of peaks and a tf_list to restrict the predictions. + The list of TFs is used as individual seed for computing the random walks. + The list of peaks is used after the random walks, filtering the results to + only the peaks of interest. + You can choose to save the result in a file and/or return it. + + + Parameters + ---------- + multilayer_f : str + Path to the multilayer folder. + config : dict + Config dictionnary. + tf_list : list, optional + List of TFs. The default is 'all'. + peak_list : list, optional + List of peaks. The default is 'all'. + config_name : str, optional + Name of the config file that will be saved. + The default is 'binding_regions_config.yml'. + config_folder : str, optional + Name of the config folder where the config will be save. + ! For each seed (sometimes thousands), a file should be created in this + folder. The default is 'config'. + tf_multiplex : str, optional + Name of the TF multiplex. The default is 'TF'. + peak_multiplex : str, optional + Name of the peak multiplex. The default is 'peaks'. + rna_multiplex : str, optional + Name of the RNA multiplex. The default is 'RNA'. + update_config : bool, optional + Update the config file. The default is True ; if False, the config + file won't be updated for the values of eta and lamb. + save : bool, optional + Save the result. The default is False. If True, you need to provide + an output_f name to save the predictions. + return_df : bool, optional + Return the result. The default is True. + output_f : str, optional + Name of the output file. The default is None. Only used if save=True. + njobs : int, optional + Number of jobs. The default is 1. If >1, the seeds will be saved in + different files (in the multilayer subfolder 'seed') and the random + walks will be parallelised. + + Returns + ------- + df : pandas.DataFrame + Dataframe of the random walks's results that defines the predictions. + Columns: + layer : str + Name of the target layer. + path_layer : str + Name of the layer of the path. + score : float + Score of the random walk. + tf : str + Name of the TF-seed. + peak : str + Name of the peak-target. + """ + # store mutliplex already because it will be when saving yaml file, + # while eta and lambda won't. + config['multiplex'] = {k: config['multiplex'][k] + for k in sorted(config['multiplex'].keys())} + + if update_config: + # Indicate layer where to start the random walks : rna_multiplex + eta = hummuspy.config.get_single_layer_eta(config, + tf_multiplex) + + # Define proba matrix to jump between layer : rna <--> peaks + lamb = hummuspy.config.get_binding_regions_lamb(config, + tf_multiplex, + peak_multiplex, + rna_multiplex, + draw=False) + config['eta'] = eta + config['lamb'] = lamb + # config = hummuspy.config.setup_proba_config(config, eta, lamb) + + config_path = multilayer_f+'/'+config_folder+'/'+config_name + hummuspy.config.save_config(config, config_path) + + if tf_list is None: + tf_list = [] + for layer in config['multiplex'][tf_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + tf_list = numpy.unique(numpy.concatenate([tf_list, layer_nodes] + )) + tf_list = tf_list[tf_list != 'fake_node'] + + df = hummuspy.explore_network.compute_multiple_RandomWalk( + multilayer_f, + config_name=config_name, + output_f=output_f, + list_seeds=tf_list, + config_folder=config_folder, + save=False, + return_df=return_df, + spec_layer_result_saved=peak_multiplex, + # save only peaks proba + njobs=njobs) + + df['tf'] = df['seed'] + df['peak'] = df['target'] + del df['target'] + del df['seed'] + + if peak_list is None: + peak_list = [] + for layer in config['multiplex'][peak_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + peak_list = numpy.unique(numpy.concatenate([peak_list, layer_nodes] + )) + + # Add normalisation ? + df = df[df['peak'].isin(peak_list)] + + if save is True: + assert output_f is not None, 'You need to provide an output_f name ' +\ + 'to save the enhancers prediction result.' + df.sort_values(by='score', ascending=False).to_csv(output_f, + sep='\t', + index=False, + header=True) + if return_df: + return df + + +###################################################### +# 4/4 Define target genes config and compute results # +###################################################### +def define_target_genes_from_config( + multilayer_f, + config, + gene_list=None, + tf_list=None, + config_name='target_genes_config.yml', + config_folder='config', + tf_multiplex: str = 'TF', + peak_multiplex: str = 'peaks', + rna_multiplex: str = 'RNA', + update_config=True, + save=False, + return_df=True, + output_f=None, + njobs=1): + """Return target genes prediction from a multilayer network and a config + file. Random walks are computed for each TF in the TF list and we keep the + probability to reach each gene in the gene list. + You can provide a list of genes and a tf_list to restrict the predictions. + The list of TFs is used as individual seed for computing the random walks. + The list of genes is used after the random walks, filtering the results to + only the genes of interest. + You can choose to save the result in a file and/or return it. + + Parameters + ---------- + multilayer_f : str + Path to the multilayer folder. + config : dict + Config dictionnary. + gene_list : list, optional + List of genes. The default is 'all'. + tf_list : list, optional + List of TFs. The default is 'all'. + config_name : str, optional + Name of the config file that will be saved. + The default is 'target_genes_config.yml'. + config_folder : str, optional + Name of the config folder where the config will be save. + ! For each seed (sometimes thousands), a file should be created in this + folder. The default is 'config'. + tf_multiplex : str, optional + Name of the TF multiplex. The default is 'TF'. + peak_multiplex : str, optional + Name of the peak multiplex. The default is 'peaks'. + rna_multiplex : str, optional + Name of the RNA multiplex. The default is 'RNA'. + update_config : bool, optional + Update the config file. The default is True ; if False, the config + file won't be updated for the values of eta and lamb. + save : bool, optional + Save the result. The default is False. If True, you need to provide + an output_f name to save the predictions. + return_df : bool, optional + Return the result. The default is True. + output_f : str, optional + Name of the output file. The default is None. Only used if save=True. + njobs : int, optional + Number of jobs. The default is 1. If >1, the seeds will be saved in + different files (in the multilayer subfolder 'seed') and the random + walks will be parallelised. + + Returns + ------- + df : pandas.DataFrame + Dataframe of the random walks's results that defines the predictions. + Columns: + layer : str + Name of the target layer. + path_layer : str + Name of the layer of the path. + score : float + Score of the random walk. + tf : str + Name of the TF-seed. + gene : str + Name of the gene-target. + """ + # store mutliplex already because it will be when saving yaml file, + # while eta and lambda won't. + config['multiplex'] = {k: config['multiplex'][k] + for k in sorted(config['multiplex'].keys())} + + if update_config: + eta = hummuspy.config.get_single_layer_eta(config, + tf_multiplex) + + lamb = hummuspy.config.get_target_genes_lamb(config, + tf_multiplex, + peak_multiplex, + rna_multiplex, + draw=False) + + config['eta'] = eta + config['lamb'] = lamb + # config = hummuspy.config.setup_proba_config(config, eta, lamb) + + config_path = multilayer_f+'/'+config_folder+'/'+config_name + hummuspy.config.save_config(config, config_path) + + if gene_list is None: + gene_list = [] + for layer in config['multiplex'][rna_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + gene_list = numpy.unique(numpy.concatenate([gene_list, layer_nodes] + )) + + if tf_list is None: + tf_list = [] + for layer in config['multiplex'][tf_multiplex]['layers']: + df_layer = pandas.read_csv( + multilayer_f+'/'+layer, + sep='\t', + header=None, + index_col=None) + + layer_nodes = numpy.concatenate([ + numpy.unique(df_layer[0].values), + numpy.unique(df_layer[1].values)]) + tf_list = numpy.unique(numpy.concatenate([tf_list, layer_nodes] + )) + tf_list = tf_list[tf_list != 'fake_node'] + + df = hummuspy.explore_network.compute_multiple_RandomWalk( + multilayer_f, + config_name=config_name, + output_f=output_f, + list_seeds=tf_list, + config_folder=config_folder, + save=False, + return_df=return_df, + spec_layer_result_saved=rna_multiplex, + njobs=njobs) + + df['tf'] = df['seed'] + df['gene'] = df['target'] + del df['target'] + del df['seed'] + + # Add normalisation ? + df = df[df['gene'].isin(gene_list)] + + if save is True: + assert output_f is not None, 'You need to provide an output_f name ' +\ + 'to save the GRN result' + df.sort_values(by='score', ascending=False).to_csv(output_f, + sep='\t', + index=False, + header=True) + if return_df: + return df + + +def get_output_from_dicts( + output_request: str, + multilayer_f, + multiplexes_list, + bipartites_list, + folder_multiplexes='multiplex', + folder_bipartites='bipartite', + gene_list=None, + tf_list=None, + peak_list=None, + config_filename='config.yml', + config_folder='config', + tf_multiplex: str = 'TF', + peak_multiplex: str = 'peaks', + rna_multiplex: str = 'RNA', + bipartites_type=('00', '00'), + update_config=True, + save=False, + return_df=True, + output_f=None, + njobs=1): + """ + Compute an output from a multilayer network and a config file, that can be + chosen among ['grn', 'enhancers', 'binding_regions', 'target_genes']. + + It is a wrapper of the functions define_*_from_config, that are called + depending on the output_request parameter. + + Parameters + ---------- + output_request : ['grn', 'enhancers', 'binding_regions', 'target_genes'] + Type of output requested. + multilayer_f : str + Path to the multilayer folder. + config : dict + Config dictionnary. + gene_list : list, optional + List of genes. The default is 'all'. + tf_list : list, optional + List of TFs. The default is 'all'. + config_name : str, optional + Name of the config file. The default is 'config.yml'. + config_folder : str, optional + Name of the config folder. The default is 'config'. + tf_multiplex : str, optional + Name of the TF multiplex. The default is 'TF'. + peak_multiplex : str, optional + Name of the peak multiplex. The default is 'peaks'. + rna_multiplex : str, optional + Name of the RNA multiplex. The default is 'RNA'. + update_config : bool, optional + Update the config file. The default is True. + save : bool, optional + Save the result. The default is False. + return_df : bool, optional + Return the result. The default is True. + output_f : str, optional + Name of the output file. The default is None. + njobs : int, optional + Number of jobs. The default is 1. + + Returns + -------ith open(self.config_path) as fin: + self.config_dic = yaml.load(fin, Loader=yaml.BaseLoader) + df : pandas.DataFrame + Dataframe containing the random walks's results that defines the GRN. + Columns: + layer : str + Name of the target layer. + + path_layer : str + Name of the layer of the path. + score : float + Score of the random walk. + gene : str + Name of the gene-seed. + tf : str + Name of the TF-target. + + """ + + njobs = int(njobs) + print('multiplexes_list : ', multiplexes_list) + print('bipartites_list : ', bipartites_list) + print('folder_multiplexes : ', folder_multiplexes) + print('folder_bipartites : ', folder_bipartites) + print('gene_list : ', gene_list) + print('tf_list : ', tf_list) + print('peak_list : ', peak_list) + print('config_filename : ', config_filename) + print('config_folder : ', config_folder) + print('tf_multiplex : ', tf_multiplex) + print('peak_multiplex : ', peak_multiplex) + print('rna_multiplex : ', rna_multiplex) + print('update_config : ', update_config) + print('save : ', save) + print('return_df : ', return_df) + print('output_f : ', output_f) + print('njobs : ', njobs) + + # Create general config file + config = hummuspy.config.general_config( + multiplexes=multiplexes_list, + bipartites=bipartites_list, + folder_multiplexes=folder_multiplexes, + folder_bipartites=folder_bipartites, + suffix='.tsv', + self_loops=0, + restart_prob=0.7, + bipartites_type=bipartites_type, + save_configfile=False, + config_filename=config_filename) + + parameters = { + 'multilayer_f': multilayer_f, + 'config': config, + 'gene_list': gene_list, + 'peak_list': peak_list, + 'tf_list': tf_list, + 'config_name': config_filename, + 'config_folder': config_folder, + 'tf_multiplex': tf_multiplex, + 'peak_multiplex': peak_multiplex, + 'rna_multiplex': rna_multiplex, + 'update_config': update_config, + 'save': save, + 'return_df': return_df, + 'output_f': output_f, + 'njobs': njobs + } + + if output_request == 'grn': + del parameters['peak_list'] + df = define_grn_from_config(**parameters) + + elif output_request == 'enhancers': + del parameters['tf_list'] + df = define_enhancers_from_config(**parameters) + + elif output_request == 'binding_regions': + del parameters['gene_list'] + df = define_binding_regions_from_config(**parameters) + + elif output_request == 'target_genes': + del parameters['peak_list'] + df = define_target_genes_from_config(**parameters) + else: + raise ValueError("Please select an output_request value in ('grn', 'enhancers', 'binding_regions', 'target_genes').") + + return df diff --git a/hummuspy/src/hummuspy/create_multilayer.py b/hummuspy/src/hummuspy/create_multilayer.py index eee51f9..ea80cd9 100644 --- a/hummuspy/src/hummuspy/create_multilayer.py +++ b/hummuspy/src/hummuspy/create_multilayer.py @@ -1,9 +1,16 @@ +from distributed import LocalCluster, Client +import joblib +import networkx +import pandas +from fractions import Fraction import networkx import numpy import os import pandas import sys +from joblib import delayed, Parallel, dump, load + from multixrank.logger_setup import logger import itertools import sys @@ -23,14 +30,25 @@ from multixrank.Parameters import Parameters from multixrank.logger_setup import logger from multixrank.TransitionMatrix import TransitionMatrix - from typing import Union +from rich.progress import track + + +class MissingSeedError(Exception): + pass class Bipartite: """Bipartite layer""" - def __init__(self, key, abspath, graph_type, self_loops, on_disk=True, edge_list_df=None): + def __init__( + self, + key, + abspath, + graph_type, + self_loops, + on_disk=True, + edge_list_df=None): """ @@ -50,15 +68,16 @@ def __init__(self, key, abspath, graph_type, self_loops, on_disk=True, edge_list self.edge_list_df = edge_list_df self.on_disk = on_disk - if self.on_disk == True: + if self.on_disk is True: if not os.path.isfile(abspath): # error if path not exist - logger.error("This path does not exist: {}".format(abspath)) - sys.exit(1) + raise FileNotFoundError("This path does not exist: {}".format( + abspath)) if not (graph_type in ['00', '10', '01', '11']): - logger.error('MultiplexLayer multigraph type must take one of these values: 00, 10, 01, 11. ' - 'Current value: {}'.format(graph_type)) - sys.exit(1) + raise FileNotFoundError( + 'MultiplexLayer multigraph type must take one of these values' + + ': 00, 10, 01, 11. Current value: {}'.format(graph_type)) + self.graph_type = graph_type self.self_loops = self_loops @@ -84,7 +103,7 @@ def networkx(self) -> networkx.Graph: if self.graph_type[0] == '1': # undirected/directed layer (0/1) networkx_graph_obj = networkx.DiGraph() - if self.on_disk == True: + if self.on_disk is True: multiplex_layer_edge_list_df = pandas.read_csv(self.abspath, sep="\t", header=None, names=names, dtype=dtype, usecols=usecols) else: multiplex_layer_edge_list_df = self.edge_list_df @@ -93,7 +112,7 @@ def networkx(self) -> networkx.Graph: if not self.self_loops: multiplex_layer_edge_list_df = multiplex_layer_edge_list_df.loc[ ~(multiplex_layer_edge_list_df.col1 == multiplex_layer_edge_list_df.col2)] - multiplex_layer_edge_list_df['network_key'] = self.key + multiplex_layer_edge_list_df.loc[:, 'network_key'] = self.key self._networkx = networkx.from_pandas_edgelist( df=multiplex_layer_edge_list_df, source='col2', target='col1', @@ -104,10 +123,9 @@ def networkx(self) -> networkx.Graph: # networkx has no edges # TODO replace edges with nodes if len(self._networkx.edges()) == 0: - logger.error( + raise NoEdgeNetworkError( 'The following bipartite graph does not return any edge: {}'.format( self.key)) - sys.exit(1) return self._networkx @@ -141,15 +159,15 @@ def __init__(self, key, abspath, graph_type, multiplex, tau, self_loops, on_disk self.edge_list_df = edge_list_df self.on_disk = on_disk - if self.on_disk == True: + if self.on_disk is True: if not os.path.isfile(abspath): # error if path not exist - logger.error("This path does not exist: {}".format(abspath)) - sys.exit(1) + raise FileNotFoundError("This path does not exist: {}".format( + abspath)) if not (graph_type in ['00', '10', '01', '11']): - logger.error('MultiplexLayer multigraph type must take one of these values: 00, 10, 01, 11. ' - 'Current value: {}'.format(graph_type)) - sys.exit(1) + raise ValueError( + 'MultiplexLayer multigraph type must take one of these values' + + ': 00, 10, 01, 11. Current value: {}'.format(graph_type)) self._networkx = None @@ -177,27 +195,35 @@ def networkx(self) -> networkx.Graph: if self.graph_type[0] == '1': # undirected/directed layer (0/1) networkx_graph_obj = networkx.DiGraph() - if self.on_disk == True: + if self.on_disk is True: multiplex_layer_edge_list_df = pandas.read_csv( - self.abspath, sep="\t", header=None, names=name_lst, dtype=dtype_dic, keep_default_na=False, usecols=usecols) + self.abspath, + sep="\t", + header=None, + names=name_lst, + dtype=dtype_dic, + keep_default_na=False, + usecols=usecols) else: multiplex_layer_edge_list_df = self.edge_list_df # remove df lines with self-loops, ie source==target if bipartite_notes=true if not self.self_loops: multiplex_layer_edge_list_df = multiplex_layer_edge_list_df.loc[ ~(multiplex_layer_edge_list_df.source == multiplex_layer_edge_list_df.target)] - multiplex_layer_edge_list_df['network_key'] = self.key + multiplex_layer_edge_list_df.loc[:, 'network_key'] = self.key self._networkx = networkx.from_pandas_edgelist( df=multiplex_layer_edge_list_df, source='source', - target='target', create_using=networkx_graph_obj, edge_attr=edge_attr) + target='target', create_using=networkx_graph_obj, + edge_attr=edge_attr) - self._networkx.remove_edges_from(networkx.selfloop_edges(self._networkx)) + self._networkx.remove_edges_from( + networkx.selfloop_edges(self._networkx)) # networkx has no edges if len(self._networkx.nodes()) == 0: - logger.error( - 'The following multiplex layer does not return any edge: {}'.format(self.key)) - sys.exit(1) + raise NoEdgeNetworkError( + 'The following multiplex layer' + + 'does not return any edge: {}'.format(self.key)) return self._networkx @@ -206,7 +232,13 @@ class Seed: """Handle seeds""" - def __init__(self, path: str, multiplexall: MultiplexAll, seeds: list=list()): + def __init__( + self, + path: str, + multiplexall: + MultiplexAll, seeds: + list = list() + ): """Handles seeds Args: @@ -229,7 +261,6 @@ def __init__(self, path: str, multiplexall: MultiplexAll, seeds: list=list()): def seed_list(self) -> list: """Get list with all nodes""" - ################################################################### # # Check seed list @@ -237,17 +268,15 @@ def seed_list(self) -> list: ################################################################### if len(self._seed_list) <= 0: # no seeds - logger_setup.logger.error( + raise MissingSeedError( "These seed nodes were not found in the network: {}".format( self._seed_list)) - sys.exit(1) seeds_not_in_multiplex_set = set(self._seed_list) - set(self._multiplexall_obj.nodes) if len(seeds_not_in_multiplex_set) > 0: # seeds not in multiplex - logger_setup.logger.error( + raise MissingSeedError( "These seed nodes were not found in the network: {}".format( seeds_not_in_multiplex_set)) - sys.exit(1) return self._seed_list @@ -276,28 +305,44 @@ def get_seed_scores(self, transition) -> (numpy.array, pandas.DataFrame): """ - multiplexall_layer_count_list = [len(m.layer_tuple) for m in self._multiplexall_obj.multiplex_tuple] - multiplexall_node_count_list = [len(m.nodes) for m in self._multiplexall_obj.multiplex_tuple] - multiplexall_node_list2d = [multiplexone_obj.nodes for multiplexone_obj in self._multiplexall_obj.multiplex_tuple] + multiplexall_layer_count_list = [ + len(m.layer_tuple) for m in self._multiplexall_obj.multiplex_tuple] + multiplexall_node_count_list = [ + len(m.nodes) for m in self._multiplexall_obj.multiplex_tuple] + multiplexall_node_list2d = [ + multiplexone_obj.nodes + for multiplexone_obj in self._multiplexall_obj.multiplex_tuple] multiplexall_layer_key_list = [] multiplexall_layer_key_list2d = [] for multiplex in self._multiplexall_obj.multiplex_tuple: - multiplexall_layer_key_list2d.append([layer_obj.key for layer_obj in multiplex.layer_tuple]) + multiplexall_layer_key_list2d.append( + [layer_obj.key for layer_obj in multiplex.layer_tuple]) for layer in multiplex.layer_tuple: multiplexall_layer_key_list.append(layer.key) - multiplexall_layer_key_lst = [layer.key for multiplex in self._multiplexall_obj.multiplex_tuple for layer in multiplex.layer_tuple] - seed_score_df = pandas.DataFrame(0, index=self._seed_list, columns=multiplexall_layer_key_lst) + multiplexall_layer_key_lst = [layer.key for multiplex + in self._multiplexall_obj.multiplex_tuple + for layer in multiplex.layer_tuple] + seed_score_df = pandas.DataFrame(0.0, index=self._seed_list, + columns=multiplexall_layer_key_lst) prox_vector = numpy.zeros((numpy.shape(transition)[0], 1)) for seed_label in seed_score_df.index: # loop through seeds - for multiplex_idx, multiplex in enumerate(self._multiplexall_obj.multiplex_tuple): + for multiplex_idx, multiplex in enumerate( + self._multiplexall_obj.multiplex_tuple): if seed_label in multiplex.nodes: for layer_idx, layer in enumerate(multiplex.layer_tuple): seed_score_df.loc[seed_label, layer.key] = multiplex.eta * layer.tau / len(self.multiplex_seed_list2d[multiplex_idx]) - start = sum(numpy.array(multiplexall_node_count_list[:multiplex_idx]) * numpy.array(multiplexall_layer_count_list[:multiplex_idx])) + (multiplexall_node_count_list[multiplex_idx] * layer_idx) - pos = multiplexall_node_list2d[multiplex_idx].index(seed_label) + start - prox_vector[pos] = seed_score_df.loc[seed_label, layer.key] + start = sum(numpy.array( + multiplexall_node_count_list[:multiplex_idx]) + * numpy.array( + multiplexall_layer_count_list[:multiplex_idx])) + + (multiplexall_node_count_list[multiplex_idx] * layer_idx) + pos = multiplexall_node_list2d[multiplex_idx].index( + seed_label) + start + prox_vector[pos] = seed_score_df.loc[ + seed_label, + layer.key] return prox_vector, seed_score_df @@ -313,7 +358,8 @@ def __init__( seeds, self_loops, restart_proba, - pr=None): + pr=None + ): """ Constructs an object for the random walk with restart. @@ -434,6 +480,7 @@ def random_walk_rank(self) -> pandas.DataFrame: Returns : rwr_ranking_df (pandas.DataFrame) : A pandas Dataframe with columns: multiplex, node, layer, score """ + bipartite_matrix = self.bipartiteall_obj.bipartite_matrix transition_matrix_obj = TransitionMatrix(multiplex_all=self.multiplexall_obj, bipartite_matrix=bipartite_matrix, lamb=self.lamb) transition_matrixcoo = transition_matrix_obj.transition_matrixcoo @@ -449,6 +496,94 @@ def random_walk_rank(self) -> pandas.DataFrame: return rwr_ranking_df + # 2.1.1 : + def per_seed_random_walk_rank(self, n_jobs=1) -> pandas.DataFrame: + """ + Function that carries ous the full random walk with restart from a list of seeds. + + Returns : + rwr_ranking_df (pandas.DataFrame) : A pandas Dataframe with columns: multiplex, node, layer, score + """ + + def __par_seed_random_walk_restart(prox_vector, r): + """ + + Function that realize the RWR and give back the steady probability distribution for + each multiplex in a dataframe. + + self.results (list) : A list of ndarray. Each ndarray correspond to the probability distribution of the + nodes of the multiplex. + + """ + rwr_result_lst = list() + threshold = 1e-10 + residue = 1 + itera = 1 + prox_vector_norm = prox_vector / (sum(prox_vector)) + restart_vector = prox_vector_norm + while residue >= threshold: + old_prox_vector = prox_vector_norm + prox_vector_norm = (1 - r) * (transition_matrixcoo.dot(prox_vector_norm)) + r * restart_vector + residue = numpy.sqrt(sum((prox_vector_norm - old_prox_vector) ** 2)) + itera += 1 + + return prox_vector_norm + + bipartite_matrix = self.bipartiteall_obj.bipartite_matrix + transition_matrix_obj = TransitionMatrix(multiplex_all=self.multiplexall_obj, bipartite_matrix=bipartite_matrix, lamb=self.lamb) + transition_matrixcoo = transition_matrix_obj.transition_matrixcoo + + + # stored in list for parallelisation + all_seeds_rwr_ranking_df = [] + prox_vectors = [] + seed_scores = [] + + rwr_result_lst = list() + threshold = 1e-10 + residue = 1 + itera = 1 + r = self.r + + for seed in self.seed_obj._seed_list: + # Get initial seed probability distribution + if type(self.pr) == type(None): + individual_seed = Seed(path='', multiplexall=self.multiplexall_obj, seeds=[seed]) + prox_vector, seed_score = individual_seed.get_seed_scores(transition=transition_matrixcoo) + prox_vectors.append(prox_vector) + seed_scores.append(seed_score) + print(len(prox_vectors)) + + # Run RWR algorithm parallelised + with LocalCluster( + n_workers=n_jobs, + processes=True, + threads_per_worker=1, + ) as cluster, Client(cluster) as client: + + # Monitor your computation with the Dask dashboard + print(client.dashboard_link) + with joblib.parallel_config(backend="dask"): + all_seeds_rwr_ranking_lst = Parallel()( + delayed(__par_seed_random_walk_restart)(prox_vectors[i], r) + for i in range(len(seed_scores))) + + # divide per multiplex: + start_end_nodes = [] + for k in range(len(self.multiplex_layer_count_list)): + start = sum(numpy.array(self.multiplexall_node_count_list[:k]) * numpy.array(self.multiplex_layer_count_list[:k])) + end = start + self.multiplexall_node_count_list[k] * self.multiplex_layer_count_list[k] + start_end_nodes.append((start, end)) + + for i in range(len(prox_vectors)): + rwr_ranking_df = self.__random_walk_rank_lst_to_df([numpy.array(all_seeds_rwr_ranking_lst[i][s:e]) for s,e in start_end_nodes]) + all_seeds_rwr_ranking_df.append(rwr_ranking_df) + all_seeds_rwr_ranking_df[-1]['seed'] = self.seed_obj._seed_list[i] + + all_seeds_rwr_ranking_df = pandas.concat(all_seeds_rwr_ranking_df) + + return all_seeds_rwr_ranking_df + def write_ranking(self, random_walk_rank: pandas.DataFrame, path: str, top: int = None, aggregation: str = "gmean", degree: bool = False): """Writes the 'random walk results' to a subnetwork with the 'top' nodes as a SIF format (See Cytoscape documentation) @@ -460,9 +595,8 @@ def write_ranking(self, random_walk_rank: pandas.DataFrame, path: str, top: int """ if not (aggregation in ['nomean', 'gmean', 'hmean', 'mean', 'sum']): - logger.error('Aggregation parameter must take one of these values: "nomean", "gmean", "hmean", "mean", or "sum". ' + raise ValueError('Aggregation parameter must take one of these values: "nomean", "gmean", "hmean", "mean", or "sum". ' 'Current value: {}'.format(aggregation)) - sys.exit(1) output_obj = Output(random_walk_rank, self.multiplexall_obj, top=top, top_type="layered", aggregation=aggregation) output_obj.to_tsv(outdir=path, degree=degree) @@ -479,14 +613,12 @@ def to_sif(self, random_walk_rank: pandas.DataFrame, path: str, top: int = None, """ if not (aggregation in ['nomean', 'gmean', 'hmean', 'mean', 'sum']): - logger.error('Aggregation parameter must take one of these values: "nomean", "gmean", "hmean", "mean", or "sum". ' + raise ValueError('Aggregation parameter must take one of these values: "nomean", "gmean", "hmean", "mean", or "sum". ' 'Current value: {}'.format(aggregation)) - sys.exit(1) if not (top_type in ['layered', 'all']): - logger.error('top_type parameter must take one of these values: "layered" or "all". ' + raise ValueError('top_type parameter must take one of these values: "layered" or "all". ' 'Current value: {}'.format(top_type)) - sys.exit(1) output_obj = Output(random_walk_rank, self.multiplexall_obj, top=top, top_type=top_type, aggregation=aggregation) pathlib.Path(os.path.dirname(path)).mkdir(exist_ok=True, parents=True) @@ -515,7 +647,6 @@ class CreateMultilayer: def __init__(self, multiplex, bipartite, eta, lamb, seeds, self_loops, restart_proba): """Takes a confMultiplexLayerig_path of the config yaml file""" - # default is 00 for all interaction types @@ -526,7 +657,7 @@ def __init__(self, multiplex, bipartite, eta, lamb, seeds, self_loops, restart_p self.self_loops = self_loops self.seeds = seeds self.restart_proba = restart_proba - + print(self.multiplex) def parse(self): @@ -624,22 +755,32 @@ def __parse_bipartite(self): if 'source' in self.bipartite[layer_key]: bipartite_source = self.bipartite[layer_key]['source'] else: - logger_setup.logger.error("No 'source' field found for bipartite network") - sys.exit(1) + raise KeyrError("No 'source' field found for bipartite network") if 'target' in self.bipartite[layer_key]: bipartite_target = self.bipartite[layer_key]['target'] else: - logger_setup.logger.error("No 'target' field found for bipartite network") - sys.exit(1) - - layer_obj = Bipartite( - key=layer_key, - abspath='', - graph_type=graph_type, - self_loops=self.self_loops, - on_disk=False, - edge_list_df = self.bipartite[layer_key]['edge_list_df']) + raise KeyError("No 'target' field found for bipartite network") + + print(self.bipartite[layer_key]) + + if type(self.bipartite[layer_key]['edge_list_df']) == str: + print("Opening network from {}.".format( + self.bipartite[layer_key]['edge_list_df'] + )) + layer_obj = Bipartite(key=layer_key, + abspath=self.bipartite[layer_key]['edge_list_df'], + graph_type=graph_type, + self_loops=self.self_loops, + on_disk=True, + edge_list_df = None) + else: + layer_obj = Bipartite(key=layer_key, + abspath='', + graph_type=graph_type, + self_loops=self.self_loops, + on_disk=False, + edge_list_df = self.bipartite[layer_key]['edge_list_df']) source_target_bipartite_dic[(bipartite_source, bipartite_target)] = layer_obj return BipartiteAll(source_target_bipartite_dic, multiplexall=self.multiplexall_obj) @@ -649,7 +790,6 @@ def __parse_multiplex(self): Reads multiplex field and create MultiplexAll object """ - multiplex_count = len(self.multiplex) # convert int to strings multiplex_obj_list = [] @@ -693,14 +833,27 @@ def __parse_multiplex(self): # loop over layers for layer_idx, layer_key in enumerate(layer_key_tuple): print(layer_key) - layer_obj = MultiplexLayer(key=layer_key, - abspath='', - graph_type=graph_type_lst[layer_idx], - multiplex=multiplex_key, - tau=tau_lst[layer_idx], - self_loops=self.self_loops, - on_disk=False, - edge_list_df = self.multiplex[multiplex_key]['layers'][layer_idx]) + if type(self.multiplex[multiplex_key]['layers'][layer_idx]) == str: + print("Opening network from {}.".format( + self.multiplex[multiplex_key]['layers'][layer_idx] + )) + layer_obj = MultiplexLayer(key=layer_key, + abspath=self.multiplex[multiplex_key]['layers'][layer_idx], + graph_type=graph_type_lst[layer_idx], + multiplex=multiplex_key, + tau=tau_lst[layer_idx], + self_loops=self.self_loops, + on_disk=True, + edge_list_df = None) + else: + layer_obj = MultiplexLayer(key=layer_key, + abspath='', + graph_type=graph_type_lst[layer_idx], + multiplex=multiplex_key, + tau=tau_lst[layer_idx], + self_loops=self.self_loops, + on_disk=False, + edge_list_df = self.multiplex[multiplex_key]['layers'][layer_idx]) multiplex_node_list = sorted([*set(multiplex_node_list + [*layer_obj.networkx.nodes])]) layer_obj_list.append(layer_obj) @@ -758,100 +911,4 @@ def __parse_seed(self): seed_obj = Seed(path='', multiplexall=self.multiplexall_obj, seeds = self.seeds) - return seed_obj - - -def format_multilayer( - TF_layer: Union[list, pandas.DataFrame], - ATAC_layer: Union[list, pandas.DataFrame], - RNA_layer: Union[list, pandas.DataFrame], - TF_ATAC_bipartite: pandas.DataFrame, - ATAC_RNA_bipartite: pandas.DataFrame, - TF_layer_graph_type: Union[str, list] = '00', - ATAC_layer_graph_type: Union[str, list] = '01', - RNA_layer_graph_type: Union[str, list] = '01', - TF_ATAC_bipartite_graph_type: str = '00', - ATAC_RNA_bipartite_graph_type: str = '00'): - - """ Format layers and bipartites data as needed for HuMMuS/MultiXrank - - ! The DataFrame should have 2-3 columns : - ['source', 'target', 'weight'], or ['source', 'target'] ! - #TODO : explain graph_type !! - Args: - TF_layer (Union[list, pandas.DataFrame]): TF layer(s) edge list - ATAC_layer (Union[list, pandas.DataFrame]): ATAC layer(s) edge list - RNA_layer (Union[list, pandas.DataFrame]): RNA layer(s) edge list - TF_ATAC_bipartite (pandas.DataFrame): TF-ATAC bipartite edge list - ATAC_RNA_bipartite (pandas.DataFrame): ATAC-RNA bipartite edge list - TF_layer_graph_type (Union[str, list], optional): Graph type for TF layer(s). - Defaults to '00'. - ATAC_layer_graph_type (Union[str, list], optional): Graph type for ATAC layer(s). - Defaults to '01'. - RNA_layer_graph_type (Union[str, list], optional): Graph type for RNA layer(s). - Defaults to '01'. - TF_ATAC_bipartite_graph_type (str, optional): Graph type for TF-ATAC bipartite. - Defaults to '00'. - ATAC_RNA_bipartite_graph_type (str, optional): Graph type for ATAC-RNA bipartite. - Defaults to '00'. - - Returns: - dict: Formatted data as input for MultiXrank-HuMMuS class - """ - if type(TF_layer) == pandas.DataFrame: - TF_layer = [TF_layer] - if type(ATAC_layer) == pandas.DataFrame: - ATAC_layer = [ATAC_layer] - if type(RNA_layer) == pandas.DataFrame: - RNA_layer = [RNA_layer] - - if type(TF_layer_graph_type) == str: - TF_layer_graph_type = [TF_layer_graph_type]*len(TF_layer) - if type(ATAC_layer_graph_type) == str: - ATAC_layer_graph_type = [ATAC_layer_graph_type]*len(ATAC_layer) - if type(RNA_layer_graph_type) == str: - RNA_layer_graph_type = [RNA_layer_graph_type]*len(RNA_layer) - - assert len(TF_layer) == len(TF_layer_graph_type), "TF_layer and TF_layer_graph_type must have the same length" - assert len(ATAC_layer) == len(ATAC_layer_graph_type), "ATAC_layer and ATAC_layer_graph_type must have the same length" - assert len(RNA_layer) == len(RNA_layer_graph_type), "RNA_layer and RNA_layer_graph_type must have the same length" - - keys_data = ['names', 'graph_type', 'layers'] - # Multiplex - multiplex = { - 'TF': {key:[] for key in keys_data}, - 'ATAC': {key:[] for key in keys_data}, - 'RNA': {key:[] for key in keys_data} - } - - for i, layer in enumerate(TF_layer): - multiplex['TF']['names'].append(f'TF_{i}') - multiplex['TF']['graph_type'].append(TF_layer_graph_type[i]) - multiplex['TF']['layers'].append(layer) - - for i, layer in enumerate(ATAC_layer): - multiplex['ATAC']['names'].append(f'ATAC_{i}') - multiplex['ATAC']['graph_type'].append(ATAC_layer_graph_type[i]) - multiplex['ATAC']['layers'].append(layer) - - for i, layer in enumerate(RNA_layer): - multiplex['RNA']['names'].append(f'RNA_{i}') - multiplex['RNA']['graph_type'].append(RNA_layer_graph_type[i]) - multiplex['RNA']['layers'].append(layer) - - # Bipartite - bipartite = { - 'TF_ATAC': { - 'source': 'TF', - 'target': 'ATAC', - 'edge_list_df': TF_ATAC_bipartite, - 'graph_type': TF_ATAC_bipartite_graph_type}, - - 'ATAC_RNA': { - 'source': 'ATAC', - 'target': 'RNA', - 'edge_list_df': ATAC_RNA_bipartite, - 'graph_type': ATAC_RNA_bipartite_graph_type} - } - - return multiplex, bipartite \ No newline at end of file + return seed_obj \ No newline at end of file diff --git a/hummuspy/src/hummuspy/explore_network.py b/hummuspy/src/hummuspy/explore_network.py index 1bc7009..06d2521 100644 --- a/hummuspy/src/hummuspy/explore_network.py +++ b/hummuspy/src/hummuspy/explore_network.py @@ -1,26 +1,23 @@ -# import typing -# import os -# import time -import yaml -import numpy as np -import pandas as pd -import multixrank as mxr -from joblib import Parallel, delayed -from tqdm import tqdm -# from typing import Union -import hummuspy.config +from hummuspy.create_multilayer import Multixrank +import os +import numpy +import pandas -def compute_multiple_RandomWalk( - multilayer_f, - config_name, - list_seeds, - config_folder='config', +def compute_RandomWalk( + multiplex, + bipartite, + eta, + lamb, + seeds, + self_loops=True, + restart_proba=0.7, + pr=None, save=True, output_f=None, - return_df=False, + return_df=True, spec_layer_result_saved='all', - njobs=1): + n_jobs=1): """Compute random walks for a list of seeds. Parameters @@ -29,28 +26,22 @@ def compute_multiple_RandomWalk( Path to the multilayer folder. config_name : str Name of the config file. - output_f : str - Name of the output file. - list_seeds : list + seeds : list List of seeds. config_folder : str, optional Name of the config folder. The default is 'config'. - save : bool, optional - Save the result. The default is True. - return_df : bool, optional - Return the result. The default is False. spec_layer_result_saved : str, optional Name of the layer to save. The default is 'all'. - Specify here if you want to keep only the scores of going to one of the layer - (e.g.: If you're interested only in TF-genes - interactions and not in peaks-related scores, put ['RNA'] + unnamed : bool, optional + If True, the seeds file will be named 'seeds.txt'. + The default is False. njobs : int, optional Number of jobs. The default is 1. Returns ------- - ranking_all_dfs : pd.DataFrame - Dataframe containing the result of the random walks. + ranking_df : pd.DataFrame + Dataframe containing the result of the random walk. Structure: layer : str Name of the target layer. @@ -69,64 +60,69 @@ def compute_multiple_RandomWalk( >>> multilayer_f = 'path/to/multilayer/folder' >>> config_folder = 'config' >>> config_name = 'hummuspy.config.yml' - >>> list_seeds = ['seed1', 'seed2'] - >>> df = compute_multiple_RandomWalk(multilayer_f, - config_name, - list_seeds, - config_folder=config_folder, - save=False, - return_df=True, - spec_layer_result_saved='all', # or 'TF' - njobs=5) + >>> seed = 'seed1' + >>> df = compute_RandomWalk(multilayer_f, + config_name, + seed, + # seeds_filename = 'auto'/'your_name.txt' + config_folder=config_folder, + spec_layer_result_saved='all', # or 'TF' + njobs=5) """ - ranking_all_dfs = pd.DataFrame(columns=['layer', - 'target', - 'path_layer', - 'score', - 'seed']) + # multixrank + multixrank_obj = Multixrank( + multiplex=multiplex, + bipartite=bipartite, + eta=eta, + lamb=lamb, + seeds=seeds, + self_loops=self_loops, + restart_proba=restart_proba, + pr=pr) + ranking_df = multixrank_obj.random_walk_rank().sort_values( + by='score', + ascending=False) - l_ranking_df = Parallel(n_jobs=njobs)(delayed(compute_RandomWalk) - (multilayer_f=multilayer_f, - config_name=config_name, - seeds=seeds, - config_folder=config_folder, - save=False, - spec_layer_result_saved=spec_layer_result_saved) - for seeds in tqdm(list_seeds, - position=0, - leave=True)) - ranking_all_dfs = pd.concat([ranking_all_dfs]+l_ranking_df) - ranking_all_dfs = ranking_all_dfs.sort_values(by='score', ascending=False) - ranking_all_dfs = ranking_all_dfs.reset_index(drop=True) + # and filter df results and add seeds name + ranking_df['seed'] = '_'.join(seeds) + ranking_df = ranking_df[ranking_df.score > 0] # ?? + ranking_df.columns = ['layer', 'target', 'path_layer', 'score', 'seed'] + if spec_layer_result_saved != 'all': + if type(spec_layer_result_saved) is str: + spec_layer_result_saved = [spec_layer_result_saved] + ranking_df = ranking_df[ranking_df['layer'].isin( + spec_layer_result_saved)] if save: assert output_f is not None, 'You need to provide an output_f name' +\ ' to save the random walks result' - ranking_all_dfs.to_csv(output_f, sep='\t', index=False, header=True) + ranking_df.to_csv(output_f, sep='\t', index=False, header=True) if return_df: - return ranking_all_dfs + return ranking_df -def compute_RandomWalk( - multilayer_f, - config_name, +def compute_multiple_RandomWalk( + multiplex, + bipartite, + eta, + lamb, seeds, - seeds_filename='auto', - seeds_folder='seed', - config_folder='config', + self_loops=True, + restart_proba=0.7, + pr=None, save=True, output_f=None, return_df=True, spec_layer_result_saved='all', - njobs=1): + n_jobs=1): """Compute random walks for a list of seeds. Parameters ---------- multilayer_f : str Path to the multilayer folder. - config_name : str + config_name : strLINC01409 Name of the config file. seeds : list List of seeds. @@ -171,842 +167,35 @@ def compute_RandomWalk( spec_layer_result_saved='all', # or 'TF' njobs=5) """ - seeds_filename - # seeds file names - seeds = hummuspy.config.make_values_list(seeds) - if seeds_filename != 'auto': - if njobs > 1: - raise Exception("Impossible to use only one seeds filename while" + - " parallelising random walks." + - "\nTry seeds_filename = 'auto', or njobs=1.") - else: - seeds_filename = '_'.join(seeds) - - # write seeds file - with open(multilayer_f + "/" + seeds_folder + "/" + - seeds_filename + '.txt', 'w') as f: - f.write('\n'.join(seeds)+'\n') - - # config file personalised with seed file - with open(multilayer_f+'/{}/'.format(config_folder)+config_name, 'r') as f: - config = yaml.load(f, Loader=yaml.BaseLoader) - config['seed'] = seeds_folder+'/'+seeds_filename+'.txt' - with open(multilayer_f+'/{}/'.format(config_folder) - + seeds_filename + '_' + config_name, 'w') as f: - yaml.dump(config, f, sort_keys=False) # multixrank - multixrank_obj = mxr.Multixrank( - config=multilayer_f + '/' + config_folder + '/' - + seeds_filename + '_' + config_name, - wdir=multilayer_f) - ranking_df = multixrank_obj.random_walk_rank() + multixrank_obj = Multixrank( + multiplex=multiplex, + bipartite=bipartite, + eta=eta, + lamb=lamb, + seeds=seeds, + self_loops=self_loops, + restart_proba=restart_proba, + pr=pr) + + ranking_df = multixrank_obj.per_seed_random_walk_rank( + n_jobs=n_jobs).sort_values(by='score', ascending=False) # and filter df results and add seeds name - ranking_df['seed'] = seeds_filename ranking_df = ranking_df[ranking_df.score > 0] # ?? ranking_df.columns = ['layer', 'target', 'path_layer', 'score', 'seed'] if spec_layer_result_saved != 'all': - if type(spec_layer_result_saved) == str: + if type(spec_layer_result_saved) is str: spec_layer_result_saved = [spec_layer_result_saved] ranking_df = ranking_df[ranking_df['layer'].isin( spec_layer_result_saved)] + ranking_df = ranking_df.reset_index() + if save: assert output_f is not None, 'You need to provide an output_f name' +\ ' to save the random walks result' ranking_df.to_csv(output_f, sep='\t', index=False, header=True) if return_df: return ranking_df - - -############################################# -# 1/4 Define GRN config and compute results # -############################################# -def define_grn_from_config( - multilayer_f, - config, - gene_list=None, - tf_list=None, - config_name='grn_config.yml', - config_folder='config', - tf_multiplex: str = 'TF', - peak_multiplex: str = 'peaks', - rna_multiplex: str = 'RNA', - update_config=True, - save=False, - return_df=True, - output_f=None, - njobs=1): - """Define a GRN from a multilayer network and a config file. - Random walks are computed for each gene in the gene list and we keep - the probability to reach each TF in the TF list. - You can provide a list of genes and TFs to restrict the GRN. - The gene_list is used as individual seed for computing the random walks. - The list of TFs is used after the random walks, filtering the results to - only the TFs of interest. - You can choose to save the result in a file and/or return it. - - Parameters - ---------- - multilayer_f : str - Path to the multilayer folder. - config : dict - Config dictionnary. - gene_list : list, optional - List of genes. The default is 'all'. - tf_list : list, optional - List of TFs. The default is 'all'. - config_name : str, optional - Name of the config file that will be saved. - The default is 'grn_config.yml'. - config_folder : str, optional - Name of the config folder where the config will be save. - ! For each seed (sometimes thousands), a file should be created in this - folder. The default is 'config'. - tf_multiplex : str, optional - Name of the TF multiplex. The default is 'TF'. - peak_multiplex : str, optional - Name of the peak multiplex. The default is 'peaks'. - rna_multiplex : str, optional - Name of the RNA multiplex. The default is 'RNA'. - update_config : bool, optional - Update the config file. The default is True ; if False, the config - file won't be updated for the values of eta and lamb. - save : bool, optional - Save the result. The default is False. If True, you need to provide - an output_f name to save the GRN result. - return_df : bool, optional - Return the result. The default is True. - output_f : str, optional - Name of the output file. The default is None. Only used if save=True. - njobs : int, optional - Number of jobs. The default is 1. If >1, the seeds will be saved in - different files (in the multilayer subfolder 'seed') and the random - walks will be parallelised. - Returns - ------- - df : pd.DataFrame - Dataframe containing the random walks's results that defines the GRN. - Columns: - layer : str - Name of the target layer. - path_layer : str - Name of the layer of the path. - score : float - Score of the random walk. - gene : str - Name of the gene-seed. - tf : str - Name of the TF-target. - - """ - # store mutliplex already because it will be when saving yaml file, - # while eta and lambda won't. - config['multiplex'] = {k: config['multiplex'][k] - for k in sorted(config['multiplex'].keys())} - - if update_config: - eta = hummuspy.config.get_single_layer_eta(config, - rna_multiplex) - - lamb = hummuspy.config.get_grn_lamb(config, - tf_multiplex, - peak_multiplex, - rna_multiplex, - draw=False) - config['eta'] = eta - config['lamb'] = lamb - # config = hummuspy.config.setup_proba_config(config, eta, lamb) - - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) - - if gene_list is None: - gene_list = [] - for layer in config['multiplex'][rna_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - gene_list = np.unique(np.concatenate([gene_list, - layer_nodes])) - - df = compute_multiple_RandomWalk(multilayer_f, - config_name=config_name, - output_f=output_f, - list_seeds=gene_list, - config_folder=config_folder, - save=False, - return_df=return_df, - spec_layer_result_saved=tf_multiplex, - njobs=njobs) - - df['gene'] = df['seed'] - df['tf'] = df['target'] - del df['target'] - del df['seed'] - - if tf_list is None: - tf_list = [] - for layer in config['multiplex'][tf_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - tf_list = np.unique(np.concatenate([tf_list, - layer_nodes])) - tf_list = tf_list[tf_list != 'fake_node'] - - # Add normalisation ? - df = df[df['tf'].isin(tf_list)] - - if save is True: - assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the GRN result' - df.sort_values(by='score', ascending=False).to_csv(output_f, - sep='\t', - index=False, - header=True) - if return_df: - return df - - -############################################################################### -############################################# -# 2/4 Define enhancers config and compute results # -############################################# -def define_enhancers_from_config( - multilayer_f, - config, - gene_list=None, - peak_list=None, - config_name='enhancers_config.yml', - config_folder='config', - tf_multiplex: str = 'TF', - peak_multiplex: str = 'peaks', - rna_multiplex: str = 'RNA', - update_config=True, - save=False, - return_df=True, - output_f=None, - njobs=1): - """Return enhancers prediction from a multilayer network and a config file. - Random walks are computed for each gene in the gene list and we keep - the probability to reach each peak in the peak list. - You can provide a peak_list and a gene_list to restrict the predictions. - The gene_list is used as individual seed for computing the random walks. - The list of peaks is used after the random walks, filtering the results to - only the peaks of interest. - You can choose to save the result in a file and/or return it. - - Parameters - ---------- - multilayer_f : str - Path to the multilayer folder. - config : dict - Config dictionnary. - gene_list : list, optional - List of genes. The default is 'all'. - peak_list : list, optional - List of peaks. The default is 'all'. - config_name : str, optional - Name of the config file that will be saved. - The default is 'enhancers_config.yml'. - config_folder : str, optional - Name of the config folder where the config will be save. - ! For each seed (sometimes thousands), a file should be created in this - folder. The default is 'config'. - tf_multiplex : str, optional - Name of the TF multiplex. The default is 'TF'. - peak_multiplex : str, optional - Name of the peak multiplex. The default is 'peaks'. - rna_multiplex : str, optional - Name of the RNA multiplex. The default is 'RNA'. - update_config : bool, optional - Update the config file. The default is True ; if False, the config - file won't be updated for the values of eta and lamb. - save : bool, optional - Save the result. The default is False. If True, you need to provide - an output_f name to save the predictions. - return_df : bool, optional - Return the result. The default is True. - output_f : str, optional - Name of the output file. The default is None. Only used if save=True. - njobs : int, optional - Number of jobs. The default is 1. If >1, the seeds will be saved in - different files (in the multilayer subfolder 'seed') and the random - walks will be parallelised. - Returns - ------- - df : pd.DataFrame - Dataframe of the random walks's results that defines the predictions. - Columns: - layer : str - Name of the target layer. - path_layer : str - Name of the layer of the path. - score : float - Score of the random walk. - gene : str - Name of the gene-seed. - peak : str - Name of the peak-target. - """ - # store mutliplex already because it will be when saving yaml file, - # while eta and lambda won't. - config['multiplex'] = {k: config['multiplex'][k] - for k in sorted(config['multiplex'].keys())} - - if update_config: - # Indicate layer where to start the random walks : rna_multiplex - eta = hummuspy.config.get_single_layer_eta(config, - rna_multiplex) - - # Define proba matrix to jump between layer : rna <--> peaks - lamb = hummuspy.config.get_enhancers_lamb(config, - tf_multiplex, - peak_multiplex, - rna_multiplex, - draw=False) - - config['eta'] = eta - config['lamb'] = lamb - # config = hummuspy.config.setup_proba_config(config, eta, lamb) - - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) - - if gene_list is None: - gene_list = [] - for layer in config['multiplex'][rna_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - gene_list = np.unique(np.concatenate([gene_list, - layer_nodes])) - - df = compute_multiple_RandomWalk(multilayer_f, - config_name=config_name, - output_f=output_f, - list_seeds=gene_list, - config_folder=config_folder, - save=False, - return_df=return_df, - spec_layer_result_saved=peak_multiplex, - # save only peaks proba - njobs=njobs) - - df['gene'] = df['seed'] - df['peak'] = df['target'] - del df['target'] - del df['seed'] - - if peak_list is None: - peak_list = [] - for layer in config['multiplex'][peak_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - peak_list = np.unique(np.concatenate([peak_list, - layer_nodes])) - - # Add normalisation ? - df = df[df['peak'].isin(peak_list)] - - if save is True: - assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the enhancers prediction result.' - df.sort_values(by='score', ascending=False).to_csv(output_f, - sep='\t', - index=False, - header=True) - if return_df: - return df - - -######################################################### -# 3/4 Define binding regions config and compute results # -######################################################### -def define_binding_regions_from_config( - multilayer_f, - config, - tf_list=None, - peak_list=None, - config_name='binding_regions_config.yml', - config_folder='config', - tf_multiplex: str = 'TF', - peak_multiplex: str = 'peaks', - rna_multiplex: str = 'RNA', - update_config=True, - save=False, - return_df=True, - output_f=None, - njobs=1): - """Return binding regions prediction from a multilayer network and a config - file. Random walks are computed for each TF in the TF list and we keep the - probability to reach each peak in the peak list. - You can provide a list of peaks and a tf_list to restrict the predictions. - The list of TFs is used as individual seed for computing the random walks. - The list of peaks is used after the random walks, filtering the results to - only the peaks of interest. - You can choose to save the result in a file and/or return it. - - - Parameters - ---------- - multilayer_f : str - Path to the multilayer folder. - config : dict - Config dictionnary. - tf_list : list, optional - List of TFs. The default is 'all'. - peak_list : list, optional - List of peaks. The default is 'all'. - config_name : str, optional - Name of the config file that will be saved. - The default is 'binding_regions_config.yml'. - config_folder : str, optional - Name of the config folder where the config will be save. - ! For each seed (sometimes thousands), a file should be created in this - folder. The default is 'config'. - tf_multiplex : str, optional - Name of the TF multiplex. The default is 'TF'. - peak_multiplex : str, optional - Name of the peak multiplex. The default is 'peaks'. - rna_multiplex : str, optional - Name of the RNA multiplex. The default is 'RNA'. - update_config : bool, optional - Update the config file. The default is True ; if False, the config - file won't be updated for the values of eta and lamb. - save : bool, optional - Save the result. The default is False. If True, you need to provide - an output_f name to save the predictions. - return_df : bool, optional - Return the result. The default is True. - output_f : str, optional - Name of the output file. The default is None. Only used if save=True. - njobs : int, optional - Number of jobs. The default is 1. If >1, the seeds will be saved in - different files (in the multilayer subfolder 'seed') and the random - walks will be parallelised. - - Returns - ------- - df : pd.DataFrame - Dataframe of the random walks's results that defines the predictions. - Columns: - layer : str - Name of the target layer. - path_layer : str - Name of the layer of the path. - score : float - Score of the random walk. - tf : str - Name of the TF-seed. - peak : str - Name of the peak-target. - """ - # store mutliplex already because it will be when saving yaml file, - # while eta and lambda won't. - config['multiplex'] = {k: config['multiplex'][k] - for k in sorted(config['multiplex'].keys())} - - if update_config: - # Indicate layer where to start the random walks : rna_multiplex - eta = hummuspy.config.get_single_layer_eta(config, - tf_multiplex) - - # Define proba matrix to jump between layer : rna <--> peaks - lamb = hummuspy.config.get_binding_regions_lamb(config, - tf_multiplex, - peak_multiplex, - rna_multiplex, - draw=False) - config['eta'] = eta - config['lamb'] = lamb - # config = hummuspy.config.setup_proba_config(config, eta, lamb) - - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) - - if tf_list is None: - tf_list = [] - for layer in config['multiplex'][tf_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - tf_list = np.unique(np.concatenate([tf_list, - layer_nodes])) - tf_list = tf_list[tf_list != 'fake_node'] - - df = compute_multiple_RandomWalk(multilayer_f, - config_name=config_name, - output_f=output_f, - list_seeds=tf_list, - config_folder=config_folder, - save=False, - return_df=return_df, - spec_layer_result_saved=peak_multiplex, - # save only peaks proba - njobs=njobs) - - df['tf'] = df['seed'] - df['peak'] = df['target'] - del df['target'] - del df['seed'] - - if peak_list is None: - peak_list = [] - for layer in config['multiplex'][peak_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - peak_list = np.unique(np.concatenate([peak_list, - layer_nodes])) - - # Add normalisation ? - df = df[df['peak'].isin(peak_list)] - - if save is True: - assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the enhancers prediction result.' - df.sort_values(by='score', ascending=False).to_csv(output_f, - sep='\t', - index=False, - header=True) - if return_df: - return df - - -###################################################### -# 4/4 Define target genes config and compute results # -###################################################### -def define_target_genes_from_config( - multilayer_f, - config, - gene_list=None, - tf_list=None, - config_name='target_genes_config.yml', - config_folder='config', - tf_multiplex: str = 'TF', - peak_multiplex: str = 'peaks', - rna_multiplex: str = 'RNA', - update_config=True, - save=False, - return_df=True, - output_f=None, - njobs=1): - """Return target genes prediction from a multilayer network and a config - file. Random walks are computed for each TF in the TF list and we keep the - probability to reach each gene in the gene list. - You can provide a list of genes and a tf_list to restrict the predictions. - The list of TFs is used as individual seed for computing the random walks. - The list of genes is used after the random walks, filtering the results to - only the genes of interest. - You can choose to save the result in a file and/or return it. - - Parameters - ---------- - multilayer_f : str - Path to the multilayer folder. - config : dict - Config dictionnary. - gene_list : list, optional - List of genes. The default is 'all'. - tf_list : list, optional - List of TFs. The default is 'all'. - config_name : str, optional - Name of the config file that will be saved. - The default is 'target_genes_config.yml'. - config_folder : str, optional - Name of the config folder where the config will be save. - ! For each seed (sometimes thousands), a file should be created in this - folder. The default is 'config'. - tf_multiplex : str, optional - Name of the TF multiplex. The default is 'TF'. - peak_multiplex : str, optional - Name of the peak multiplex. The default is 'peaks'. - rna_multiplex : str, optional - Name of the RNA multiplex. The default is 'RNA'. - update_config : bool, optional - Update the config file. The default is True ; if False, the config - file won't be updated for the values of eta and lamb. - save : bool, optional - Save the result. The default is False. If True, you need to provide - an output_f name to save the predictions. - return_df : bool, optional - Return the result. The default is True. - output_f : str, optional - Name of the output file. The default is None. Only used if save=True. - njobs : int, optional - Number of jobs. The default is 1. If >1, the seeds will be saved in - different files (in the multilayer subfolder 'seed') and the random - walks will be parallelised. - - Returns - ------- - df : pd.DataFrame - Dataframe of the random walks's results that defines the predictions. - Columns: - layer : str - Name of the target layer. - path_layer : str - Name of the layer of the path. - score : float - Score of the random walk. - tf : str - Name of the TF-seed. - gene : str - Name of the gene-target. - """ - # store mutliplex already because it will be when saving yaml file, - # while eta and lambda won't. - config['multiplex'] = {k: config['multiplex'][k] - for k in sorted(config['multiplex'].keys())} - - if update_config: - eta = hummuspy.config.get_single_layer_eta(config, - tf_multiplex) - - lamb = hummuspy.config.get_target_genes_lamb(config, - tf_multiplex, - peak_multiplex, - rna_multiplex, - draw=False) - - config['eta'] = eta - config['lamb'] = lamb - # config = hummuspy.config.setup_proba_config(config, eta, lamb) - - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) - - if gene_list is None: - gene_list = [] - for layer in config['multiplex'][rna_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - gene_list = np.unique(np.concatenate([gene_list, - layer_nodes])) - - if tf_list is None: - tf_list = [] - for layer in config['multiplex'][tf_multiplex]['layers']: - df_layer = pd.read_csv(multilayer_f+'/'+layer, - sep='\t', - header=None, - index_col=None) - - layer_nodes = np.concatenate([np.unique(df_layer[0].values), - np.unique(df_layer[1].values)]) - tf_list = np.unique(np.concatenate([tf_list, - layer_nodes])) - tf_list = tf_list[tf_list != 'fake_node'] - - df = compute_multiple_RandomWalk(multilayer_f, - config_name=config_name, - output_f=output_f, - list_seeds=tf_list, - config_folder=config_folder, - save=False, - return_df=return_df, - spec_layer_result_saved=rna_multiplex, - njobs=njobs) - - df['tf'] = df['seed'] - df['gene'] = df['target'] - del df['target'] - del df['seed'] - - # Add normalisation ? - df = df[df['gene'].isin(gene_list)] - - if save is True: - assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the GRN result' - df.sort_values(by='score', ascending=False).to_csv(output_f, - sep='\t', - index=False, - header=True) - if return_df: - return df - - -def get_output_from_dicts( - output_request: str, - multilayer_f, - multiplexes_list, - bipartites_list, - folder_multiplexes='multiplex', - folder_bipartites='bipartite', - gene_list=None, - tf_list=None, - peak_list=None, - config_filename='config.yml', - config_folder='config', - tf_multiplex: str = 'TF', - peak_multiplex: str = 'peaks', - rna_multiplex: str = 'RNA', - bipartites_type=('00', '00'), - update_config=True, - save=False, - return_df=True, - output_f=None, - njobs=1): - """ - Compute an output from a multilayer network and a config file, that can be - chosen among ['grn', 'enhancers', 'binding_regions', 'target_genes']. - - It is a wrapper of the functions define_*_from_config, that are called - depending on the output_request parameter. - - Parameters - ---------- - output_request : ['grn', 'enhancers', 'binding_regions', 'target_genes'] - Type of output requested. - multilayer_f : str - Path to the multilayer folder. - config : dict - Config dictionnary. - gene_list : list, optional - List of genes. The default is 'all'. - tf_list : list, optional - List of TFs. The default is 'all'. - config_name : str, optional - Name of the config file. The default is 'config.yml'. - config_folder : str, optional - Name of the config folder. The default is 'config'. - tf_multiplex : str, optional - Name of the TF multiplex. The default is 'TF'. - peak_multiplex : str, optional - Name of the peak multiplex. The default is 'peaks'. - rna_multiplex : str, optional - Name of the RNA multiplex. The default is 'RNA'. - update_config : bool, optional - Update the config file. The default is True. - save : bool, optional - Save the result. The default is False. - return_df : bool, optional - Return the result. The default is True. - output_f : str, optional - Name of the output file. The default is None. - njobs : int, optional - Number of jobs. The default is 1. - - Returns - -------ith open(self.config_path) as fin: - self.config_dic = yaml.load(fin, Loader=yaml.BaseLoader) - df : pd.DataFrame - Dataframe containing the random walks's results that defines the GRN. - Columns: - layer : str - Name of the target layer. - - path_layer : str - Name of the layer of the path. - score : float - Score of the random walk. - gene : str - Name of the gene-seed. - tf : str - Name of the TF-target. - - """ - - njobs = int(njobs) - print('multiplexes_list : ', multiplexes_list) - print('bipartites_list : ', bipartites_list) - print('folder_multiplexes : ', folder_multiplexes) - print('folder_bipartites : ', folder_bipartites) - print('gene_list : ', gene_list) - print('tf_list : ', tf_list) - print('peak_list : ', peak_list) - print('config_filename : ', config_filename) - print('config_folder : ', config_folder) - print('tf_multiplex : ', tf_multiplex) - print('peak_multiplex : ', peak_multiplex) - print('rna_multiplex : ', rna_multiplex) - print('update_config : ', update_config) - print('save : ', save) - print('return_df : ', return_df) - print('output_f : ', output_f) - print('njobs : ', njobs) - - # Create general config file - config = hummuspy.config.general_config( - multiplexes=multiplexes_list, - bipartites=bipartites_list, - folder_multiplexes=folder_multiplexes, - folder_bipartites=folder_bipartites, - suffix='.tsv', - self_loops=0, - restart_prob=0.7, - bipartites_type=bipartites_type, - save_configfile=False, - config_filename=config_filename) - - parameters = { - 'multilayer_f': multilayer_f, - 'config': config, - 'gene_list': gene_list, - 'peak_list': peak_list, - 'tf_list': tf_list, - 'config_name': config_filename, - 'config_folder': config_folder, - 'tf_multiplex': tf_multiplex, - 'peak_multiplex': peak_multiplex, - 'rna_multiplex': rna_multiplex, - 'update_config': update_config, - 'save': save, - 'return_df': return_df, - 'output_f': output_f, - 'njobs': njobs - } - - if output_request == 'grn': - del parameters['peak_list'] - df = define_grn_from_config(**parameters) - - elif output_request == 'enhancers': - del parameters['tf_list'] - df = define_enhancers_from_config(**parameters) - - elif output_request == 'binding_regions': - del parameters['gene_list'] - df = define_binding_regions_from_config(**parameters) - - elif output_request == 'target_genes': - del parameters['peak_list'] - df = define_target_genes_from_config(**parameters) - else: - raise ValueError("Please select an output_request value in ('grn', 'enhancers', 'binding_regions', 'target_genes').") - - return df From 65b75cdc8dd36b264f8c8ab4fe11f995807a0f2d Mon Sep 17 00:00:00 2001 From: r-trimbour Date: Sun, 14 Jul 2024 21:32:29 +0200 Subject: [PATCH 02/18] test hummuspy import --- vignettes/chen_vignette.Rmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index 61376a8..151c030 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -50,6 +50,8 @@ knitr::opts_chunk$set(eval = TRUE) library(reticulate) # install python dependency py_install("hummuspy", envname = "r-reticulate", method="auto") +reticulate::use_virtualenv("r-reticulate") +reticulate::import("hummuspy") library(HuMMuS) ``` ## Download the single-cell data From 3bf72e317d9692d04ff0163e32607bedcf00b678 Mon Sep 17 00:00:00 2001 From: r-trimbour Date: Mon, 15 Jul 2024 02:29:23 +0200 Subject: [PATCH 03/18] update dependencies and package version --- hummuspy/pyproject.toml | 4 +++- hummuspy/src/hummuspy/__init__.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/hummuspy/pyproject.toml b/hummuspy/pyproject.toml index 4986aab..cdd03b5 100644 --- a/hummuspy/pyproject.toml +++ b/hummuspy/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hummuspy" -version = "0.1.4" +version = "0.1.6" description = "HuMMuS is a novel method for the inference of regulatory mechanisms from multi-omics data with any type and number of omics, through a heterogeneous multilayer network framework." authors = ["RĂ©mi Trimbour "] license = "GPL-3.0-only" @@ -16,6 +16,8 @@ pandas = "^2.0.0" pyyaml = "^6.0" matplotlib = "^3.4.3" scipy = "^1.8.0" +distributed = "^2024.0.0" +rich = "^10.12.0" [build-system] requires = ["poetry-core"] diff --git a/hummuspy/src/hummuspy/__init__.py b/hummuspy/src/hummuspy/__init__.py index 92c2628..9580ff2 100644 --- a/hummuspy/src/hummuspy/__init__.py +++ b/hummuspy/src/hummuspy/__init__.py @@ -1,2 +1,3 @@ import hummuspy.config -import hummuspy.explore_network \ No newline at end of file +import hummuspy.explore_network +import hummuspy.core_grn From 74a65b1abe8d5b302becc1aca04166e67623eff5 Mon Sep 17 00:00:00 2001 From: r-trimbour Date: Mon, 15 Jul 2024 14:59:52 +0200 Subject: [PATCH 04/18] update hummuspy to work faster and the R wrappers around it --- hummuspy/dist/hummuspy-0.1.3-py3-none-any.whl | Bin 21420 -> 0 bytes hummuspy/dist/hummuspy-0.1.3.tar.gz | Bin 20269 -> 0 bytes hummuspy/dist/hummuspy-0.1.4-py3-none-any.whl | Bin 21421 -> 0 bytes hummuspy/dist/hummuspy-0.1.4.tar.gz | Bin 20271 -> 0 bytes hummuspy/pyproject.toml | 6 +- hummuspy/src/hummuspy/core_grn.py | 81 +++++++++--------- hummuspy/src/hummuspy/create_multilayer.py | 4 +- vignettes/chen_vignette.Rmd | 10 +-- 8 files changed, 53 insertions(+), 48 deletions(-) delete mode 100644 hummuspy/dist/hummuspy-0.1.3-py3-none-any.whl delete mode 100644 hummuspy/dist/hummuspy-0.1.3.tar.gz delete mode 100644 hummuspy/dist/hummuspy-0.1.4-py3-none-any.whl delete mode 100644 hummuspy/dist/hummuspy-0.1.4.tar.gz diff --git a/hummuspy/dist/hummuspy-0.1.3-py3-none-any.whl b/hummuspy/dist/hummuspy-0.1.3-py3-none-any.whl deleted file mode 100644 index bfc43a4c5712332cacaefc828f269acd0e2ad2d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21420 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 diff --git a/hummuspy/dist/hummuspy-0.1.3.tar.gz b/hummuspy/dist/hummuspy-0.1.3.tar.gz deleted file mode 100644 index 3f3c8d1fb45eb54a2049f6d54c66a7466b67ebbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20269 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 E0O4Haga7~l diff --git a/hummuspy/dist/hummuspy-0.1.4-py3-none-any.whl b/hummuspy/dist/hummuspy-0.1.4-py3-none-any.whl deleted file mode 100644 index 191c67d1f916cbaec7df10b5de7fe5fd4752c31f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21421 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 G2m$~o8|YyG diff --git a/hummuspy/pyproject.toml b/hummuspy/pyproject.toml index cdd03b5..1977f7f 100644 --- a/hummuspy/pyproject.toml +++ b/hummuspy/pyproject.toml @@ -9,15 +9,17 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.8" multixrank = "^0.1" -joblib = "^1.2.0" +joblib = "^1.3.0" tqdm = "^4.65.0" numpy = "^1.24.2" pandas = "^2.0.0" pyyaml = "^6.0" matplotlib = "^3.4.3" scipy = "^1.8.0" -distributed = "^2024.0.0" +distributed = "^2023.0.0" +dask = "^2023.0.0" rich = "^10.12.0" +bokeh = ">=2.4.2,!=3.0.*" [build-system] requires = ["poetry-core"] diff --git a/hummuspy/src/hummuspy/core_grn.py b/hummuspy/src/hummuspy/core_grn.py index bb33295..2ca9fc8 100644 --- a/hummuspy/src/hummuspy/core_grn.py +++ b/hummuspy/src/hummuspy/core_grn.py @@ -3,6 +3,7 @@ import pandas import hummuspy.config import hummuspy.explore_network +import hummuspy def format_multilayer( @@ -298,11 +299,15 @@ def define_grn_from_config( config['lamb'] = lamb # config = hummuspy.config.setup_proba_config(config, eta, lamb) + # process the config to the right format to + # compute the random walks without local saving + config = hummuspy.config.process_config(config, multilayer_f) + if gene_list is None: gene_list = [] for layer in config['multiplex'][rna_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -313,18 +318,15 @@ def define_grn_from_config( gene_list = numpy.unique(numpy.concatenate([gene_list, layer_nodes] )) - # process the config to the right format to - # compute the random walks without local saving - config = hummuspy.config.process_config(config, multilayer_f) + config['seeds'] = gene_list df = hummuspy.explore_network.compute_multiple_RandomWalk( **config, output_f=output_f, - seeds=gene_list, save=False, return_df=return_df, spec_layer_result_saved=tf_multiplex, - njobs=njobs) + n_jobs=njobs) df['gene'] = df['seed'] df['tf'] = df['target'] @@ -335,7 +337,7 @@ def define_grn_from_config( tf_list = [] for layer in config['multiplex'][tf_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -350,6 +352,7 @@ def define_grn_from_config( df = df[df['tf'].isin(tf_list)] if save is True: + print('Saving the result in ', output_f) assert output_f is not None, 'You need to provide an output_f name ' +\ 'to save the GRN result' df.sort_values(by='score', ascending=False).to_csv(output_f, @@ -462,14 +465,15 @@ def define_enhancers_from_config( config['lamb'] = lamb # config = hummuspy.config.setup_proba_config(config, eta, lamb) - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) + # process the config to the right format to + # compute the random walks without local saving + config = hummuspy.config.process_config(config, multilayer_f) if gene_list is None: gene_list = [] for layer in config['multiplex'][rna_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -479,18 +483,15 @@ def define_enhancers_from_config( numpy.unique(df_layer[1].values)]) gene_list = numpy.unique(numpy.concatenate([gene_list, layer_nodes] )) + config['seeds'] = gene_list df = hummuspy.explore_network.compute_multiple_RandomWalk( - multilayer_f, - config_name=config_name, + **config, output_f=output_f, - list_seeds=gene_list, - config_folder=config_folder, save=False, return_df=return_df, spec_layer_result_saved=peak_multiplex, - # save only peaks proba - njobs=njobs) + n_jobs=njobs) df['gene'] = df['seed'] df['peak'] = df['target'] @@ -501,7 +502,7 @@ def define_enhancers_from_config( peak_list = [] for layer in config['multiplex'][peak_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -516,6 +517,7 @@ def define_enhancers_from_config( df = df[df['peak'].isin(peak_list)] if save is True: + print('Saving the result in ', output_f) assert output_f is not None, 'You need to provide an output_f name ' +\ 'to save the enhancers prediction result.' df.sort_values(by='score', ascending=False).to_csv(output_f, @@ -617,7 +619,6 @@ def define_binding_regions_from_config( # Indicate layer where to start the random walks : rna_multiplex eta = hummuspy.config.get_single_layer_eta(config, tf_multiplex) - # Define proba matrix to jump between layer : rna <--> peaks lamb = hummuspy.config.get_binding_regions_lamb(config, tf_multiplex, @@ -628,14 +629,15 @@ def define_binding_regions_from_config( config['lamb'] = lamb # config = hummuspy.config.setup_proba_config(config, eta, lamb) - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) + # process the config to the right format to + # compute the random walks without local saving + config = hummuspy.config.process_config(config, multilayer_f) if tf_list is None: tf_list = [] for layer in config['multiplex'][tf_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -647,17 +649,15 @@ def define_binding_regions_from_config( )) tf_list = tf_list[tf_list != 'fake_node'] + config['seeds'] = tf_list + df = hummuspy.explore_network.compute_multiple_RandomWalk( - multilayer_f, - config_name=config_name, + **config, output_f=output_f, - list_seeds=tf_list, - config_folder=config_folder, save=False, return_df=return_df, spec_layer_result_saved=peak_multiplex, - # save only peaks proba - njobs=njobs) + n_jobs=njobs) df['tf'] = df['seed'] df['peak'] = df['target'] @@ -668,7 +668,7 @@ def define_binding_regions_from_config( peak_list = [] for layer in config['multiplex'][peak_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -683,8 +683,9 @@ def define_binding_regions_from_config( df = df[df['peak'].isin(peak_list)] if save is True: + print('Saving the result in ', output_f) assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the enhancers prediction result.' + 'to save the binding regions prediction result.' df.sort_values(by='score', ascending=False).to_csv(output_f, sep='\t', index=False, @@ -782,7 +783,6 @@ def define_target_genes_from_config( if update_config: eta = hummuspy.config.get_single_layer_eta(config, tf_multiplex) - lamb = hummuspy.config.get_target_genes_lamb(config, tf_multiplex, peak_multiplex, @@ -793,14 +793,15 @@ def define_target_genes_from_config( config['lamb'] = lamb # config = hummuspy.config.setup_proba_config(config, eta, lamb) - config_path = multilayer_f+'/'+config_folder+'/'+config_name - hummuspy.config.save_config(config, config_path) + # process the config to the right format to + # compute the random walks without local saving + config = hummuspy.config.process_config(config, multilayer_f) if gene_list is None: gene_list = [] for layer in config['multiplex'][rna_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -815,7 +816,7 @@ def define_target_genes_from_config( tf_list = [] for layer in config['multiplex'][tf_multiplex]['layers']: df_layer = pandas.read_csv( - multilayer_f+'/'+layer, + layer, sep='\t', header=None, index_col=None) @@ -827,16 +828,15 @@ def define_target_genes_from_config( )) tf_list = tf_list[tf_list != 'fake_node'] + config['seeds'] = tf_list + df = hummuspy.explore_network.compute_multiple_RandomWalk( - multilayer_f, - config_name=config_name, + **config, output_f=output_f, - list_seeds=tf_list, - config_folder=config_folder, save=False, return_df=return_df, spec_layer_result_saved=rna_multiplex, - njobs=njobs) + n_jobs=njobs) df['tf'] = df['seed'] df['gene'] = df['target'] @@ -847,8 +847,9 @@ def define_target_genes_from_config( df = df[df['gene'].isin(gene_list)] if save is True: + print('Saving the result in ', output_f) assert output_f is not None, 'You need to provide an output_f name ' +\ - 'to save the GRN result' + 'to save the terget gene predictions result' df.sort_values(by='score', ascending=False).to_csv(output_f, sep='\t', index=False, diff --git a/hummuspy/src/hummuspy/create_multilayer.py b/hummuspy/src/hummuspy/create_multilayer.py index ea80cd9..13500cf 100644 --- a/hummuspy/src/hummuspy/create_multilayer.py +++ b/hummuspy/src/hummuspy/create_multilayer.py @@ -566,7 +566,9 @@ def __par_seed_random_walk_restart(prox_vector, r): with joblib.parallel_config(backend="dask"): all_seeds_rwr_ranking_lst = Parallel()( delayed(__par_seed_random_walk_restart)(prox_vectors[i], r) - for i in range(len(seed_scores))) + for i in track(range(len(seed_scores)), + description="Processing seeds...", + total=len(seed_scores))) # divide per multiplex: start_end_nodes = [] diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index 151c030..9d87f76 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -24,7 +24,7 @@ resource_files: ```{r setup, include=FALSE} knitr::opts_chunk$set(eval = TRUE) -#devtools::install_github("cantinilab/HuMMuS") +devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") ``` # General description of the pipeline ![Overall pipeline](figures/schema_HuMMuS.png) @@ -49,9 +49,9 @@ knitr::opts_chunk$set(eval = TRUE) ```{r import_packages} library(reticulate) # install python dependency -py_install("hummuspy", envname = "r-reticulate", method="auto") +reticulate::py_install("hummuspy", envname = "r-reticulate", method="auto") reticulate::use_virtualenv("r-reticulate") -reticulate::import("hummuspy") +hummuspy <- reticulate::import("hummuspy") library(HuMMuS) ``` ## Download the single-cell data @@ -319,11 +319,11 @@ The GRN is defined using the multixrank algorithm. It requires to have grn <- define_grn( hummus, multilayer_f = "chen_multilayer", - njobs = 1 + njobs = 5 ) ``` ```{r head_grn, eval=FALSE} -head(grn) +grn ``` ### 3.4. Retrieve enhancers From 51c83b035a41e8c055b4e26fa08ff4bc2afffd66 Mon Sep 17 00:00:00 2001 From: r-trimbour Date: Mon, 15 Jul 2024 15:20:01 +0200 Subject: [PATCH 05/18] update dependencies hummuspy, allowing more recent versions of dask and distributed notably --- hummuspy/pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hummuspy/pyproject.toml b/hummuspy/pyproject.toml index 1977f7f..fbfa589 100644 --- a/hummuspy/pyproject.toml +++ b/hummuspy/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hummuspy" -version = "0.1.6" +version = "0.1.7" description = "HuMMuS is a novel method for the inference of regulatory mechanisms from multi-omics data with any type and number of omics, through a heterogeneous multilayer network framework." authors = ["RĂ©mi Trimbour "] license = "GPL-3.0-only" @@ -10,15 +10,15 @@ readme = "README.md" python = ">=3.8" multixrank = "^0.1" joblib = "^1.3.0" -tqdm = "^4.65.0" +tqdm = ">=4.65.0" numpy = "^1.24.2" pandas = "^2.0.0" pyyaml = "^6.0" matplotlib = "^3.4.3" scipy = "^1.8.0" -distributed = "^2023.0.0" -dask = "^2023.0.0" -rich = "^10.12.0" +distributed = ">=2023.0.0" +dask = ">=2023.0.0" +rich = ">=10.12.0" bokeh = ">=2.4.2,!=3.0.*" [build-system] From 9d9facd335c500464d532620131af85147ebc2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:59:31 +0200 Subject: [PATCH 06/18] test from conda_env pkgdown.yaml --- .github/workflows/pkgdown.yaml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 054462b..310afb4 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -35,18 +35,7 @@ jobs: python-version: '3.10' - name: Setup hummuspy env - run: reticulate::virtualenv_create("r-reticulate", Sys.which("python"), packages="hummuspy") - shell: Rscript {0} - - - name: ubuntu setup for Monocle3 - run: sudo apt-get install libgdal-dev libgeos-dev libproj-dev - - - name: Install Monocle3 - run: devtools::install_github('cole-trapnell-lab/monocle3') - shell: Rscript {0} - - - name: Install Cicero - run: devtools::install_github("cole-trapnell-lab/cicero-release", ref = "monocle3") + run: reticulate::conda_create("r-reticulate", packages="hummuspy") shell: Rscript {0} - name: Build site From 674e10434a07d1860e9509e479a5f7111f329e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:00:20 +0200 Subject: [PATCH 07/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 310afb4..7eb4c3e 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -35,7 +35,7 @@ jobs: python-version: '3.10' - name: Setup hummuspy env - run: reticulate::conda_create("r-reticulate", packages="hummuspy") + run: reticulate::conda_create("r-reticulate", packages="python") shell: Rscript {0} - name: Build site From 8908ce4ec43f20b781e9dbbea0d92b98bec5e5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:11:27 +0200 Subject: [PATCH 08/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 7eb4c3e..d91befb 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -35,7 +35,9 @@ jobs: python-version: '3.10' - name: Setup hummuspy env - run: reticulate::conda_create("r-reticulate", packages="python") + run: | + reticulate::conda_create("r-reticulate", packages="python") + reticulate::py_install("hummuspy", envname = "r-reticulate", method="auto") shell: Rscript {0} - name: Build site From e29867d488a687cd9cbc2db2bf97566fdf6f5a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:25:07 +0200 Subject: [PATCH 09/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index d91befb..85e0275 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -27,7 +27,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::pkgdown, local::., any::devtools + extra-packages: any::pkgdown, any::reticulate, any::devtools needs: website - uses: actions/setup-python@v2 From 1be744361c1a427a78c9118c340888783dc52af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:30:56 +0200 Subject: [PATCH 10/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 85e0275..4c32e25 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -37,7 +37,7 @@ jobs: - name: Setup hummuspy env run: | reticulate::conda_create("r-reticulate", packages="python") - reticulate::py_install("hummuspy", envname = "r-reticulate", method="auto") + reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) shell: Rscript {0} - name: Build site From e8088b9e7fbf3233a901acd062b8bb405e370533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:02:45 +0200 Subject: [PATCH 11/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 4c32e25..f4eb4ee 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -27,7 +27,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::pkgdown, any::reticulate, any::devtools + extra-packages: any::pkgdown, any::reticulate, local::., any::devtools needs: website - uses: actions/setup-python@v2 From aa993680006f65737f52c16a0e10dc3ad2bf7d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:34:54 +0200 Subject: [PATCH 12/18] Update chen_vignette.Rmd --- vignettes/chen_vignette.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index 9d87f76..fcafc71 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -49,7 +49,7 @@ devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") ```{r import_packages} library(reticulate) # install python dependency -reticulate::py_install("hummuspy", envname = "r-reticulate", method="auto") +# reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) reticulate::use_virtualenv("r-reticulate") hummuspy <- reticulate::import("hummuspy") library(HuMMuS) From cea0e30392c1104ea250ff9c07b1cfd5b545060b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:26:51 +0200 Subject: [PATCH 13/18] Update chen_vignette.Rmd --- vignettes/chen_vignette.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index fcafc71..6452ed9 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -50,7 +50,7 @@ devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") library(reticulate) # install python dependency # reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) -reticulate::use_virtualenv("r-reticulate") +reticulate::use_condaenv("r-reticulate") hummuspy <- reticulate::import("hummuspy") library(HuMMuS) ``` From b6c8ac591e5e26e531eaec6564315677fa4d666e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:55:40 +0200 Subject: [PATCH 14/18] Update add_networks.Rmd --- vignettes/add_networks.Rmd | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vignettes/add_networks.Rmd b/vignettes/add_networks.Rmd index 2399236..37e7871 100644 --- a/vignettes/add_networks.Rmd +++ b/vignettes/add_networks.Rmd @@ -20,9 +20,10 @@ knitr::opts_chunk$set(eval = TRUE) ##### 3. Add a new bipartite ```{r import_packages} -library(reticulate) # install python dependency -py_install("hummuspy", envname = "r-reticulate", method="auto") +# reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) +reticulate::use_condaenv("r-reticulate") +hummuspy <- reticulate::import("hummuspy") library(HuMMuS) ``` From 9dfe03c65ae99eb2e0bca482ffa0daf585359973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:29:05 +0200 Subject: [PATCH 15/18] Update chen_vignette.Rmd --- vignettes/chen_vignette.Rmd | 1 - 1 file changed, 1 deletion(-) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index 6452ed9..6753110 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -47,7 +47,6 @@ devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") ## 0. Setting up the environment ```{r import_packages} -library(reticulate) # install python dependency # reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) reticulate::use_condaenv("r-reticulate") From 9c5e464c5429ec38d4acd942299d7846277f1125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:33:32 +0200 Subject: [PATCH 16/18] Update pkgdown.yaml --- .github/workflows/pkgdown.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index f4eb4ee..665c7f9 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -36,7 +36,7 @@ jobs: - name: Setup hummuspy env run: | - reticulate::conda_create("r-reticulate", packages="python") + reticulate::conda_create("r-reticulate", packages="python==3.10") reticulate::py_install("hummuspy", envname = "r-reticulate", pip=TRUE) shell: Rscript {0} From df04ed58ef99346ceb443dd747dccf3126f364d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:54:06 +0200 Subject: [PATCH 17/18] Update chen_vignette.Rmd --- vignettes/chen_vignette.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index 6753110..eefcb8b 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -22,7 +22,7 @@ resource_files: *Documentation*: https://cantinilab.github.io/HuMMuS/ -```{r setup, include=FALSE} +```{r setup, include=FALSE, eval=False} knitr::opts_chunk$set(eval = TRUE) devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") ``` From 0846877ada2bd8e74a32d070ef8b6b6c18d95d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Trimbour?= <77151348+r-trimbour@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:14:20 +0200 Subject: [PATCH 18/18] Update chen_vignette.Rmd --- vignettes/chen_vignette.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/chen_vignette.Rmd b/vignettes/chen_vignette.Rmd index eefcb8b..6753110 100644 --- a/vignettes/chen_vignette.Rmd +++ b/vignettes/chen_vignette.Rmd @@ -22,7 +22,7 @@ resource_files: *Documentation*: https://cantinilab.github.io/HuMMuS/ -```{r setup, include=FALSE, eval=False} +```{r setup, include=FALSE} knitr::opts_chunk$set(eval = TRUE) devtools::install_github("cantinilab/HuMMuS", ref="dev_SeuratV5") ```