File size: 11,599 Bytes
932f7e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbddf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
932f7e0
 
 
 
 
 
 
 
 
 
 
 
cbddf96
932f7e0
 
 
 
 
 
 
 
 
 
 
 
 
 
cbddf96
932f7e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbddf96
 
 
 
932f7e0
cbddf96
 
 
 
 
 
 
 
 
932f7e0
cbddf96
 
932f7e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbddf96
 
 
 
932f7e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import argparse
import ast
import libsbml
import os
import re
import numpy as np
import pandas as pd
import sys
from utils.utils import *

def extract_genes_from_gpa(gpa_str):
    """Extract all gene IDs from a gpaAssociation string (e.g. 'G_A or (G_B and G_C)')."""
    if pd.isna(gpa_str) or str(gpa_str).strip() == "":
        return []
    tokens = re.findall(r'[A-Za-z_]\w*', str(gpa_str))
    keywords = {'or', 'and', 'not'}
    return [t for t in tokens if t.lower() not in keywords]

def parse_genes(gene_value):
    """Parse either a stringified gene list or a boolean gene expression."""
    if pd.isna(gene_value) or str(gene_value).strip() == "":
        return []

    if isinstance(gene_value, (list, tuple, set)):
        return list(gene_value)

    try:
        parsed = ast.literal_eval(str(gene_value))
    except (ValueError, SyntaxError):
        return extract_genes_from_gpa(gene_value)

    if isinstance(parsed, (list, tuple, set)):
        return list(parsed)
    return extract_genes_from_gpa(parsed)

def read_combined_geneExpr(combined_geneExpr_filename):
    if not os.path.isfile(combined_geneExpr_filename):
        raise FileNotFoundError(f"Missing combined normalized count file, expected at {combined_geneExpr_filename}")
    combined_geneExpr_df = pd.read_csv(combined_geneExpr_filename, index_col=0)
    return combined_geneExpr_df

def is_real_gene_with_mapping(gene):
    return not gene.startswith(("unmapped", "unknown", "Spontaneous", "Exchange", "Sink", "Diffusion"))

def read_system_gene(model_pre_filename):
    rxn_df = read_model_excel(model_pre_filename, "Reactions")

    # Support both stringified lists and boolean expressions in 'genes'.
    if "genes" in rxn_df.columns:
        gene_col = "genes"
    elif "gpaAssociation" in rxn_df.columns:
        gene_col = "gpaAssociation"
    else:
        raise ValueError(
            "Reactions sheet must have a 'genes' or 'gpaAssociation' column — "
            f"columns found: {list(rxn_df.columns)}"
        )

    system_genes_dict = {}
    missing_gene_system_dict = {}
    all_genes_set = set()
    for system, gene_list_raw in zip(rxn_df["system"], rxn_df[gene_col]):
        gene_list = parse_genes(gene_list_raw)

        if system != "" and system not in system_genes_dict:
            system_genes_dict[system] = set()

        for gene in gene_list:
            if system != "" and is_real_gene_with_mapping(gene):
                system_genes_dict[system].add(gene)
            if gene.startswith(("unmapped", "unknown")):
                missing_gene_system_dict[gene] = system
            if is_real_gene_with_mapping(gene):
                all_genes_set.add(gene)

    missing_gene_system_dict = dict(sorted(missing_gene_system_dict.items()))
    return system_genes_dict, missing_gene_system_dict, all_genes_set

def map_genes(mapping_dict, system_genes_dict, missing_gene_system_dict, all_genes_set):
    """Map model gene tags to expression IDs and retain unmapped tags for bootstrapping."""
    mapped_system_genes = {}
    missing_genes = dict(missing_gene_system_dict)

    for system, gene_set in system_genes_dict.items():
        mapped_genes = set()
        for gene in gene_set:
            if gene in mapping_dict:
                mapped_genes.add(mapping_dict[gene])
            else:
                missing_genes.setdefault(gene, system)
        if mapped_genes:
            mapped_system_genes[system] = mapped_genes

    mapped_all_genes = {mapping_dict[gene] for gene in all_genes_set if gene in mapping_dict}
    return mapped_system_genes, dict(sorted(missing_genes.items())), mapped_all_genes

