# Brain's web app # Created in December 2016, slcu, hui # # Last modified 29 June 2017 [made function get_successors, udpated function get_predecessors] # Last modified 3 July 2017 [added method option, so users can input 'AT1G65480 leaf' to get neighours based on RNA-seq samples from leaf tissue. See get_method()] # Last modified 25 Jan 2018 [Note that if the 8th field (lst[8] in build_network_from_file()) is all 0.0. Edges will be missing. Search workaround in this file to 'fix' it. The 8th field represents the confidence of the edge.] # Last modified 1 Aug 2019. Can I share global variables G and SOURCE_NODES between requests? Failed to do that. The current solution is save two pickle files under the static folder. import networkx as nx from networkx.readwrite import json_graph import json import util_networkx import os import re import random import numpy as np import operator import math from datetime import datetime import time #import pickle import _pickle as cPickle import random ###################################### BRAIN_APP_PATH = '/var/www/brain/brain/' # the place of the application folder EDGE_FILE = '../Data/temp/edges.txt' # [change] EDGE_DB = BRAIN_APP_PATH + 'static/edges/edges.sqlite' TF_CATEGORY_FILE = '../Data/information/2D_table2.xls' # put TFs into four categories: TI, TN, GI, GN. SHOW_N_NODE_EACH_TIME = 50 TOP_N_NEIGHBOURS = 30 STRENGTH_TAU = 0.1 # don't show edges with low strength ALPHA = 0.6 ###################################### def scale_factor(n, N, LL, alpha): ''' n -- number of RNA-seq experiments N -- maximum number of RNA-seq experiments LL -- total log likelihood alpha -- weight for number of RNA-seq experiments ''' #print('DEBUG n=%d N=%d LL=%g' % (n,N,LL)) return alpha * (1.0*n/N) + (1-alpha)*(np.e**(1.0*min(LL,0)/n))**0.1 def get_method(s): s = s.lower() recognised_methods = ['root', 'bud', 'meristem', 'aerial', 'leaf', 'stem', 'seedling', 'seed', 'shoot', 'flower', 'mixreg', 'all', 'hclust.group', 'hclust.fixed.group', 'wedge', 'mixtool'] for x in recognised_methods: if x in s: return x return '' def contain_whole_word(s, t): ''' whole word s is in t. ''' lst = t.split(',') # t is sth like: 'shoot, all, seedling, hclust, seed, root' lst.append('') # handle the case when s is '', to make it true return s in lst def get_successors(G, gene, method): ''' Only select successors with good relationship strength with gene ''' node = gene successors = [] for n in G.successors(node): d = G.get_edge_data(node, n) max_strength = 0 for k in d.keys(): # select a best edge if d[k]['strength'] > max_strength and contain_whole_word(method, d[k]['method']): max_strength = d[k]['strength'] if max_strength > STRENGTH_TAU: successors.append(n) return successors def get_predecessors(G, gene, num, method): ''' Get top num predecessors of gene ''' node = gene predecessors = [] # don't include edges with small strength for n in G.predecessors(node): d = G.get_edge_data(n, node) max_strength = 0 for k in d.keys(): # select a best edge #print('DEBUG: %s %4.2f %4.2f' % (d[k]['link'], d[k]['weight'], d[k]['strength'] ) ) if d[k]['strength'] > max_strength and contain_whole_word(method, d[k]['method']): max_strength = d[k]['strength'] if max_strength > STRENGTH_TAU: predecessors.append(n) #print('DEBUG: len(predecessors)=%d num=%d' % (len(predecessors), num)) if len(predecessors) < num: return predecessors d1 = {} for n in predecessors: d = G.get_edge_data(n, node) max_strength = 0 for k in d.keys(): # select a best edge if d[k]['strength'] > max_strength: max_strength = d[k]['strength'] if n == 'AT3G50410' or n == 'AT1G52150': print('WARNING: %s should not be here. Method:%s.' % (n, method)) d1[n] = max_strength sorted_lst = sorted(d1.items(), key=operator.itemgetter(1), reverse=True) a, b = zip(*sorted_lst) return np.random.choice(a, num, replace=False, p=1.0 * np.array(b)/sum(b)) # choose with probability def get_max_rsubset_size(fname): ''' get the max number of RNA-seq experiment ''' max_rsize = 0 f = open(fname) for line in f: line = line.strip() lst = line.split('\t') condR_lst = [] if line != '' and len(lst) > 3: if len(lst) > 6: condR_lst = lst[4].split() size_condR = len(condR_lst) if size_condR > max_rsize: max_rsize = size_condR f.close() return max_rsize def build_network_from_file(edge_fname, tf_category_fname): EDGE_DIR = 'static/edges' tf_category_dict = get_tf_category_dict(tf_category_fname) ## build the network N = get_max_rsubset_size(edge_fname) G = nx.MultiDiGraph(max_rsubset_size=N) source_nodes = [] d = {} # count dict f = open(edge_fname) for line in f: line = line.strip() lst = line.split('\t') if line != '' and random.randint(1,10) < 10: strength = max(abs(float(lst[2])), float(lst[8])) # prevent strength being zero #strength = abs(float(lst[2])) # a workaround since lst[8] is now zero as time elapse, check compute_metric() in update_network.py for details method_or_tissue = lst[9] g1 = lst[0].split()[0] # target gene ID g1_label = lst[0].split()[1].split(';')[0] if lst[0].split()[1] != '.' else g1 g1_name = lst[0].split()[1] if lst[0].split()[1] != '.' else '' g2 = lst[1].split()[0] # source gene ID g2_label = lst[1].split()[1].split(';')[0] if lst[1].split()[1] != '.' else g2 g2_name = lst[1].split()[1] if lst[1].split()[1] != '.' else '' g1_link = '/' + EDGE_DIR.rstrip('/') + '/' + g1 + '.html' g2_link = '/' + EDGE_DIR.rstrip('/') + '/' + g2 + '.html' g1_category = tf_category_dict[g1] if g1 in tf_category_dict else [-9.0, 'N'] # -9.0 means not tissue-specific, and N means not inducible g1_TG_score = g1_category[0] g1_TG_category = TorGorN(g1_TG_score, 2.40) # T/G, 2.40 is selected to satisfy 'GI > TI and TN > GN' g1_IN_category = g1_category[1] # I/N g2_category = tf_category_dict[g2] if g2 in tf_category_dict else [-9.0, 'N'] g2_TG_score = g2_category[0] g2_TG_category = TorGorN(g2_TG_score, 2.40) # 'T' if g2_TG_score >= 2.40 else 'G' g2_IN_category = g2_category[1] G.add_node(g1, full_name=g1_name, label=g1_label, link=g1_link, istf='0', tf_category=g1_TG_category+g1_IN_category, tf_TG_score=g1_TG_score, tf_IN_category=g1_IN_category) # if g1 is also a TF, then istf='0' will overwrite it in the following for loop G.add_node(g2, full_name=g2_name, label=g2_label, link=g2_link, istf='1', tf_category=g2_TG_category+g2_IN_category, tf_TG_score=g2_TG_score, tf_IN_category=g2_IN_category) # tf_category contains default TF category code. It can be modified later given user's input if not g2+g1 in d: d[g2+g1] = 0 else: d[g2+g1] += 1 link_html_name = '/brain/edges/' + '%s_%s_%d.html' % (g2, g1, d[g2+g1]) G.add_edge(g2, g1, weight=float(lst[2]), link=link_html_name, strength=strength, method=method_or_tissue) # g2 is source, and g1 is target source_nodes.append(g2) f.close() source_nodes = list(set(source_nodes)) for n in source_nodes: G.node[n]['istf'] = '1' # force source nodes to be TFs return G, source_nodes def is_valid_query_gene(gid, G): if not re.match(r'AT\dG\d+', gid): return 'Invalid gene ID. Example of a valid gene ID: AT1G80870.' if not gid in G.nodes(): return 'Not in graph.' return '' def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def validate_chip_peak_file(fname, protein_id, protein_name, status, description): protein_id = protein_id.strip().upper() if protein_id == 'E.G., AT0G12345' or not protein_id.startswith('AT') or protein_id.find('G') != 3 or len(protein_id) != 9: return 'Invalide protein ID: %s.' % (protein_id) protein_name = protein_name.strip() description = description.strip() status = status.strip().upper() if status == 'UNSPECIFIED': return 'Please select ChIP-seq experiment status.' f = open(fname) lines = f.readlines() for line in lines: line = line.strip() lst = line.split('\t') if len(lst) < 10: # check we have at least 10 columns return 'The line %s should have at least 10 columns (see BED6+4 format).' % line if not lst[0].isdigit(): c = lst[0].lower() if (c.startswith('chr') and c[3:].isdigit()) or (c in ['mt', 'pt', 'ct']): pass else: return 'First column must be a chromosome number. [line: %s].' % line if not lst[1].isdigit(): return 'Second column (starting position) must be a number. [line: %s].' % line if not lst[2].isdigit(): return 'Third column (end position) must be a number. [line: %s].' % line if not lst[4].isdigit(): return 'Fourth column (binding strength) must be a number. [line: %s].' % line f.close() return '' def edit_chip_peak_file(fname, protein_id, protein_name, status, description): protein_id = protein_id.strip().upper() protein_name = protein_name.strip() description = description.strip() status = status.strip().upper() f = open(fname) lines = f.readlines() f.close() f = open(fname, 'w') f.write('# PROTEIN_ID:%s\n' % protein_id) f.write('# PROTEIN_NAME:%s\n' % protein_name.upper()) f.write('# STATUS:%s\n' % status.upper()) f.write('# DESCRIPTION:%s\n' % ' '.join([x.strip() for x in description.split('\n')])) f.write(''.join(lines)) f.close() # Taken from https://stackoverflow.com/questions/354038/how-do-i-check-if-a-string-is-a-number-float def is_number(s): try: float(s) return True except ValueError: return False def validate_and_edit_rnaseq_file(fname, genes): protein_id = genes.strip().upper() protein_id_lst = protein_id.split() for x in protein_id_lst: if not x.startswith('AT') or x.find('G') != 3 or len(x) != 9: return 'Invalide gene ID in %s: %s.' % (fname, x) fn = os.path.basename(fname)[16:] f = open(fname) lines = f.readlines() head_line = lines[0].strip() head_line_lst = head_line.split() if len(head_line_lst) > 2: return 'Invalid head line in %s: %s. The head line must contain one or two sample IDs.' % (fn, head_line) lineno = 1 for line in lines[1:]: line = line.strip() lst = line.split('\t') x = lst[0] # gene id if not x.startswith('AT') or x.find('G') != 3 or len(x) != 9: return 'Invalide gene ID in %s: %s at line %d.' % (fn, x, lineno) for x in lst[1:]: if not is_number(x): return 'Invalid number %s in %s.' % (x, fn) lineno += 1 f.close() return '' def write_gene_list(genes_str, directory, fname): lst = genes_str.strip().split() keep_lst = [] for x in lst: g = x.strip() if g != '' and g != 'AT0G12345': keep_lst.append(g) f = open(os.path.join(directory, fname), 'w') if len(keep_lst) > 0: f.write('\n'.join(keep_lst)) else: f.write('AT0G12345\n') # ghost gene f.close() def write_log_file(fname, s): # in the future, if we see concurrency problems, use the python logging module. f = open(fname, 'a') curr_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') f.write('[%s] %s\n' % (curr_time, s)) f.close() def number_of_columns(fname): ''' Return number of transcriptome columns in fname ''' f = open(fname) lines = f.readlines() f.close() if len(lines) < 2: return 0 second_line = lines[1] lst = second_line.split('\t') return len(lst) - 1 # number of transcriptome columns def map_to_transcriptome_space(fname): nc = number_of_columns(fname) if nc == 1: cmd = 'Rscript map_to_transcriptome_space.R %s' % (fname) os.system(cmd) elif nc == 2: cmd = 'Rscript map_to_transcriptome_space2.R %s' % (fname) os.system(cmd) else: return {} # an empty dictionary head = os.path.basename(fname)[:16] static_temp = './static/temp/' if not os.path.isdir(static_temp): # make the directory if not exists os.makedirs(static_temp) f = open(static_temp + head + 'result.txt') s = f.read() f.close() if nc == 1: return {'result': s, 'heatmap': head + 'heatmap.png', 'edges': head + 'edges.txt'} if nc == 2: return {'head': head, 'result': s, 'heatmap': head + 'heatmap.png', 'edges': head + 'edges.txt', 'diff_expr_genes': head + 'diff_expr_genes.txt'} def peak_to_target_genes(peak_file, protein_id): ''' Convert bed file to target gene list. Also make two files, date_time_targets.txt and date_time_tfs.txt. ''' static_temp = './static/temp' if not os.path.isdir(static_temp): # make the directory if not exists os.makedirs(static_temp) static_info = './static/info' info_bed_file = os.path.join(static_info, 'genesTAIR10.bed') fname_head = os.path.splitext(os.path.basename(peak_file))[0] fname_head = fname_head[:16] # get date and time info #fname_head = 'test' bedmap_result = fname_head + 'bedmap_result.txt' bedmap_result = os.path.join(static_temp, bedmap_result) cmd = 'bedmap --echo --echo-map-id-uniq --delim \'\t\' ' \ + '--range 3000' \ + ' ' + peak_file \ + ' ' + info_bed_file + ' > %s' % (bedmap_result) os.system(cmd) print(cmd) #cmd = 'cp ./static/temp/20170919_100450_bedmap_result.txt %s' % (bedmap_result) # [to be deleted] #os.system(cmd) # [to be deleted] f = open(bedmap_result) lines = f.readlines() d = {} for line in lines: line = line.strip() lst = line.split('\t') strength = lst[6] if len(lst) >= 11: genes = lst[10].replace('"','') # remove double-quotes for g in genes.split(';'): d[g] = strength f.close() tf_file = os.path.join(static_temp, fname_head + 'tfs.txt') f = open(tf_file, 'w') s = 'gene_id\tbinding_strength\n%s\t0.0\n' % (protein_id) f.write(s) f.close() target_file = os.path.join(static_temp, fname_head + 'targets.txt') s = 'gene_id\tbinding_strength\n' for k in sorted(d.keys()): s += '%s\t%s\n' % (k, d[k]) f = open(target_file, 'w') f.write(s) f.close() tissue_specific_edge_file = os.path.join(static_temp, fname_head + 'tissue_specific_edges.txt') # generated by edge_per_tissue.R result_dict = {'targets':target_file, 'tissue_specific_edges':tissue_specific_edge_file} return result_dict def get_go_enrichment(fname): # make a list of functional targets (from the tissue specific edge file) d = {} f = open(fname) lines = f.readlines() f.close() for line in lines[1:]: # skip the head line line = line.strip() lst = line.split('\t') target = (lst[0].split())[0] # gene id only, no gene name if not target.startswith('NA'): d[target] = 1 temp_fname = fname + '.temp' f = open(temp_fname, 'w') f.write('\n'.join(sorted(d.keys())) + '\n') f.close() # call goatools head = os.path.basename(fname)[0:16] go_enrichment_file = './static/temp/' + head + 'go_enrichment.txt' cmd = 'python /home/hui/software/goatools/goatools/scripts/find_enrichment.py %s /home/hui/software/goatools/goatools/data/population /home/hui/software/goatools/goatools/data/association --obo /home/hui/software/goatools/goatools/scripts/go-basic.obo > %s' % (temp_fname, go_enrichment_file) os.system(cmd) return go_enrichment_file def get_tf_category(fname): ''' Return a dictionary where key is gene id and value is TI/TN/GI/GN. ''' d = {} f = open(fname) lines = f.readlines() f.close() for line in lines[1:]: line = line.strip() lst = line.split('\t') gene = lst[0] category = lst[3] if not gene in d: d[gene] = category return d def weighted_score_for_tissue(a, b): a = a b = b # demoninator r = a / b scale = math.log(b+1, 2) # scale factor return scale * r def weighted_score_for_treatment(a, b): if a == 0: # avoid returning 1 in the case of 0/0 return 0 a = a + 1 b = b + 1 return a/b # avoid dividing by zeros def TorG(x, tau): ''' Phil's version ''' if (x >= tau): return ('T', x) else: return ('G', x) def TorGorN(x, tau): if (x >= tau): return 'T' elif x >= 0: return 'G' else: return 'N' def IorN(lst): ''' lst -- a list of tuples of the form [('cold', 2), ('heat', 2), ...]''' L = [x[1] for x in lst] maxL = max(L) if maxL >= 1: return ('I', maxL) else: return ('N', maxL) def get_tf_category_dict(fname): ''' fname is 2D_table2.xls, whose third column tissue.specific.score specifies the strength of being tissue-speficif. ''' f = open(fname) lines = f.readlines() f.close() d = {} # key is TF and value is category code TI/GI/TN/GN for line in lines[1:]: # skip the first line line = line.strip() lst = line.split('\t') tissue_score_lst = [] tname = ['flower', 'leaf', 'meristem', 'root', 'seed', 'seedling', 'silique', 'stem'] # tissue names in fname j = 0 for x in lst[5:13]: # tissue column starts from column 6 y = x.split('\\') t = (tname[j], weighted_score_for_tissue(int(y[0]), int(y[1]))) j += 1 tissue_score_lst.append(t) #torg = TorG(float(lst[2]), tissue_specific_tau) # tissue_specific_tau = 1.9 would make SEP3 a T torg_score = float(lst[2]) treat_score_lst = [] tname = ['cold', 'heat', 'wounding', 'drought'] j = 0 for x in lst[14:]: y = x.split('\\') t = (tname[j], weighted_score_for_treatment(int(y[0]), int(y[1]))) j += 1 treat_score_lst.append(t) iorn = IorN(treat_score_lst) tf_id = lst[0] if not tf_id in d: d[tf_id] = [torg_score, iorn[0]] return d def get_tissue_specific_tau(s): s = s.upper() index = s.find('TISSUE SPECIFIC TAU') if index < 0: return -9000.0 s2 = s[index+19:] for x in s2.split( ): if is_number(x): return float(x) return -9001.0 def get_user_option(s): ''' Return a dictionary containing user's input''' d = {} s = s.upper() lst = s.split() accepted_options = ['TISSUE SPECIFIC TAU', 'JUST TFS'] for x in accepted_options: if x in s and x == 'TISSUE SPECIFIC TAU': d[x] = get_tissue_specific_tau(s) elif x in s: d[x] = 1 return d def load_pickle_data(fname): with open(fname, 'rb') as f: return pickle.load(f) def num_line(fname): ''' Return number of lines in file fname. ''' if not os.path.exists(fname): return 0 f = open(fname) lines = f.readlines() f.close() return len(lines) if __name__ == "__main__": print('Call build_network_from_file ...') G, SOURCE_NODES = build_network_from_file(EDGE_FILE, TF_CATEGORY_FILE) # [change] use networkx binary file later with open('../Data/temp/SOURCE_NODES.json', 'w') as p: json.dump(SOURCE_NODES, p) #with open('../Data/temp/G.json', 'w') as p: # json.dump(json_graph.node_link_data(G), p) nx.write_gpickle(G, '../Data/temp/G.gpickle') print('Network updated and tf category loaded.')