Skip to content

Commit 17c9946

Browse files
committed
created flowrunTEanalysis.py script for TE pipeline
1 parent f47c198 commit 17c9946

4 files changed

Lines changed: 411 additions & 9 deletions

File tree

analysis/flowrunTEanalysis.py

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import logging
4+
import sys
5+
from typing import Dict, List, Tuple
6+
import numpy as np
7+
import re
8+
import requests
9+
10+
FLOWBIO_USERNAME = "username"
11+
FLOWBIO_PASSWORD = "password"
12+
13+
# -------------------------
14+
# CLI Usage - python3 ./flowrunTEanalysis.py --pid ######### --filter sample_name 'STAU2_HepG2.*$' -n #ofbatches --start-batch 1 --end-batch 10
15+
# -------------------------
16+
def parse_args():
17+
p = argparse.ArgumentParser(description="Run Flow.bio TE analysis using flowbio library (fetch + client-side filter by sample name)")
18+
p.add_argument("--pid", "--PID", dest="project_id", required=True,
19+
help="Flow.bio Project ID (string)")
20+
p.add_argument("--filter", nargs=2, metavar=("KEY", "VALUE"), action="append", default=None,
21+
help='Metadata filter. Supported: --filter sample_name "<regex>", --filter experimental_method "<text>"')
22+
p.add_argument("-n", "--num-chunks", type=int, default=1,
23+
help="Split selected samples into N executions using numpy.array_split (default: 1)")
24+
p.add_argument("--start-batch", type=int, default=1,
25+
help="Start execution from batch number (1-based, default: 1)")
26+
p.add_argument("--end-batch", type=int, default=None,
27+
help="End execution at batch number (1-based, default: all batches)")
28+
p.add_argument("--limit", type=str, default=None,
29+
help="Limit samples after filtering. Use 'N' for first N samples, or 'M-N' for range (e.g., '15-37'). Default: no limit")
30+
return p.parse_args()
31+
32+
# -------------------------
33+
# Logging
34+
# -------------------------
35+
def setup_logging():
36+
pass
37+
38+
# -------------------------
39+
# CLIP pipeline settings
40+
# -------------------------
41+
PIPELINE_CLIP = {
42+
"prep_execution_id": "583150835173081055",
43+
"pipeline_id": "860300013917014252",
44+
"pipeline_name": "hanalysis-clipseq",
45+
"pipeline_version": "test",
46+
"nextflow_version": "25.10.4",
47+
}
48+
49+
# -------------------------
50+
# Data parameters (file IDs)
51+
# -------------------------
52+
DATA_PARAMS = {
53+
"gtf": "865333367351887680",
54+
"fasta": "320468299270948664",
55+
"seg_gtf": "657490829677407079",
56+
"fasta_fai": "416915275728809491",
57+
"regions_gtf": "322316217358102943",
58+
"filtered_gtf": "166527636003294230",
59+
"genome_index": "626326653018779728",
60+
"regions_filt_gtf": "607452590642180296",
61+
"ncrna_chrom_sizes": "754069987244417321",
62+
"genome_chrom_sizes": "655637113164580324",
63+
"ncrna_genome_index": "660565400267158951",
64+
"regions_resolved_gtf": "484888182031316412",
65+
"representative_transcript": "166527636003294230",
66+
"representative_transcript_fai": "530453389518046265",
67+
"representative_transcript_gtf": "166527636003294230",
68+
"ncrna_fasta": "472564573697657643",
69+
"ncrna_fasta_fai": "896151603580010259",
70+
"telescope_gtf": "396036320917779302",
71+
"tetranscripts_gtf": "607147647259993520",
72+
}
73+
74+
# -------------------------
75+
# REST helpers for data discovery (prep execution and samples)
76+
# -------------------------
77+
API_BASE = "https://api.flow.bio"
78+
79+
def rest_login(session: requests.Session) -> str:
80+
r = session.post(f"{API_BASE}/login", json={"username": FLOWBIO_USERNAME, "password": FLOWBIO_PASSWORD}, timeout=30)
81+
r.raise_for_status()
82+
return r.json()["token"]
83+
84+
def resolve_pipeline_version_id(session: requests.Session, token: str, pipeline_id: str, version_label: str) -> str:
85+
headers = {"Authorization": f"Bearer {token}"}
86+
r = session.get(f"{API_BASE}/pipelines/{pipeline_id}", headers=headers, timeout=30)
87+
r.raise_for_status()
88+
pipeline = r.json()
89+
return next((v["id"] for v in pipeline.get("versions", []) if v.get("name") == version_label), None)
90+
91+
def fetch_prep_execution(session: requests.Session, token: str, prep_execution_id: str) -> Dict:
92+
headers = {"Authorization": f"Bearer {token}"}
93+
r = session.get(f"{API_BASE}/executions/{prep_execution_id}", headers=headers, timeout=30)
94+
r.raise_for_status()
95+
return r.json()
96+
97+
def fetch_all_project_samples(session: requests.Session, token: str, project_id: str, page_size: int = 100, fetch_details: bool = False) -> List[Dict]:
98+
headers = {"Authorization": f"Bearer {token}"}
99+
page = 1
100+
collected: List[Dict] = []
101+
while True:
102+
r = session.get(
103+
f"{API_BASE}/projects/{project_id}/samples",
104+
params={"page": page, "count": page_size},
105+
headers=headers,
106+
timeout=30,
107+
)
108+
r.raise_for_status()
109+
payload = r.json()
110+
samples = payload.get("samples", [])
111+
if not samples:
112+
break
113+
114+
# If fetch_details is True, get full sample details including metadata
115+
if fetch_details:
116+
for sample in samples:
117+
sample_id = sample.get("id")
118+
if sample_id:
119+
detail_r = session.get(
120+
f"{API_BASE}/samples/{sample_id}",
121+
headers=headers,
122+
timeout=30,
123+
)
124+
detail_r.raise_for_status()
125+
detail_data = detail_r.json()
126+
# Merge detail data into sample
127+
sample.update(detail_data)
128+
129+
collected.extend(samples)
130+
if len(samples) < page_size:
131+
break
132+
page += 1
133+
return collected
134+
135+
# -------------------------
136+
# Limit parsing
137+
# -------------------------
138+
def parse_limit(limit_str: str | None) -> Tuple[int | None, int | None]:
139+
"""
140+
Parse limit string into start and end indices (0-based).
141+
Returns (start, end) where end is exclusive.
142+
Examples:
143+
- "14" -> (0, 14) # first 14 samples
144+
- "15-37" -> (14, 37) # samples 15-37 (1-based becomes 0-based)
145+
"""
146+
if not limit_str:
147+
return None, None
148+
149+
if '-' in limit_str:
150+
# Range format: M-N
151+
try:
152+
start_str, end_str = limit_str.split('-', 1)
153+
start = int(start_str.strip()) - 1 # Convert to 0-based
154+
end = int(end_str.strip()) # Keep as 1-based for end
155+
if start < 0 or end <= start:
156+
raise ValueError("Invalid range: start must be >= 1 and end must be > start")
157+
return start, end
158+
except ValueError as e:
159+
raise SystemExit(f"Invalid range format '{limit_str}': {e}. Use format like '15-37'")
160+
else:
161+
# Single number format: N (first N samples)
162+
try:
163+
count = int(limit_str.strip())
164+
if count <= 0:
165+
raise ValueError("Count must be > 0")
166+
return 0, count
167+
except ValueError as e:
168+
raise SystemExit(f"Invalid limit format '{limit_str}': {e}. Use format like '14' or '15-37'")
169+
170+
# -------------------------
171+
# Client-side filters
172+
# -------------------------
173+
def filter_by_sample_name(samples: List[Dict], regex_expr: str | None) -> List[Dict]:
174+
if not regex_expr:
175+
return samples
176+
try:
177+
pattern = re.compile(regex_expr)
178+
except re.error as e:
179+
raise SystemExit(f"Invalid regex for sample_name: {e}")
180+
matched = [s for s in samples if pattern.search((s.get("name") or ""))]
181+
return matched
182+
183+
def filter_by_experimental_method(samples: List[Dict], search_text: str | None) -> List[Dict]:
184+
"""Filter samples where experimental method field matches search_text (case-insensitive)"""
185+
if not search_text:
186+
return samples
187+
188+
matched = []
189+
found_methods = set() # For debugging
190+
191+
for s in samples:
192+
exp_method = None
193+
metadata = s.get("metadata", {})
194+
if isinstance(metadata, dict):
195+
exp_obj = (
196+
metadata.get("experimentalMethod")
197+
or metadata.get("experimental_method")
198+
or metadata.get("Experimental Method")
199+
)
200+
if isinstance(exp_obj, dict):
201+
exp_method = exp_obj.get("value")
202+
elif isinstance(exp_obj, str):
203+
exp_method = exp_obj
204+
205+
if not exp_method:
206+
exp_method = s.get("experimental_method") or s.get("experimentalMethod") or s.get("Experimental Method")
207+
208+
if exp_method:
209+
found_methods.add(str(exp_method))
210+
if search_text.lower() == str(exp_method).lower():
211+
matched.append(s)
212+
213+
# Debug output
214+
if not matched and found_methods:
215+
print(f"\nDEBUG: Found experimental methods in samples: {sorted(found_methods)}")
216+
print(f"DEBUG: Searching for: '{search_text}'")
217+
elif not found_methods:
218+
print(f"\nDEBUG: No experimental method found in any sample metadata")
219+
print(f"DEBUG: Sample metadata keys (first sample): {list(samples[0].keys()) if samples else 'no samples'}")
220+
if samples and isinstance(samples[0].get("metadata"), dict):
221+
print(f"DEBUG: Metadata keys (first sample): {list(samples[0].get('metadata', {}).keys())}")
222+
223+
return matched
224+
225+
# -------------------------
226+
# Main
227+
# -------------------------
228+
def main():
229+
args = parse_args()
230+
setup_logging()
231+
232+
project_id = args.project_id
233+
234+
# CLIP pipeline settings
235+
prep_execution_id = PIPELINE_CLIP["prep_execution_id"]
236+
pipeline_id = PIPELINE_CLIP["pipeline_id"]
237+
pipeline_name = PIPELINE_CLIP["pipeline_name"]
238+
pipeline_version = PIPELINE_CLIP["pipeline_version"]
239+
nextflow_version = PIPELINE_CLIP["nextflow_version"]
240+
241+
# REST session & auth
242+
session = requests.Session()
243+
token = rest_login(session)
244+
headers = {"Authorization": f"Bearer {token}"}
245+
246+
# Resolve pipeline version ID
247+
version_id = resolve_pipeline_version_id(session, token, pipeline_id, pipeline_version)
248+
249+
# Prep execution & reference files via REST
250+
ex = fetch_prep_execution(session, token, prep_execution_id)
251+
fileset_id = ex["fileset"]["id"]
252+
data_params = DATA_PARAMS.copy()
253+
254+
# Parse --filter to determine if we need full sample details
255+
needs_details = False
256+
if args.filter:
257+
for filter_key, filter_value in args.filter:
258+
if filter_key.lower() == "experimental_method":
259+
needs_details = True # This is in full metadata, need to fetch details
260+
break
261+
262+
# Fetch all samples for the project via REST (with details if needed for filtering)
263+
project_samples = fetch_all_project_samples(session, token, project_id, page_size=100, fetch_details=needs_details)
264+
265+
# Apply filters
266+
selected = project_samples
267+
if args.filter:
268+
for key, value in args.filter:
269+
if key.lower() == "sample_name":
270+
selected = filter_by_sample_name(selected, value)
271+
elif key.lower() == "experimental_method":
272+
selected = filter_by_experimental_method(selected, value)
273+
else:
274+
raise SystemExit(f"Unsupported filter key: {key}. Supported: sample_name, experimental_method")
275+
276+
# Apply limit if specified
277+
if args.limit:
278+
start_idx, end_idx = parse_limit(args.limit)
279+
original_count = len(selected)
280+
281+
if start_idx is not None and end_idx is not None:
282+
# Validate range
283+
if start_idx >= len(selected):
284+
raise SystemExit(f"Start index {start_idx + 1} exceeds available samples ({len(selected)})")
285+
if end_idx > len(selected):
286+
end_idx = len(selected)
287+
288+
selected = selected[start_idx:end_idx]
289+
290+
# Print filtered list
291+
print(f"\nFiltered {len(selected)} samples:")
292+
for s in selected:
293+
print(f" {s['id']}: {s['name']}")
294+
295+
if not selected:
296+
raise SystemExit("No samples selected after applying filter.")
297+
298+
# Split into N execution batches
299+
n_chunks = max(1, int(args.num_chunks))
300+
chunks = [list(chunk) for chunk in np.array_split(np.array(selected, dtype=object), n_chunks)]
301+
302+
# Determine which batches to execute
303+
start_batch = max(1, args.start_batch)
304+
end_batch = args.end_batch if args.end_batch is not None else len(chunks)
305+
end_batch = min(end_batch, len(chunks))
306+
307+
if start_batch > len(chunks):
308+
raise SystemExit(f"Start batch {start_batch} exceeds total batches {len(chunks)}")
309+
310+
# Build and submit one execution per chunk in the specified range
311+
run_urls = []
312+
for i, chunk in enumerate(chunks, start=1):
313+
# Skip batches outside the specified range
314+
if i < start_batch or i > end_batch:
315+
continue
316+
317+
# Build rows in the format expected by the pipeline
318+
rows = [{
319+
"sample": s["id"],
320+
"values": {
321+
}
322+
} for s in chunk]
323+
324+
# Pipeline parameters
325+
params = {
326+
"move_umi_to_header": "false",
327+
#"umi_header_format": "NNNNNNNNNN",
328+
"umi_separator": "rbc:",
329+
"skip_umi_dedupe": "false",
330+
"crosslink_position": "start",
331+
"encode_eclip": "true",
332+
"run_te": "true",
333+
"source": "fastq",
334+
#"star_params": "--outFilterMultimapNmax 100 --outFilterMultimapScoreRange 1 --outSAMattributes All --alignSJoverhangMin 8 --alignSJDBoverhangMin 1 --outFilterType BySJout --alignIntronMin 20 --alignIntronMax 1000000 --outFilterScoreMin 10 --alignEndsType Extend5pOfRead1 --twopassMode Basic --limitOutSJcollapsed 4000000",
335+
}
336+
337+
# Build payload for REST API submission
338+
payload = {
339+
"params": params,
340+
"data_params": data_params,
341+
"csv_params": {"input": {"rows": rows}},
342+
"retries": None,
343+
"nextflow_version": nextflow_version,
344+
"fileset": fileset_id,
345+
"resequence_samples": False,
346+
}
347+
348+
if i == 1:
349+
proceed = input("Submit? (y/n): ").strip().lower()
350+
if proceed != "y":
351+
sys.exit(0)
352+
353+
r = session.post(
354+
f"{API_BASE}/pipelines/versions/{version_id}/run",
355+
headers=headers,
356+
json=payload,
357+
timeout=120,
358+
)
359+
r.raise_for_status()
360+
run_id = r.json()["id"]
361+
362+
url = f"https://app.flow.bio/executions/{run_id}"
363+
run_urls.append(url)
364+
print(f"Batch {i}: {url}")
365+
366+
if __name__ == "__main__":
367+
main()

0 commit comments

Comments
 (0)