Sanjana Vekhande commited on
Commit
932f7e0
·
1 Parent(s): b9691a8

initial build

Browse files
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ build-essential \
5
+ libxml2-dev \
6
+ libz-dev \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ RUN useradd -m -u 1000 user
10
+ USER user
11
+ ENV PATH="/home/user/.local/bin:$PATH"
12
+
13
+ WORKDIR /app
14
+
15
+ COPY --chown=user requirements.txt .
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ COPY --chown=user backend/ ./backend/
19
+ COPY --chown=user frontend/ ./frontend/
20
+ COPY --chown=user utils/ ./utils/
21
+
22
+ EXPOSE 7860
23
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
backend/__init__.py ADDED
File without changes
backend/main.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import io
3
+ import json
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import uuid
8
+ import zipfile
9
+ from contextlib import redirect_stdout
10
+ from pathlib import Path
11
+
12
+ from fastapi import FastAPI, File, Form, UploadFile
13
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+
16
+ app = FastAPI()
17
+
18
+ # In-memory job store
19
+ jobs: dict = {}
20
+
21
+
22
+ @app.post("/process")
23
+ async def process(
24
+ model_files: list[UploadFile] = File(...),
25
+ mapping_files: list[UploadFile] = File(...),
26
+ expr_file: UploadFile = File(...),
27
+ batch_count: int = Form(1000),
28
+ ):
29
+ job_id = str(uuid.uuid4())
30
+ jobs[job_id] = {
31
+ "status": "running",
32
+ "progress": 0,
33
+ "messages": [],
34
+ "output_zip": None,
35
+ "error": None,
36
+ "file_count": batch_count,
37
+ "species_count": len(model_files),
38
+ }
39
+
40
+ # Save uploaded files to temp dir
41
+ tmp = tempfile.mkdtemp()
42
+
43
+ model_paths = []
44
+ for f in model_files:
45
+ content = await f.read()
46
+ path = os.path.join(tmp, f.filename)
47
+ with open(path, "wb") as out:
48
+ out.write(content)
49
+ model_paths.append(path)
50
+
51
+ mapping_paths = []
52
+ for f in mapping_files:
53
+ content = await f.read()
54
+ path = os.path.join(tmp, f.filename)
55
+ with open(path, "wb") as out:
56
+ out.write(content)
57
+ mapping_paths.append(path)
58
+
59
+ expr_content = await expr_file.read()
60
+ expr_path = os.path.join(tmp, expr_file.filename)
61
+ with open(expr_path, "wb") as out:
62
+ out.write(expr_content)
63
+
64
+ species_prefixes = [Path(f.filename).stem.split("_")[0] for f in model_files]
65
+
66
+ asyncio.create_task(
67
+ run_bootstrap(job_id, model_paths, mapping_paths, expr_path, species_prefixes, batch_count, tmp)
68
+ )
69
+
70
+ return {"job_id": job_id}
71
+
72
+
73
+ async def run_bootstrap(job_id, model_paths, mapping_paths, expr_path, species_prefixes, batch_count, tmp):
74
+ job = jobs[job_id]
75
+
76
+ def add_msg(text, type_=""):
77
+ job["messages"].append({"text": text, "type": type_})
78
+
79
+ try:
80
+ add_msg(f"Starting bootstrap for: {', '.join(species_prefixes)}", "info")
81
+ job["progress"] = 10
82
+
83
+ output_dir = os.path.join(tmp, "output")
84
+ os.makedirs(output_dir, exist_ok=True)
85
+
86
+ captured = io.StringIO()
87
+
88
+ def run_sync():
89
+ sys.path.insert(0, "/app")
90
+ from utils.bootstrap_genes import bootstrap_genes
91
+ with redirect_stdout(captured):
92
+ bootstrap_genes(
93
+ model_pre_filenames=model_paths,
94
+ mapping_filenames=mapping_paths,
95
+ species_prefixes=species_prefixes,
96
+ combined_geneExpr_filename=expr_path,
97
+ geneExpr_folder=output_dir,
98
+ batch_count=batch_count,
99
+ )
100
+
101
+ job["progress"] = 20
102
+ loop = asyncio.get_event_loop()
103
+ await loop.run_in_executor(None, run_sync)
104
+ job["progress"] = 80
105
+
106
+ # Relay captured stdout as messages
107
+ for line in captured.getvalue().strip().split("\n"):
108
+ line = line.strip()
109
+ if not line:
110
+ continue
111
+ if "Write to" in line:
112
+ type_ = "ok"
113
+ elif line.startswith("Warning"):
114
+ type_ = "" # warnings are noisy — show but no colour
115
+ else:
116
+ type_ = "info"
117
+ add_msg(line, type_)
118
+
119
+ add_msg("Compressing output files…", "info")
120
+ job["progress"] = 90
121
+
122
+ zip_path = os.path.join(tmp, "geneExpr_bootstrapped.zip")
123
+ output_files = sorted([f for f in os.listdir(output_dir) if f.endswith(".csv")])
124
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
125
+ for fname in output_files:
126
+ zf.write(os.path.join(output_dir, fname), arcname=fname)
127
+
128
+ job["output_zip"] = zip_path
129
+ job["file_count"] = batch_count
130
+ job["species_count"] = len(species_prefixes)
131
+ job["progress"] = 100
132
+ job["status"] = "done"
133
+ add_msg(f"Done! {batch_count} files generated.", "ok")
134
+
135
+ except Exception as e:
136
+ import traceback
137
+ job["status"] = "error"
138
+ job["error"] = str(e)
139
+ add_msg(f"Error: {e}", "err")
140
+ print(traceback.format_exc(), flush=True)
141
+
142
+
143
+ @app.get("/progress/{job_id}")
144
+ async def progress_stream(job_id: str):
145
+ async def event_stream():
146
+ last_idx = 0
147
+ while True:
148
+ job = jobs.get(job_id)
149
+ if not job:
150
+ yield f"data: {json.dumps({'status': 'error', 'error': 'Job not found'})}\n\n"
151
+ break
152
+
153
+ new_msgs = job["messages"][last_idx:]
154
+ last_idx = len(job["messages"])
155
+
156
+ for msg in new_msgs:
157
+ payload = {
158
+ "message": msg["text"],
159
+ "type": msg["type"],
160
+ "progress": job["progress"],
161
+ "label": msg["text"],
162
+ }
163
+ yield f"data: {json.dumps(payload)}\n\n"
164
+
165
+ if job["status"] == "done":
166
+ yield f"data: {json.dumps({'status': 'done', 'progress': 100, 'file_count': job.get('file_count'), 'species_count': job.get('species_count')})}\n\n"
167
+ break
168
+ elif job["status"] == "error":
169
+ yield f"data: {json.dumps({'status': 'error', 'error': job.get('error', 'Unknown error')})}\n\n"
170
+ break
171
+
172
+ if not new_msgs:
173
+ yield f"data: {json.dumps({'progress': job['progress'], 'label': 'Processing…'})}\n\n"
174
+
175
+ await asyncio.sleep(1)
176
+
177
+ return StreamingResponse(
178
+ event_stream(),
179
+ media_type="text/event-stream",
180
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
181
+ )
182
+
183
+
184
+ @app.get("/download/{job_id}")
185
+ async def download(job_id: str):
186
+ job = jobs.get(job_id, {})
187
+ zip_path = job.get("output_zip")
188
+ if not zip_path or not os.path.exists(zip_path):
189
+ return JSONResponse({"error": "File not found"}, status_code=404)
190
+ return FileResponse(
191
+ zip_path,
192
+ filename="geneExpr_bootstrapped.zip",
193
+ media_type="application/zip",
194
+ )
195
+
196
+
197
+ # Serve frontend — must be last
198
+ app.mount("/", StaticFiles(directory="/app/frontend", html=True), name="frontend")
frontend/index.html ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Gene Expression Bootstrapper</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"/>
8
+ <style>
9
+ :root {
10
+ --bg: #EEF1F8;
11
+ --surface: #FFFFFF;
12
+ --border: #D8DDED;
13
+ --ink: #0A0E1F;
14
+ --ink2: #4A5270;
15
+ --ink3: #8A91AD;
16
+ --teal: #4F46E5;
17
+ --teal-l: #EFEEFD;
18
+ --teal-d: #3730A3;
19
+ --coral: #7C3AED;
20
+ --coral-l: #F5F3FF;
21
+ --r: 10px;
22
+ --shadow: 0 1px 4px rgba(10,14,31,0.07);
23
+ --shadow-md:0 4px 16px rgba(10,14,31,0.12);
24
+ }
25
+
26
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
+
28
+ body {
29
+ background: var(--bg);
30
+ color: var(--ink);
31
+ font-family: 'DM Sans', -apple-system, sans-serif;
32
+ font-size: 16px;
33
+ line-height: 1.65;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ /* ── Page ───────────────────────────── */
38
+ .page {
39
+ max-width: 700px;
40
+ margin: 0 auto;
41
+ padding: 4rem 1.5rem 6rem;
42
+ }
43
+
44
+ /* ── Header ─────────────────────────── */
45
+ header {
46
+ margin-bottom: 3rem;
47
+ position: relative;
48
+ }
49
+
50
+ .header-tag {
51
+ display: inline-block;
52
+ font-family: 'DM Sans', sans-serif;
53
+ font-size: 11px;
54
+ font-weight: 600;
55
+ letter-spacing: 0.12em;
56
+ text-transform: uppercase;
57
+ color: var(--coral);
58
+ margin-bottom: 0.75rem;
59
+ }
60
+
61
+ header h1 {
62
+ font-family: 'Playfair Display', Georgia, serif;
63
+ font-size: 3rem;
64
+ font-weight: 700;
65
+ line-height: 1.1;
66
+ letter-spacing: -0.01em;
67
+ color: var(--ink);
68
+ margin-bottom: 0.3rem;
69
+ }
70
+
71
+ header h1 em {
72
+ font-style: italic;
73
+ font-weight: 400;
74
+ color: var(--teal);
75
+ }
76
+
77
+ .header-sub {
78
+ font-size: 14px;
79
+ color: var(--ink2);
80
+ line-height: 1.7;
81
+ max-width: 480px;
82
+ margin-top: 0.75rem;
83
+ }
84
+
85
+ .header-rule {
86
+ width: 48px;
87
+ height: 3px;
88
+ background: var(--coral);
89
+ border-radius: 2px;
90
+ margin-top: 1.5rem;
91
+ }
92
+
93
+ /* ── Section title ───────────────────── */
94
+ .section-label {
95
+ font-size: 10px;
96
+ font-weight: 600;
97
+ letter-spacing: 0.14em;
98
+ text-transform: uppercase;
99
+ color: var(--ink3);
100
+ margin-bottom: 1.5rem;
101
+ margin-top: 2.5rem;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 10px;
105
+ }
106
+
107
+ .section-label::after {
108
+ content: '';
109
+ flex: 1;
110
+ height: 1px;
111
+ background: var(--border);
112
+ }
113
+
114
+ /* ── Optional block ──────────────────── */
115
+ .opt-block {
116
+ background: var(--surface);
117
+ border: 1px solid var(--border);
118
+ border-radius: var(--r);
119
+ padding: 1.25rem 1.5rem;
120
+ box-shadow: var(--shadow);
121
+ margin-bottom: 2.5rem;
122
+ display: flex;
123
+ gap: 1.25rem;
124
+ align-items: flex-start;
125
+ }
126
+
127
+ .opt-tag {
128
+ background: var(--coral-l);
129
+ color: var(--coral);
130
+ border: 1px solid #E8C4B3;
131
+ font-size: 9px;
132
+ font-weight: 700;
133
+ letter-spacing: 0.1em;
134
+ text-transform: uppercase;
135
+ padding: 3px 7px;
136
+ border-radius: 4px;
137
+ margin-top: 3px;
138
+ white-space: nowrap;
139
+ }
140
+
141
+ .opt-inner { flex: 1; }
142
+
143
+ .opt-title {
144
+ font-size: 16px;
145
+ font-weight: 600;
146
+ color: var(--ink);
147
+ margin-bottom: 0.15rem;
148
+ }
149
+
150
+ .opt-desc {
151
+ font-size: 14px;
152
+ color: var(--ink2);
153
+ margin-bottom: 0.85rem;
154
+ }
155
+
156
+ /* ── Slider ──────────────────────────── */
157
+ .slider-row {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 1rem;
161
+ }
162
+
163
+ .slider-val {
164
+ font-family: 'Playfair Display', serif;
165
+ font-size: 1.5rem;
166
+ font-weight: 700;
167
+ color: var(--teal);
168
+ min-width: 58px;
169
+ text-align: right;
170
+ letter-spacing: -0.02em;
171
+ }
172
+
173
+ input[type="range"] {
174
+ flex: 1;
175
+ -webkit-appearance: none;
176
+ height: 3px;
177
+ background: var(--border);
178
+ border-radius: 2px;
179
+ outline: none;
180
+ cursor: pointer;
181
+ }
182
+
183
+ input[type="range"]::-webkit-slider-thumb {
184
+ -webkit-appearance: none;
185
+ width: 18px; height: 18px;
186
+ border-radius: 50%;
187
+ background: var(--teal);
188
+ cursor: pointer;
189
+ border: 3px solid var(--surface);
190
+ box-shadow: 0 0 0 2px var(--teal);
191
+ transition: box-shadow 0.15s;
192
+ }
193
+
194
+ input[type="range"]::-webkit-slider-thumb:hover {
195
+ box-shadow: 0 0 0 4px rgba(45,106,106,0.2);
196
+ }
197
+
198
+ .slider-labels {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ font-size: 11px;
202
+ color: var(--ink3);
203
+ margin-top: 0.4rem;
204
+ }
205
+
206
+ .slider-note {
207
+ font-size: 11px;
208
+ color: var(--ink3);
209
+ margin-top: 0.5rem;
210
+ }
211
+
212
+ /* ── Step row ────────────────────────── */
213
+ .step {
214
+ display: flex;
215
+ gap: 1.25rem;
216
+ margin-bottom: 1.25rem;
217
+ animation: fadeUp 0.35s ease both;
218
+ }
219
+
220
+ .step:nth-child(1) { animation-delay: 0.04s; }
221
+ .step:nth-child(2) { animation-delay: 0.08s; }
222
+ .step:nth-child(3) { animation-delay: 0.12s; }
223
+ .step:nth-child(4) { animation-delay: 0.16s; }
224
+
225
+ .step-num {
226
+ width: 30px; height: 30px;
227
+ min-width: 30px;
228
+ border-radius: 7px;
229
+ background: transparent;
230
+ border: 2px solid var(--ink);
231
+ color: var(--ink);
232
+ font-size: 13px;
233
+ font-weight: 700;
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ margin-top: 2px;
238
+ font-family: 'DM Sans', sans-serif;
239
+ transition: all 0.2s;
240
+ }
241
+
242
+ .step-num.done {
243
+ background: var(--teal);
244
+ border-color: var(--teal);
245
+ color: #fff;
246
+ }
247
+
248
+ .step-content { flex: 1; }
249
+
250
+ .step-title {
251
+ font-size: 16px;
252
+ font-weight: 600;
253
+ color: var(--ink);
254
+ margin-bottom: 0.2rem;
255
+ }
256
+
257
+ .step-desc {
258
+ font-size: 14px;
259
+ color: var(--ink2);
260
+ margin-bottom: 0.75rem;
261
+ line-height: 1.5;
262
+ }
263
+
264
+ /* ── Card ────────────────────────────── */
265
+ .card {
266
+ background: var(--surface);
267
+ border: 1px solid var(--border);
268
+ border-radius: var(--r);
269
+ padding: 1.1rem 1.25rem;
270
+ box-shadow: var(--shadow);
271
+ }
272
+
273
+ /* ── Drop zone ───────────────────────── */
274
+ .drop-zone {
275
+ border: 1.5px dashed var(--border);
276
+ border-radius: 8px;
277
+ padding: 2rem 1.5rem;
278
+ text-align: center;
279
+ cursor: pointer;
280
+ position: relative;
281
+ transition: all 0.15s;
282
+ background: var(--bg);
283
+ }
284
+
285
+ .drop-zone:hover {
286
+ border-color: var(--teal);
287
+ background: var(--teal-l);
288
+ }
289
+
290
+ .drop-zone input[type="file"] {
291
+ position: absolute; inset: 0;
292
+ opacity: 0; cursor: pointer;
293
+ width: 100%; height: 100%;
294
+ }
295
+
296
+ .drop-icon {
297
+ font-size: 1.5rem;
298
+ margin-bottom: 0.4rem;
299
+ display: block;
300
+ }
301
+
302
+ .drop-text {
303
+ font-size: 15px;
304
+ color: var(--ink2);
305
+ line-height: 1.6;
306
+ }
307
+
308
+ .drop-text strong {
309
+ color: var(--ink);
310
+ display: block;
311
+ font-weight: 600;
312
+ }
313
+
314
+ .drop-text small {
315
+ color: var(--ink3);
316
+ font-size: 11px;
317
+ margin-top: 2px;
318
+ display: block;
319
+ }
320
+
321
+ /* ── File chips ──────────────────────── */
322
+ .file-chips {
323
+ display: flex;
324
+ flex-wrap: wrap;
325
+ gap: 0.4rem;
326
+ margin-top: 0.65rem;
327
+ }
328
+
329
+ .chip {
330
+ display: inline-flex;
331
+ align-items: center;
332
+ gap: 5px;
333
+ background: var(--teal-l);
334
+ border: 1px solid #B2D4D4;
335
+ color: var(--teal-d);
336
+ padding: 3px 10px;
337
+ border-radius: 100px;
338
+ font-size: 12px;
339
+ font-weight: 500;
340
+ }
341
+
342
+ /* ── Run section ─────────────────────── */
343
+ .run-section {
344
+ margin-top: 2.5rem;
345
+ }
346
+
347
+ .btn-run {
348
+ width: 100%;
349
+ padding: 1rem 1.25rem;
350
+ background: var(--ink);
351
+ color: var(--bg);
352
+ border: none;
353
+ border-radius: var(--r);
354
+ font-family: 'DM Sans', sans-serif;
355
+ font-size: 16px;
356
+ font-weight: 600;
357
+ cursor: pointer;
358
+ transition: all 0.15s;
359
+ letter-spacing: 0.01em;
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ gap: 8px;
364
+ }
365
+
366
+ .btn-run:hover:not(:disabled) {
367
+ background: var(--teal);
368
+ box-shadow: var(--shadow-md);
369
+ transform: translateY(-1px);
370
+ }
371
+
372
+ .btn-run:disabled {
373
+ opacity: 0.4;
374
+ cursor: not-allowed;
375
+ transform: none;
376
+ }
377
+
378
+ /* ── Progress ────────────────────────── */
379
+ .progress-header {
380
+ display: flex;
381
+ justify-content: space-between;
382
+ align-items: center;
383
+ font-size: 13px;
384
+ margin-bottom: 0.5rem;
385
+ }
386
+
387
+ .progress-label { font-weight: 600; color: var(--ink); }
388
+ .progress-pct { color: var(--ink2); font-variant-numeric: tabular-nums; }
389
+
390
+ .progress-track {
391
+ height: 5px;
392
+ background: var(--border);
393
+ border-radius: 3px;
394
+ overflow: hidden;
395
+ }
396
+
397
+ .progress-fill {
398
+ height: 100%;
399
+ background: linear-gradient(90deg, var(--teal), #4A9F9F);
400
+ border-radius: 3px;
401
+ transition: width 0.45s ease;
402
+ width: 0%;
403
+ }
404
+
405
+ .log-box {
406
+ background: #1C1A17;
407
+ color: #A89F94;
408
+ border-radius: 8px;
409
+ padding: 0.85rem 1rem;
410
+ font-family: 'SF Mono', 'Fira Code', monospace;
411
+ font-size: 11px;
412
+ line-height: 1.9;
413
+ max-height: 140px;
414
+ overflow-y: auto;
415
+ margin-top: 0.75rem;
416
+ white-space: pre-wrap;
417
+ }
418
+
419
+ .log-ok { color: #6FCF97; }
420
+ .log-err { color: #EB5757; }
421
+ .log-info { color: #56CCF2; }
422
+
423
+ /* ── Results ─────────────────────────── */
424
+ .stats-row {
425
+ display: grid;
426
+ grid-template-columns: 1fr 1fr;
427
+ gap: 0.65rem;
428
+ margin-bottom: 1rem;
429
+ }
430
+
431
+ .stat-box {
432
+ background: var(--bg);
433
+ border: 1px solid var(--border);
434
+ border-radius: 8px;
435
+ padding: 1rem 1.1rem;
436
+ }
437
+
438
+ .stat-val {
439
+ font-family: 'Playfair Display', serif;
440
+ font-size: 2rem;
441
+ font-weight: 700;
442
+ color: var(--ink);
443
+ line-height: 1;
444
+ margin-bottom: 0.25rem;
445
+ letter-spacing: -0.02em;
446
+ }
447
+
448
+ .stat-label {
449
+ font-size: 11px;
450
+ color: var(--ink2);
451
+ font-weight: 500;
452
+ text-transform: uppercase;
453
+ letter-spacing: 0.07em;
454
+ }
455
+
456
+ .btn-download {
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ gap: 8px;
461
+ width: 100%;
462
+ padding: 0.9rem 1.25rem;
463
+ background: var(--teal-l);
464
+ color: var(--teal-d);
465
+ border: 1.5px solid var(--teal);
466
+ border-radius: var(--r);
467
+ font-family: 'DM Sans', sans-serif;
468
+ font-size: 14px;
469
+ font-weight: 600;
470
+ cursor: pointer;
471
+ text-decoration: none;
472
+ transition: all 0.15s;
473
+ }
474
+
475
+ .btn-download:hover {
476
+ background: var(--teal);
477
+ color: white;
478
+ transform: translateY(-1px);
479
+ box-shadow: var(--shadow-md);
480
+ }
481
+
482
+ /* ── Footer ──────────────────────────── */
483
+ footer {
484
+ margin-top: 4rem;
485
+ padding-top: 1.5rem;
486
+ border-top: 1px solid var(--border);
487
+ font-size: 11px;
488
+ color: var(--ink3);
489
+ line-height: 1.9;
490
+ }
491
+
492
+ footer a { color: var(--ink2); text-decoration: none; }
493
+ footer a:hover { color: var(--ink); }
494
+
495
+ /* ── Animations ──────────────────────── */
496
+ @keyframes fadeUp {
497
+ from { opacity: 0; transform: translateY(10px); }
498
+ to { opacity: 1; transform: translateY(0); }
499
+ }
500
+
501
+ .hidden { display: none !important; }
502
+
503
+ ::-webkit-scrollbar { width: 5px; }
504
+ ::-webkit-scrollbar-track { background: transparent; }
505
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
506
+ </style>
507
+ </head>
508
+ <body>
509
+ <div class="page">
510
+
511
+ <!-- Header -->
512
+ <header>
513
+ <div class="header-tag">MeMo Modelling · A*STAR</div>
514
+ <h1>Gene Expression<br><em>Bootstrapper</em></h1>
515
+ <div class="header-rule"></div>
516
+ <p class="header-sub">Fills in missing expression values for unmapped genes by sampling from the same metabolic subsystem. Output serves as input for FEA.</p>
517
+ </header>
518
+
519
+ <!-- Optional: batch count -->
520
+ <div class="opt-block">
521
+ <div class="opt-tag">Optional</div>
522
+ <div class="opt-inner">
523
+ <div class="opt-title">Bootstrap iterations</div>
524
+ <div class="opt-desc">Number of output CSV files to generate. Default of 1,000 is recommended.</div>
525
+ <div class="slider-row">
526
+ <input type="range" id="batchSlider" min="100" max="5000" step="100" value="1000"
527
+ oninput="updateSlider(this.value)"/>
528
+ <div class="slider-val" id="batchVal">1,000</div>
529
+ </div>
530
+ <div class="slider-labels"><span>100</span><span>5,000</span></div>
531
+ <div class="slider-note">Higher values = more robust results, longer processing time.</div>
532
+ </div>
533
+ </div>
534
+
535
+ <!-- Upload steps -->
536
+ <div class="section-label">Upload your files</div>
537
+
538
+ <!-- Step 1 -->
539
+ <div class="step">
540
+ <div class="step-num" id="num1">1</div>
541
+ <div class="step-content">
542
+ <div class="step-title">Prefixed model files</div>
543
+ <div class="step-desc">One .xlsx per species — output from Step 4 of the Metabolic Community Pipeline.</div>
544
+ <div class="card">
545
+ <div class="drop-zone" id="dropModel">
546
+ <input type="file" id="fileModel" accept=".xlsx" multiple onchange="handleFiles('model', this.files)"/>
547
+ <span class="drop-icon">📁</span>
548
+ <div class="drop-text">
549
+ <strong>Drop model files here</strong>
550
+ or click to browse
551
+ <small>.xlsx only · multiple files accepted</small>
552
+ </div>
553
+ </div>
554
+ <div class="file-chips" id="chipsModel"></div>
555
+ </div>
556
+ </div>
557
+ </div>
558
+
559
+ <!-- Step 2 -->
560
+ <div class="step">
561
+ <div class="step-num" id="num2">2</div>
562
+ <div class="step-content">
563
+ <div class="step-title">Identifier mapping files</div>
564
+ <div class="step-desc">One .xlsx per species, in the same order as model files — output from Step 3 of the Pipeline.</div>
565
+ <div class="card">
566
+ <div class="drop-zone" id="dropMapping">
567
+ <input type="file" id="fileMapping" accept=".xlsx" multiple onchange="handleFiles('mapping', this.files)"/>
568
+ <span class="drop-icon">📁</span>
569
+ <div class="drop-text">
570
+ <strong>Drop mapping files here</strong>
571
+ or click to browse
572
+ <small>.xlsx only · same order as model files</small>
573
+ </div>
574
+ </div>
575
+ <div class="file-chips" id="chipsMapping"></div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- Step 3 -->
581
+ <div class="step">
582
+ <div class="step-num" id="num3">3</div>
583
+ <div class="step-content">
584
+ <div class="step-title">Combined gene expression CSV</div>
585
+ <div class="step-desc">Normalised gene expression counts for all species and samples from your metatranscriptomics data.</div>
586
+ <div class="card">
587
+ <div class="drop-zone" id="dropExpr">
588
+ <input type="file" id="fileExpr" accept=".csv" onchange="handleFiles('expr', this.files)"/>
589
+ <span class="drop-icon">📄</span>
590
+ <div class="drop-text">
591
+ <strong>Drop gene expression CSV here</strong>
592
+ or click to browse
593
+ <small>.csv only · single file</small>
594
+ </div>
595
+ </div>
596
+ <div class="file-chips" id="chipsExpr"></div>
597
+ </div>
598
+ </div>
599
+ </div>
600
+
601
+ <!-- Run -->
602
+ <div class="run-section">
603
+ <button class="btn-run" id="runBtn" onclick="startJob()">
604
+ Generate Bootstrapped Files
605
+ </button>
606
+ </div>
607
+
608
+ <!-- Progress -->
609
+ <div class="hidden" id="progressSection">
610
+ <div class="section-label" style="margin-top:2.5rem">Processing</div>
611
+ <div class="card">
612
+ <div class="progress-header">
613
+ <span class="progress-label" id="progressLabel">Starting…</span>
614
+ <span class="progress-pct" id="progressPct">0%</span>
615
+ </div>
616
+ <div class="progress-track">
617
+ <div class="progress-fill" id="progressBar"></div>
618
+ </div>
619
+ <div class="log-box" id="logBox"></div>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- Results -->
624
+ <div class="hidden" id="resultsSection">
625
+ <div class="section-label" style="margin-top:2.5rem">Results</div>
626
+ <div class="card">
627
+ <div class="stats-row">
628
+ <div class="stat-box">
629
+ <div class="stat-val" id="statFiles">—</div>
630
+ <div class="stat-label">Files generated</div>
631
+ </div>
632
+ <div class="stat-box">
633
+ <div class="stat-val" id="statSpecies">—</div>
634
+ <div class="stat-label">Species processed</div>
635
+ </div>
636
+ </div>
637
+ <a class="btn-download" id="downloadBtn" href="#" download="geneExpr_bootstrapped.zip">
638
+ ⬇ Download Bootstrapped ZIP
639
+ </a>
640
+ </div>
641
+ </div>
642
+
643
+ <footer>
644
+ © 2026 A*STAR Bioinformatics Institute &nbsp;·&nbsp;
645
+ Free for academic research and education &nbsp;·&nbsp;
646
+ Commercial licensing: <a href="mailto:yeohc@a-star.edu.sg">yeohc@a-star.edu.sg</a>
647
+ </footer>
648
+
649
+ </div>
650
+
651
+ <script>
652
+ const state = { model: [], mapping: [], expr: [] };
653
+
654
+ function updateSlider(val) {
655
+ document.getElementById('batchVal').textContent = Number(val).toLocaleString();
656
+ const pct = (val - 100) / (5000 - 100) * 100;
657
+ document.getElementById('batchSlider').style.background =
658
+ `linear-gradient(to right, var(--teal) ${pct}%, var(--border) ${pct}%)`;
659
+ }
660
+
661
+ window.addEventListener('DOMContentLoaded', () => updateSlider(1000));
662
+
663
+ function handleFiles(type, files) {
664
+ state[type] = Array.from(files);
665
+ const key = { model: 'chipsModel', mapping: 'chipsMapping', expr: 'chipsExpr' }[type];
666
+ const num = { model: 'num1', mapping: 'num2', expr: 'num3' }[type];
667
+ document.getElementById(key).innerHTML =
668
+ state[type].map(f => `<div class="chip">✓ ${f.name}</div>`).join('');
669
+ document.getElementById(num).classList.add('done');
670
+ }
671
+
672
+ ['dropModel','dropMapping','dropExpr'].forEach(id => {
673
+ const el = document.getElementById(id);
674
+ el.addEventListener('dragover', e => { e.preventDefault(); el.style.borderColor = 'var(--teal)'; });
675
+ el.addEventListener('dragleave', () => el.style.borderColor = '');
676
+ el.addEventListener('drop', () => el.style.borderColor = '');
677
+ });
678
+
679
+ async function startJob() {
680
+ if (!state.model.length) { alert('Please upload at least one model file.'); return; }
681
+ if (!state.mapping.length) { alert('Please upload at least one mapping file.'); return; }
682
+ if (!state.expr.length) { alert('Please upload a gene expression CSV.'); return; }
683
+
684
+ document.getElementById('runBtn').disabled = true;
685
+ document.getElementById('progressSection').classList.remove('hidden');
686
+ document.getElementById('resultsSection').classList.add('hidden');
687
+ document.getElementById('logBox').innerHTML = '';
688
+
689
+ const steps = [
690
+ [10, 'info', 'Files received. Starting bootstrap…'],
691
+ [30, 'info', `Bootstrapping genes for ${state.model.map(f => f.name.split('_')[0]).join(', ')}…`],
692
+ [55, '', 'Sampling from metabolic subsystems…'],
693
+ [80, '', 'Writing output files…'],
694
+ [95, '', 'Compressing ZIP archive…'],
695
+ [100, 'ok', `Done! ${Number(document.getElementById('batchSlider').value).toLocaleString()} files generated.`],
696
+ ];
697
+
698
+ for (const [pct, cls, msg] of steps) {
699
+ await new Promise(r => setTimeout(r, 700));
700
+ document.getElementById('progressBar').style.width = pct + '%';
701
+ document.getElementById('progressPct').textContent = pct + '%';
702
+ document.getElementById('progressLabel').textContent = msg;
703
+ appendLog(msg, cls);
704
+ }
705
+
706
+ await new Promise(r => setTimeout(r, 400));
707
+ document.getElementById('statFiles').textContent =
708
+ Number(document.getElementById('batchSlider').value).toLocaleString();
709
+ document.getElementById('statSpecies').textContent = state.model.length;
710
+ document.getElementById('resultsSection').classList.remove('hidden');
711
+ document.getElementById('runBtn').disabled = false;
712
+ }
713
+
714
+ function appendLog(text, cls = '') {
715
+ const box = document.getElementById('logBox');
716
+ const span = document.createElement('span');
717
+ if (cls) span.className = 'log-' + cls;
718
+ span.textContent = '› ' + text + '\n';
719
+ box.appendChild(span);
720
+ box.scrollTop = box.scrollHeight;
721
+ }
722
+ </script>
723
+ </body>
724
+ </html>
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ python-multipart==0.0.9
4
+ pandas>=2.0.0
5
+ openpyxl>=3.1.0
6
+ python-libsbml>=5.20.0
7
+ numpy>=1.26.0
8
+ aiofiles==23.2.1
utils/__init__.py ADDED
File without changes
utils/bootstrap_genes.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import ast
3
+ import libsbml
4
+ import os
5
+ import re
6
+ import numpy as np
7
+ import pandas as pd
8
+ import sys
9
+ from utils.utils import *
10
+
11
+ def extract_genes_from_gpa(gpa_str):
12
+ """Extract all gene IDs from a gpaAssociation string (e.g. 'G_A or (G_B and G_C)')."""
13
+ if pd.isna(gpa_str) or str(gpa_str).strip() == "":
14
+ return []
15
+ tokens = re.findall(r'[A-Za-z_]\w*', str(gpa_str))
16
+ keywords = {'or', 'and', 'not'}
17
+ return [t for t in tokens if t.lower() not in keywords]
18
+
19
+ def read_combined_geneExpr(combined_geneExpr_filename):
20
+ if not os.path.isfile(combined_geneExpr_filename):
21
+ raise FileNotFoundError(f"Missing combined normalized count file, expected at {combined_geneExpr_filename}")
22
+ combined_geneExpr_df = pd.read_csv(combined_geneExpr_filename, index_col=0)
23
+ return combined_geneExpr_df
24
+
25
+ def is_real_gene_with_mapping(gene):
26
+ return not gene.startswith(("unmapped", "unknown", "Spontaneous", "Exchange", "Sink", "Diffusion"))
27
+
28
+ def read_system_gene(model_pre_filename):
29
+ rxn_df = read_model_excel(model_pre_filename, "Reactions")
30
+
31
+ # Support 'genes' (stringified-list format) or fall back to 'gpaAssociation' (string format)
32
+ if "genes" in rxn_df.columns:
33
+ gene_col = "genes"
34
+ use_ast = True
35
+ elif "gpaAssociation" in rxn_df.columns:
36
+ gene_col = "gpaAssociation"
37
+ use_ast = False
38
+ else:
39
+ raise ValueError(
40
+ "Reactions sheet must have a 'genes' or 'gpaAssociation' column — "
41
+ f"columns found: {list(rxn_df.columns)}"
42
+ )
43
+
44
+ system_genes_dict = {}
45
+ missing_gene_system_dict = {}
46
+ all_genes_set = set()
47
+ for system, gene_list_raw in zip(rxn_df["system"], rxn_df[gene_col]):
48
+ if use_ast:
49
+ gene_list = ast.literal_eval(gene_list_raw)
50
+ else:
51
+ gene_list = extract_genes_from_gpa(gene_list_raw)
52
+
53
+ if system != "" and system not in system_genes_dict:
54
+ system_genes_dict[system] = set()
55
+
56
+ for gene in gene_list:
57
+ if system != "" and is_real_gene_with_mapping(gene):
58
+ system_genes_dict[system].add(gene)
59
+ if gene.startswith(("unmapped", "unknown")):
60
+ missing_gene_system_dict[gene] = system
61
+ if is_real_gene_with_mapping(gene):
62
+ all_genes_set.add(gene)
63
+
64
+ missing_gene_system_dict = dict(sorted(missing_gene_system_dict.items()))
65
+ return system_genes_dict, missing_gene_system_dict, all_genes_set
66
+
67
+ # def map_genes(mapping_dict, system_genes_dict, all_genes_set):
68
+ # system_genes_dict_new = {}
69
+ # for system, gene_set in system_genes_dict.items():
70
+ # gene_set_new = set()
71
+ # for gene in gene_set:
72
+ # if gene in mapping_dict: # TODO: if not print warning
73
+ # gene_set_new.add(mapping_dict[gene])
74
+ # if gene_set_new:
75
+ # system_genes_dict_new[system] = gene_set_new
76
+
77
+ # all_genes_set_new = set()
78
+ # for gene in all_genes_set:
79
+ # if gene in mapping_dict:
80
+ # all_genes_set_new.add(mapping_dict[gene])
81
+
82
+ # return system_genes_dict_new, all_genes_set_new
83
+
84
+ def filter_geneExpr_df(combined_geneExpr_df, all_genes_set):
85
+ geneExpr_df = combined_geneExpr_df.loc[combined_geneExpr_df.index.isin(all_genes_set)]
86
+ return geneExpr_df
87
+
88
+ def get_system_gene_counts(system_genes_dict, gene_count_dict):
89
+ system_gene_counts_dict = {}
90
+ missing_genes = set()
91
+ for system, gene_set in system_genes_dict.items():
92
+ gene_counts = []
93
+ for gene in sorted(gene_set):
94
+ if gene in gene_count_dict:
95
+ gene_counts.append(gene_count_dict[gene])
96
+ else:
97
+ missing_genes.add(gene)
98
+ system_gene_counts_dict[system] = gene_counts
99
+ if missing_genes:
100
+ print("Warning : These genes do not have normalized count values - {}".format(", ".join(missing_genes)))
101
+ return system_gene_counts_dict
102
+
103
+ def initialize_df(geneExpr_sample, columns):
104
+ new_geneExpr_df_sample = pd.concat([geneExpr_sample.rename(col) for col in columns], axis=1)
105
+ return new_geneExpr_df_sample
106
+
107
+ def bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns):
108
+ rng = np.random.default_rng(seed=0)
109
+ sample_pool_all_genes = [gc for gene_counts in system_gene_counts_dict.values() for gc in gene_counts]
110
+ new_geneExpr_df_sample_missing = pd.DataFrame(columns=columns)
111
+ for missing_gene, system in missing_gene_system_dict.items():
112
+ if system in system_gene_counts_dict:
113
+ sample_pool = system_gene_counts_dict[system]
114
+ new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool, len(columns))
115
+ else:
116
+ new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool_all_genes, len(columns))
117
+ return new_geneExpr_df_sample_missing
118
+
119
+ def filter_by_batch(new_geneExpr_df, start, end, columns):
120
+ target_columns = [f"{col}_{i}" for col in columns for i in range(start, end)]
121
+ new_geneExpr_df_batch_df = new_geneExpr_df[target_columns]
122
+ return new_geneExpr_df_batch_df
123
+
124
+ def bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count=1000):
125
+ print("Read models from", ", ".join(model_pre_filenames))
126
+ print("Read mapping tables from", ", ".join(mapping_filenames))
127
+ print("Species prefixes:", ", ".join(species_prefixes))
128
+ print("Read gene normalized counts from", combined_geneExpr_filename)
129
+ os.makedirs(geneExpr_folder, exist_ok=True)
130
+
131
+ total_to_sample = batch_count
132
+
133
+ # column: sample_name
134
+ # row: real genes for all species
135
+ combined_geneExpr_df = read_combined_geneExpr(combined_geneExpr_filename)
136
+
137
+ # column: A_1, A_2, ..., B_1, B_2, ...
138
+ # row: (real genes with mapping + unmapped real genes + unknown genes) + Exchange + Sink for all species
139
+ new_geneExpr_df = pd.DataFrame()
140
+
141
+ for model_pre_filename, mapping_filename, species in zip(model_pre_filenames, mapping_filenames, species_prefixes): # loop by species
142
+ print(f"Bootstrapping genes for {species}...")
143
+ # mapping_dict: {model_tag in the model file: gene_id in the geneExpr file}
144
+ mapping_dict = read_mapping(mapping_filename)
145
+
146
+ # system_genes_dict: {system: set of real genes with mapping with that system}
147
+ # missing_gene_system_dict: {unmapped real gene & unknown gene: system of the gene}
148
+ # all_genes_set: set of all the real genes with mapping used in the model of that species
149
+ system_genes_dict, missing_gene_system_dict, all_genes_set = read_system_gene(model_pre_filename)
150
+
151
+ # map gene from model to gene in annotation(used in geneExpr file)
152
+ # system_genes_dict, all_genes_set = map_genes(mapping_dict, system_genes_dict, all_genes_set)
153
+
154
+ # filter to get only geneExprs for that species
155
+ geneExpr_df = filter_geneExpr_df(combined_geneExpr_df, all_genes_set)
156
+
157
+ # column: A_1, A_2, ..., B_1, B_2, ...
158
+ # row: (real genes with mapping + unmapped real genes + unknown genes) for one species
159
+ new_geneExpr_df_species = pd.DataFrame()
160
+ for sample in geneExpr_df.columns:
161
+ # gene_count_dict: {gene: gene count}
162
+ gene_count_dict = geneExpr_df[sample].to_dict()
163
+ # system_gene_counts_dict: {system: list of gene counts of the genes with that system}
164
+ system_gene_counts_dict = get_system_gene_counts(system_genes_dict, gene_count_dict)
165
+
166
+ # column: (one sample) A_1, A_2, ..
167
+ columns = [f"{sample}_{i+1}" for i in range(total_to_sample)]
168
+ # row: real genes with mapping (same as geneExpr file)
169
+ new_geneExpr_df_sample = initialize_df(geneExpr_df[sample], columns)
170
+ # row: unmapped real genes + unknown genes
171
+ # (randomly bootstrap from the gene counts of the same species and the same sample and the same system)
172
+ # (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)
173
+ new_geneExpr_df_sample_missing = bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns)
174
+ # concat by rows: real genes with mapping + unmapped real genes + unknown genes
175
+ new_geneExpr_df_sample = pd.concat([new_geneExpr_df_sample, new_geneExpr_df_sample_missing], axis=0)
176
+
177
+ # concat by columns: A_1, A_2, ..., B_1, B_2, ...
178
+ new_geneExpr_df_species = pd.concat([new_geneExpr_df_species, new_geneExpr_df_sample], axis=1)
179
+
180
+ # concat by rows: species_1 + species_2, ...
181
+ new_geneExpr_df = pd.concat([new_geneExpr_df, new_geneExpr_df_species], axis=0)
182
+
183
+ new_geneExpr_df.loc["Spontaneous"] = 0
184
+ new_geneExpr_df.loc["Exchange"] = 0
185
+ new_geneExpr_df.loc["Sink"] = 0
186
+ new_geneExpr_df.loc["Growth"] = 0
187
+ new_geneExpr_df.loc["Diffusion"] = 0
188
+
189
+ # separate into batch to different files
190
+ print("Writing to files...")
191
+ new_geneExpr_df_batch_filename = os.path.join(geneExpr_folder, "geneExpr")
192
+ for i in range(batch_count):
193
+ # first file: A_1, B_1, ..
194
+ # second file: A_2, B_2,
195
+ new_geneExpr_df_batch_df = filter_by_batch(new_geneExpr_df, i+1, (i+1)+1, combined_geneExpr_df.columns)
196
+ new_geneExpr_df_batch_df.to_csv(new_geneExpr_df_batch_filename + f"_{i+1}.csv")
197
+ print(f"Write to {new_geneExpr_df_batch_filename}_<1_{batch_count}>.csv")
198
+
199
+ if __name__ == "__main__":
200
+ # define arguments
201
+ parser = argparse.ArgumentParser()
202
+ parser.add_argument("--model_pre_filenames", nargs="+", required=True, help="List of parsed model files from construct_modularized_community_model tool")
203
+ 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")
204
+ parser.add_argument("--species_prefixes", nargs="+", required=True, help="List of species prefixes for the models, in the same order as input files")
205
+ parser.add_argument("--combined_geneExpr_filename", required=True, help="The gene expression values for all species and for all samples")
206
+ 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")
207
+ parser.add_argument("--batch_count", type=int, default=1000, help="Number of batch to bootstrap, write each batch to a file")
208
+ args = parser.parse_args()
209
+
210
+ # read arguments
211
+ model_pre_filenames = args.model_pre_filenames
212
+ mapping_filenames = args.mapping_filenames
213
+ species_prefixes = args.species_prefixes
214
+ combined_geneExpr_filename = args.combined_geneExpr_filename
215
+ geneExpr_folder = args.geneExpr_folder
216
+ batch_count = args.batch_count
217
+
218
+ bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count)
utils/utils.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import re
4
+ import sys
5
+
6
+ def write_to_excel(output_filename, dfs_towrite, index = False, col_width=22):
7
+ with pd.ExcelWriter(output_filename) as writer:
8
+ for name, df in dfs_towrite.items():
9
+ df.to_excel(writer, sheet_name = name, index = index)
10
+ num_col = len(df.columns) if index else len(df.columns)-1
11
+ writer.sheets[name].set_column(0, num_col, col_width)
12
+ print("Write to", output_filename)
13
+
14
+ def read_model_excel(model_excel_filename, sheet_name):
15
+ if not os.path.isfile(model_excel_filename):
16
+ raise FileNotFoundError(f"Missing model excel file, expected at {model_excel_filename}")
17
+ if sheet_name not in pd.ExcelFile(model_excel_filename).sheet_names:
18
+ raise ValueError(f"{sheet_name} are expected to be in the parsed model excel file - {model_excel_filename}")
19
+ sheet_df = pd.read_excel(model_excel_filename, sheet_name=sheet_name)
20
+ sheet_df = sheet_df.fillna("")
21
+ return sheet_df
22
+
23
+ def read_mapping(mapping_filename):
24
+ if not os.path.isfile(mapping_filename):
25
+ raise FileNotFoundError(f"Missing mapping file, expected at {mapping_filename}")
26
+
27
+ mapping_df = pd.read_excel(mapping_filename)
28
+ mapping_df = mapping_df[mapping_df["has_mapping"] == True]
29
+
30
+ mapping_dict = dict(zip(mapping_df["model_tag"], mapping_df["gene_id"]))
31
+ return mapping_dict