def filter_geneExpr_df(combined_geneExpr_df, all_genes_set):
    geneExpr_df = combined_geneExpr_df.loc[combined_geneExpr_df.index.isin(all_genes_set)]
    return geneExpr_df

def get_system_gene_counts(system_genes_dict, gene_count_dict):
    system_gene_counts_dict = {}
    missing_genes = set()
    for system, gene_set in system_genes_dict.items():
        gene_counts = []
        for gene in sorted(gene_set):
            if gene in gene_count_dict:
                gene_counts.append(gene_count_dict[gene])
            else:
                missing_genes.add(gene)
        system_gene_counts_dict[system] = gene_counts
    if missing_genes:
        print("Warning   : These genes do not have normalized count values - {}".format(", ".join(missing_genes)))
    return system_gene_counts_dict

def initialize_df(geneExpr_sample, columns):
    new_geneExpr_df_sample = pd.concat([geneExpr_sample.rename(col) for col in columns], axis=1)
    return new_geneExpr_df_sample

def bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns):
    rng = np.random.default_rng(seed=0)
    sample_pool_all_genes = [gc for gene_counts in system_gene_counts_dict.values() for gc in gene_counts]
    new_geneExpr_df_sample_missing = pd.DataFrame(columns=columns)
    for missing_gene, system in missing_gene_system_dict.items():
        if system in system_gene_counts_dict:
            sample_pool = system_gene_counts_dict[system]
            new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool, len(columns))
        else:
            new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool_all_genes, len(columns))
    return new_geneExpr_df_sample_missing

def filter_by_batch(new_geneExpr_df, start, end, columns):
    target_columns = [f"{col}_{i}" for col in columns for i in range(start, end)]
    new_geneExpr_df_batch_df = new_geneExpr_df[target_columns]
    return new_geneExpr_df_batch_df

def bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count=1000):
    print("Read models from", ", ".join(model_pre_filenames))
    print("Read mapping tables from", ", ".join(mapping_filenames))
    print("Species prefixes:", ", ".join(species_prefixes))
    print("Read gene normalized counts from", combined_geneExpr_filename)
    os.makedirs(geneExpr_folder, exist_ok=True)

    total_to_sample = batch_count

    # column: sample_name
    # row: real genes for all species
    combined_geneExpr_df = read_combined_geneExpr(combined_geneExpr_filename)

    # column: A_1, A_2, ..., B_1, B_2, ...
    # row: (real genes with mapping + unmapped real genes + unknown genes) + Exchange + Sink for all species
    new_geneExpr_df = pd.DataFrame()
    
    for model_pre_filename, mapping_filename, species in zip(model_pre_filenames, mapping_filenames, species_prefixes): # loop by species
        print(f"Bootstrapping genes for {species}...")
        # mapping_dict: {model_tag in the model file: gene_id in the geneExpr file}
        mapping_dict = read_mapping(mapping_filename)
        
        # system_genes_dict: {system: set of real genes with mapping with that system}
        # missing_gene_system_dict: {unmapped real gene & unknown gene: system of the gene}
        # all_genes_set: set of all the real genes with mapping used in the model of that species
        system_genes_dict, missing_gene_system_dict, all_genes_set = read_system_gene(model_pre_filename)

        # Map model tags to the annotation IDs used in the expression file.
        system_genes_dict, missing_gene_system_dict, all_genes_set = map_genes(
            mapping_dict, system_genes_dict, missing_gene_system_dict, all_genes_set
        )

        # filter to get only geneExprs for that species
        geneExpr_df = filter_geneExpr_df(combined_geneExpr_df, all_genes_set)

        # column: A_1, A_2, ..., B_1, B_2, ...
        # row: (real genes with mapping + unmapped real genes + unknown genes) for one species
        new_geneExpr_df_species = pd.DataFrame()
        for sample in geneExpr_df.columns:
            # gene_count_dict: {gene: gene count}
            gene_count_dict = geneExpr_df[sample].to_dict()
            # system_gene_counts_dict: {system: list of gene counts of the genes with that system}
            system_gene_counts_dict = get_system_gene_counts(system_genes_dict, gene_count_dict)

            # column: (one sample) A_1, A_2, ..
            columns = [f"{sample}_{i+1}" for i in range(total_to_sample)]
            # row: real genes with mapping (same as geneExpr file)
            new_geneExpr_df_sample = initialize_df(geneExpr_df[sample], columns)
            # row: unmapped real genes + unknown genes 
            # (randomly bootstrap from the gene counts of the same species and the same sample and the same system)
            # (if the system of the missing gene do not have gene counts, randomly bootstrap from the gene counts of the same species and the same sample)
            new_geneExpr_df_sample_missing = bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns)
            # concat by rows: real genes with mapping + unmapped real genes + unknown genes
            new_geneExpr_df_sample = pd.concat([new_geneExpr_df_sample, new_geneExpr_df_sample_missing], axis=0)

            # concat by columns: A_1, A_2, ..., B_1, B_2, ...
            new_geneExpr_df_species = pd.concat([new_geneExpr_df_species, new_geneExpr_df_sample], axis=1)
            
        # concat by rows: species_1 + species_2, ...
        new_geneExpr_df = pd.concat([new_geneExpr_df, new_geneExpr_df_species], axis=0)

    new_geneExpr_df.loc["Spontaneous"] = 0
    new_geneExpr_df.loc["Exchange"] = 0
    new_geneExpr_df.loc["Sink"] = 0
    new_geneExpr_df.loc["Growth"] = 0
    new_geneExpr_df.loc["Diffusion"] = 0

    # separate into batch to different files
    print("Writing to files...")
    new_geneExpr_df_batch_filename = os.path.join(geneExpr_folder, "geneExpr")
    for i in range(batch_count):
        # first file: A_1, B_1, ..
        # second file: A_2, B_2, 
        new_geneExpr_df_batch_df = filter_by_batch(new_geneExpr_df, i+1, (i+1)+1, combined_geneExpr_df.columns)
        new_geneExpr_df_batch_df.to_csv(new_geneExpr_df_batch_filename + f"_{i+1}.csv")    
    print(f"Write to {new_geneExpr_df_batch_filename}_<1_{batch_count}>.csv")

if __name__ == "__main__":
    # define arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("--model_pre_filenames", nargs="+", required=True, help="List of parsed model files from construct_modularized_community_model tool")
    parser.add_argument("--mapping_filenames", nargs="+", required=True, help="List of mapping files to map gene in model to gene in annotation, from identifiers_mapping tool")
    parser.add_argument("--species_prefixes", nargs="+", required=True, help="List of species prefixes for the models, in the same order as input files")
    parser.add_argument("--combined_geneExpr_filename", required=True, help="The gene expression values for all species and for all samples")
    parser.add_argument("--geneExpr_folder", required=True, help="Folder containing gene expression files (geneExpr_<1-batch_count>.csv) with bootstrapped values added for unmapped and unknown genes")
    parser.add_argument("--batch_count", type=int, default=1000, help="Number of batch to bootstrap, write each batch to a file")
    args = parser.parse_args()

    # read arguments
    model_pre_filenames = args.model_pre_filenames
    mapping_filenames = args.mapping_filenames
    species_prefixes = args.species_prefixes
    combined_geneExpr_filename = args.combined_geneExpr_filename
    geneExpr_folder = args.geneExpr_folder
    batch_count = args.batch_count
    
    bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count)