@sjcrh/proteinpaint-rust 2.192.0 → 2.193.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +0 -4
- package/package.json +1 -1
- package/src/gdcGRIN2.rs +0 -1261
package/Cargo.toml
CHANGED
package/package.json
CHANGED
package/src/gdcGRIN2.rs
DELETED
|
@@ -1,1261 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
This script can either download cohort maf/cnv files from GDC or read them from local files, with default behavior being to download from GDC. It gracefully handles timeout and other possible errors related to GDC API processing or file reading for use by the client file summary div.
|
|
3
|
-
|
|
4
|
-
Key improvements:
|
|
5
|
-
1. Graceful error handling - individual file failures don't stop the entire process
|
|
6
|
-
2. Better timeout handling with retries
|
|
7
|
-
3. More detailed error reporting
|
|
8
|
-
4. Continues processing even when some files fail
|
|
9
|
-
5. Added chromosome filtering
|
|
10
|
-
6. Supports reading from local files with --from-file flag
|
|
11
|
-
|
|
12
|
-
Command-line arguments:
|
|
13
|
-
- --from-file: Read data from local files instead of downloading from GDC
|
|
14
|
-
|
|
15
|
-
Input JSON:
|
|
16
|
-
caseFiles
|
|
17
|
-
mafOptions: For SNVindel filtering
|
|
18
|
-
cnvOptions: For CNV filtering
|
|
19
|
-
chromosomes: chromosomes will be included:[]
|
|
20
|
-
|
|
21
|
-
Output mutations as JSON array.
|
|
22
|
-
{
|
|
23
|
-
grin2lesion:str,
|
|
24
|
-
summary:{}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
Example of usage:
|
|
28
|
-
echo '{"caseFiles": {"MP2PRT-PATFJE": {"maf": "26ea7b6f-8bc4-4e83-ace1-2125b493a361"},"MP2PRT-PAPIGD": {"maf": "653d7458-f4af-4328-a1ce-3bbf22a2e347"}, "TCGA-CG-4300": { "cnv":"46372ec2-ff79-4d07-b375-9ba8a12c11f3", "maf":"c09b208d-2e7b-4116-9580-27f20f4c7e67"}},"mafOptions": {"minTotalDepth": 10,"minAltAlleleCount": 2,"hyperMutator":8000,"consequences":["missense_variant","frameshift_variant"]}, "cnvOptions":{"lossThreshold":-0.4, "gainThreshold": 0.3, "segLength":2000000, "hyperMutator":500}, "chromosomes":["chr1","chr2","chr3"], "max_record": 100000}' | ./target/release/gdcGRIN2
|
|
29
|
-
Example of usage (read from local files):
|
|
30
|
-
echo '{"caseFiles": {"MP2PRT-PATFJE": {"maf": "26ea7b6f-8bc4-4e83-ace1-2125b493a361"},"MP2PRT-PAPIGD": {"maf": "653d7458-f4af-4328-a1ce-3bbf22a2e347"}, "TCGA-CG-4300": { "cnv":"46372ec2-ff79-4d07-b375-9ba8a12c11f3", "maf":"c09b208d-2e7b-4116-9580-27f20f4c7e67"}},"mafOptions": {"minTotalDepth": 10,"minAltAlleleCount": 2,"hyperMutator":8000,"consequences":["missense_variant","frameshift_variant"]}, "cnvOptions":{"lossThreshold":-0.4, "gainThreshold": 0.3, "segLength":2000000, "hyperMutator":500}, "chromosomes":["chr1","chr2","chr3"], "max_record": 100000}' | ./target/release/gdcGRIN2 --from-file
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
use flate2::read::GzDecoder;
|
|
35
|
-
use futures::StreamExt;
|
|
36
|
-
use memchr::memchr;
|
|
37
|
-
use serde::{Deserialize, Serialize};
|
|
38
|
-
use serde_json;
|
|
39
|
-
use std::collections::{HashMap, HashSet};
|
|
40
|
-
use std::env;
|
|
41
|
-
use std::fs;
|
|
42
|
-
use std::io::{self, Read};
|
|
43
|
-
use std::sync::Arc;
|
|
44
|
-
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
45
|
-
use std::time::Duration;
|
|
46
|
-
use tokio::io::{AsyncReadExt, BufReader};
|
|
47
|
-
use tokio::sync::Mutex;
|
|
48
|
-
use tokio::time::timeout;
|
|
49
|
-
|
|
50
|
-
// Struct to hold error information for JSON output
|
|
51
|
-
#[derive(serde::Serialize, Clone)]
|
|
52
|
-
struct ErrorEntry {
|
|
53
|
-
case_id: String,
|
|
54
|
-
data_type: String,
|
|
55
|
-
error_type: String,
|
|
56
|
-
error_details: String,
|
|
57
|
-
attempts_made: u32,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Define the structure for datadd
|
|
61
|
-
#[derive(Deserialize, Debug)]
|
|
62
|
-
struct DataType {
|
|
63
|
-
cnv: Option<String>,
|
|
64
|
-
maf: Option<String>,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Define the structure for mafOptions
|
|
68
|
-
#[derive(Deserialize, Debug)]
|
|
69
|
-
struct MafOptions {
|
|
70
|
-
#[serde(rename = "minTotalDepth")]
|
|
71
|
-
min_total_depth: i32,
|
|
72
|
-
#[serde(rename = "minAltAlleleCount")]
|
|
73
|
-
min_alt_allele_count: i32,
|
|
74
|
-
#[serde(rename = "hyperMutator")]
|
|
75
|
-
hyper_mutator: i32,
|
|
76
|
-
consequences: Option<Vec<String>>, // Optional list of consequences to filter MAF files
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Define the structure for cnvOptions
|
|
80
|
-
#[derive(Deserialize, Debug)]
|
|
81
|
-
struct CnvOptions {
|
|
82
|
-
#[serde(rename = "lossThreshold")]
|
|
83
|
-
loss_threshold: f32,
|
|
84
|
-
#[serde(rename = "gainThreshold")]
|
|
85
|
-
gain_threshold: f32,
|
|
86
|
-
#[serde(rename = "segLength")]
|
|
87
|
-
seg_length: i32,
|
|
88
|
-
#[serde(rename = "hyperMutator")]
|
|
89
|
-
hyper_mutator: i32,
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// struct for MAF filter details
|
|
93
|
-
#[derive(Clone, Serialize, Default)]
|
|
94
|
-
struct FilteredMafDetails {
|
|
95
|
-
matched_consequences: HashMap<String, usize>,
|
|
96
|
-
rejected_consequences: HashMap<String, usize>,
|
|
97
|
-
t_alt_count: usize,
|
|
98
|
-
t_depth: usize,
|
|
99
|
-
invalid_rows: usize,
|
|
100
|
-
excluded_by_min_depth: usize,
|
|
101
|
-
excluded_by_min_alt_count: usize,
|
|
102
|
-
excluded_by_consequence_type: usize,
|
|
103
|
-
total_processed: usize,
|
|
104
|
-
total_included: usize,
|
|
105
|
-
skipped_chromosomes: HashMap<String, usize>,
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// struct for CNV filter details
|
|
109
|
-
#[derive(Clone, Serialize, Default)]
|
|
110
|
-
struct FilteredCnvDetails {
|
|
111
|
-
segment_mean: usize,
|
|
112
|
-
seg_length: usize,
|
|
113
|
-
invalid_rows: usize,
|
|
114
|
-
excluded_by_loss_threshold: usize,
|
|
115
|
-
excluded_by_gain_threshold: usize,
|
|
116
|
-
excluded_by_segment_length: usize,
|
|
117
|
-
total_processed: usize,
|
|
118
|
-
total_included: usize,
|
|
119
|
-
skipped_chromosomes: HashMap<String, usize>,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// struct for per-case filter details
|
|
123
|
-
#[derive(Clone, Serialize)]
|
|
124
|
-
struct FilteredCaseDetails {
|
|
125
|
-
maf: FilteredMafDetails,
|
|
126
|
-
cnv: FilteredCnvDetails,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Final summary output (JSONL format)
|
|
130
|
-
#[derive(serde::Serialize)]
|
|
131
|
-
struct FinalSummary {
|
|
132
|
-
total_files: usize,
|
|
133
|
-
successful_files: usize,
|
|
134
|
-
failed_files: usize,
|
|
135
|
-
errors: Vec<ErrorEntry>,
|
|
136
|
-
filtered_records: usize,
|
|
137
|
-
filtered_maf_records: usize,
|
|
138
|
-
filtered_cnv_records: usize,
|
|
139
|
-
included_maf_records: usize,
|
|
140
|
-
included_cnv_records: usize,
|
|
141
|
-
filtered_records_by_case: HashMap<String, FilteredCaseDetails>,
|
|
142
|
-
hyper_mutator_records: HashMap<String, Vec<String>>,
|
|
143
|
-
excluded_by_max_record: HashMap<String, Vec<String>>,
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Enum to hold both SuccessfulFileoutput and FinalSummary
|
|
147
|
-
#[derive(Serialize)]
|
|
148
|
-
struct Output {
|
|
149
|
-
grin2lesion: String,
|
|
150
|
-
summary: FinalSummary,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Define the top-level input structure
|
|
154
|
-
#[derive(Deserialize, Debug)]
|
|
155
|
-
struct InputData {
|
|
156
|
-
#[serde(rename = "caseFiles")]
|
|
157
|
-
case_files: HashMap<String, DataType>,
|
|
158
|
-
#[serde(rename = "mafOptions")]
|
|
159
|
-
maf_options: Option<MafOptions>,
|
|
160
|
-
#[serde(rename = "cnvOptions")]
|
|
161
|
-
cnv_options: Option<CnvOptions>,
|
|
162
|
-
chromosomes: Vec<String>,
|
|
163
|
-
max_record: usize,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Configuration for different data types
|
|
167
|
-
#[derive(Deserialize, Debug)]
|
|
168
|
-
struct DataTypeConfig {
|
|
169
|
-
header_marker: &'static str,
|
|
170
|
-
output_columns: Vec<&'static str>,
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Function to parse TSV content
|
|
174
|
-
async fn parse_content(
|
|
175
|
-
content: &str,
|
|
176
|
-
case_id: &str,
|
|
177
|
-
data_type: &str,
|
|
178
|
-
min_total_depth: i32,
|
|
179
|
-
min_alt_allele_count: i32,
|
|
180
|
-
maf_hyper_mutator: i32,
|
|
181
|
-
consequences: &Option<Vec<String>>,
|
|
182
|
-
gain_threshold: f32,
|
|
183
|
-
loss_threshold: f32,
|
|
184
|
-
seg_length: i32,
|
|
185
|
-
cnv_hyper_mutator: i32,
|
|
186
|
-
chromosomes: &HashSet<String>,
|
|
187
|
-
filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
|
|
188
|
-
filtered_maf_records: &AtomicUsize,
|
|
189
|
-
filtered_cnv_records: &AtomicUsize,
|
|
190
|
-
included_maf_records: &AtomicUsize,
|
|
191
|
-
included_cnv_records: &AtomicUsize,
|
|
192
|
-
hyper_mutator_records: &Arc<Mutex<HashMap<String, Vec<String>>>>,
|
|
193
|
-
) -> Result<Vec<Vec<String>>, (String, String, String)> {
|
|
194
|
-
let config = match data_type {
|
|
195
|
-
"cnv" => DataTypeConfig {
|
|
196
|
-
header_marker: "Segment_Mean",
|
|
197
|
-
output_columns: vec!["Chromosome", "Start", "End", "Segment_Mean"],
|
|
198
|
-
},
|
|
199
|
-
"maf" => DataTypeConfig {
|
|
200
|
-
header_marker: "Hugo_Symbol",
|
|
201
|
-
output_columns: vec!["Chromosome", "Start_Position", "End_Position", "t_depth", "t_alt_count"],
|
|
202
|
-
},
|
|
203
|
-
_ => {
|
|
204
|
-
return Err((
|
|
205
|
-
case_id.to_string(),
|
|
206
|
-
data_type.to_string(),
|
|
207
|
-
"Invalid data type".to_string(),
|
|
208
|
-
));
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// check hyperMutator for MAF and CNV files
|
|
213
|
-
let hyper_mutator = if data_type == "maf" {
|
|
214
|
-
maf_hyper_mutator
|
|
215
|
-
} else {
|
|
216
|
-
cnv_hyper_mutator
|
|
217
|
-
};
|
|
218
|
-
if hyper_mutator > 0 {
|
|
219
|
-
let line_count = content.lines().count();
|
|
220
|
-
if line_count as i32 > hyper_mutator {
|
|
221
|
-
let mut hyper_records = hyper_mutator_records.lock().await;
|
|
222
|
-
hyper_records
|
|
223
|
-
.entry(data_type.to_string())
|
|
224
|
-
.or_insert_with(Vec::new)
|
|
225
|
-
.push(case_id.to_string());
|
|
226
|
-
if data_type == "maf" {
|
|
227
|
-
filtered_maf_records.fetch_add(line_count, Ordering::Relaxed);
|
|
228
|
-
} else if data_type == "cnv" {
|
|
229
|
-
filtered_cnv_records.fetch_add(line_count, Ordering::Relaxed);
|
|
230
|
-
}
|
|
231
|
-
return Ok(Vec::new());
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
let lines = content.lines();
|
|
236
|
-
let mut parsed_data = Vec::new();
|
|
237
|
-
let mut columns_indices: Vec<usize> = Vec::new();
|
|
238
|
-
let mut variant_classification_index: Option<usize> = None;
|
|
239
|
-
let mut header: Vec<String> = Vec::new();
|
|
240
|
-
|
|
241
|
-
for line in lines {
|
|
242
|
-
if line.starts_with("#") {
|
|
243
|
-
continue;
|
|
244
|
-
};
|
|
245
|
-
if line.contains(config.header_marker) {
|
|
246
|
-
header = line.split("\t").map(|s| s.to_string()).collect();
|
|
247
|
-
if let Err(err) = setup_columns(
|
|
248
|
-
&header,
|
|
249
|
-
&config,
|
|
250
|
-
&mut columns_indices,
|
|
251
|
-
&mut variant_classification_index,
|
|
252
|
-
case_id,
|
|
253
|
-
data_type,
|
|
254
|
-
) {
|
|
255
|
-
return Err(err);
|
|
256
|
-
}
|
|
257
|
-
continue;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
let row = match data_type {
|
|
261
|
-
"maf" => {
|
|
262
|
-
process_mafline(
|
|
263
|
-
line,
|
|
264
|
-
case_id,
|
|
265
|
-
data_type,
|
|
266
|
-
&columns_indices,
|
|
267
|
-
variant_classification_index,
|
|
268
|
-
consequences,
|
|
269
|
-
min_total_depth,
|
|
270
|
-
min_alt_allele_count,
|
|
271
|
-
chromosomes,
|
|
272
|
-
filtered_records,
|
|
273
|
-
filtered_maf_records,
|
|
274
|
-
included_maf_records,
|
|
275
|
-
)
|
|
276
|
-
.await
|
|
277
|
-
}
|
|
278
|
-
"cnv" => {
|
|
279
|
-
process_cnvline(
|
|
280
|
-
line,
|
|
281
|
-
case_id,
|
|
282
|
-
data_type,
|
|
283
|
-
&header,
|
|
284
|
-
&columns_indices,
|
|
285
|
-
gain_threshold,
|
|
286
|
-
loss_threshold,
|
|
287
|
-
seg_length,
|
|
288
|
-
chromosomes,
|
|
289
|
-
filtered_records,
|
|
290
|
-
filtered_cnv_records,
|
|
291
|
-
included_cnv_records,
|
|
292
|
-
)
|
|
293
|
-
.await
|
|
294
|
-
}
|
|
295
|
-
_ => {
|
|
296
|
-
return Err((
|
|
297
|
-
case_id.to_string(),
|
|
298
|
-
data_type.to_string(),
|
|
299
|
-
"Invalid data type".to_string(),
|
|
300
|
-
));
|
|
301
|
-
}
|
|
302
|
-
}?;
|
|
303
|
-
|
|
304
|
-
if let Some(out_lst) = row {
|
|
305
|
-
parsed_data.push(out_lst);
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if columns_indices.is_empty() {
|
|
310
|
-
return Err((
|
|
311
|
-
case_id.to_string(),
|
|
312
|
-
data_type.to_string(),
|
|
313
|
-
"No matching columns found. Problematic file!".to_string(),
|
|
314
|
-
));
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
Ok(parsed_data)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Set up column indices for processing
|
|
321
|
-
fn setup_columns(
|
|
322
|
-
header: &[String],
|
|
323
|
-
config: &DataTypeConfig,
|
|
324
|
-
columns_indices: &mut Vec<usize>,
|
|
325
|
-
variant_classification_index: &mut Option<usize>,
|
|
326
|
-
case_id: &str,
|
|
327
|
-
data_type: &str,
|
|
328
|
-
) -> Result<(), (String, String, String)> {
|
|
329
|
-
for col in &config.output_columns {
|
|
330
|
-
match header.iter().position(|x| x == col) {
|
|
331
|
-
Some(index) => columns_indices.push(index),
|
|
332
|
-
None => {
|
|
333
|
-
return Err((
|
|
334
|
-
case_id.to_string(),
|
|
335
|
-
data_type.to_string(),
|
|
336
|
-
format!("Column {} was not found", col),
|
|
337
|
-
));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if data_type == "maf" {
|
|
343
|
-
*variant_classification_index = header.iter().position(|x| x == "One_Consequence");
|
|
344
|
-
if variant_classification_index.is_none() {
|
|
345
|
-
return Err((
|
|
346
|
-
case_id.to_string(),
|
|
347
|
-
data_type.to_string(),
|
|
348
|
-
"Column Variant_Classification was not found".to_string(),
|
|
349
|
-
));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
Ok(())
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Process a single row of MAF file
|
|
357
|
-
async fn process_mafline(
|
|
358
|
-
line: &str,
|
|
359
|
-
case_id: &str,
|
|
360
|
-
data_type: &str,
|
|
361
|
-
columns_indices: &[usize],
|
|
362
|
-
variant_classification_index: Option<usize>,
|
|
363
|
-
consequences: &Option<Vec<String>>,
|
|
364
|
-
min_total_depth: i32,
|
|
365
|
-
min_alt_allele_count: i32,
|
|
366
|
-
chromosomes: &HashSet<String>,
|
|
367
|
-
filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
|
|
368
|
-
filtered_maf_records: &AtomicUsize,
|
|
369
|
-
included_maf_records: &AtomicUsize,
|
|
370
|
-
) -> Result<Option<Vec<String>>, (String, String, String)> {
|
|
371
|
-
let cont_lst: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
|
|
372
|
-
let mut out_lst = vec![case_id.to_string()];
|
|
373
|
-
|
|
374
|
-
// Initialize or update case details
|
|
375
|
-
let mut filtered_map = filtered_records.lock().await;
|
|
376
|
-
filtered_map
|
|
377
|
-
.entry(case_id.to_string())
|
|
378
|
-
.or_insert_with(|| FilteredCaseDetails {
|
|
379
|
-
maf: FilteredMafDetails::default(),
|
|
380
|
-
cnv: FilteredCnvDetails::default(),
|
|
381
|
-
});
|
|
382
|
-
let case_details = filtered_map.get_mut(case_id).unwrap();
|
|
383
|
-
|
|
384
|
-
// Track total processed records
|
|
385
|
-
case_details.maf.total_processed += 1;
|
|
386
|
-
|
|
387
|
-
// Handle consequence filtering and counting for MAF files
|
|
388
|
-
|
|
389
|
-
if let Some(var_class_idx) = variant_classification_index {
|
|
390
|
-
if var_class_idx < cont_lst.len() {
|
|
391
|
-
let variant_classification = &cont_lst[var_class_idx];
|
|
392
|
-
if let Some(consequence_filter) = consequences {
|
|
393
|
-
if !consequence_filter.is_empty() {
|
|
394
|
-
if consequence_filter.contains(variant_classification) {
|
|
395
|
-
// Matched consequence
|
|
396
|
-
*case_details
|
|
397
|
-
.maf
|
|
398
|
-
.matched_consequences
|
|
399
|
-
.entry(variant_classification.to_string())
|
|
400
|
-
.or_insert(0) += 1;
|
|
401
|
-
} else {
|
|
402
|
-
// Unmatched consequence
|
|
403
|
-
*case_details
|
|
404
|
-
.maf
|
|
405
|
-
.rejected_consequences
|
|
406
|
-
.entry(variant_classification.to_string())
|
|
407
|
-
.or_insert(0) += 1;
|
|
408
|
-
case_details.maf.excluded_by_consequence_type += 1;
|
|
409
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
410
|
-
return Ok(None);
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
// Empty filter, count as matched
|
|
414
|
-
*case_details
|
|
415
|
-
.maf
|
|
416
|
-
.matched_consequences
|
|
417
|
-
.entry(variant_classification.to_string())
|
|
418
|
-
.or_insert(0) += 1;
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
// No filter, count as matched
|
|
422
|
-
*case_details
|
|
423
|
-
.maf
|
|
424
|
-
.matched_consequences
|
|
425
|
-
.entry(variant_classification.to_string())
|
|
426
|
-
.or_insert(0) += 1;
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
case_details.maf.invalid_rows += 1;
|
|
430
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
431
|
-
return Ok(None);
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
case_details.maf.invalid_rows += 1;
|
|
435
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
436
|
-
return Ok(None);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Extract relevant columns
|
|
440
|
-
for &x in columns_indices {
|
|
441
|
-
if x >= cont_lst.len() {
|
|
442
|
-
case_details.maf.invalid_rows += 1;
|
|
443
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
444
|
-
return Ok(None); // Invalid row
|
|
445
|
-
}
|
|
446
|
-
let element = cont_lst[x].to_string();
|
|
447
|
-
out_lst.push(element);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Additional MAF-specific processing
|
|
451
|
-
if out_lst.len() < 6 {
|
|
452
|
-
case_details.maf.invalid_rows += 1;
|
|
453
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
454
|
-
return Ok(None); // Not enough columns
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
let alle_depth = out_lst[4].parse::<i32>().map_err(|_| {
|
|
458
|
-
case_details.maf.invalid_rows += 1;
|
|
459
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
460
|
-
(
|
|
461
|
-
case_id.to_string(),
|
|
462
|
-
data_type.to_string(),
|
|
463
|
-
"Failed to convert t_depth to integer.".to_string(),
|
|
464
|
-
)
|
|
465
|
-
})?;
|
|
466
|
-
|
|
467
|
-
let alt_count = out_lst[5].parse::<i32>().map_err(|_| {
|
|
468
|
-
case_details.maf.invalid_rows += 1;
|
|
469
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
470
|
-
(
|
|
471
|
-
case_id.to_string(),
|
|
472
|
-
data_type.to_string(),
|
|
473
|
-
"Failed to convert t_alt_count to integer.".to_string(),
|
|
474
|
-
)
|
|
475
|
-
})?;
|
|
476
|
-
|
|
477
|
-
if alle_depth < min_total_depth {
|
|
478
|
-
case_details.maf.t_depth += 1;
|
|
479
|
-
case_details.maf.excluded_by_min_depth += 1;
|
|
480
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
481
|
-
return Ok(None);
|
|
482
|
-
}
|
|
483
|
-
if alt_count < min_alt_allele_count {
|
|
484
|
-
case_details.maf.t_alt_count += 1;
|
|
485
|
-
case_details.maf.excluded_by_min_alt_count += 1;
|
|
486
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
487
|
-
return Ok(None);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Keep case_id, chr, start, end, and add "mutation"
|
|
491
|
-
out_lst = out_lst[0..4].to_vec();
|
|
492
|
-
out_lst.push("mutation".to_string());
|
|
493
|
-
|
|
494
|
-
// adding 'chr' to chromosome if it is not start with 'chr'
|
|
495
|
-
if !out_lst[1].starts_with("chr") {
|
|
496
|
-
out_lst[1] = format!("chr{}", out_lst[1]);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Chromosome filtering
|
|
500
|
-
if !chromosomes.is_empty() && !chromosomes.contains(&out_lst[1]) {
|
|
501
|
-
*case_details
|
|
502
|
-
.maf
|
|
503
|
-
.skipped_chromosomes
|
|
504
|
-
.entry(out_lst[1].clone())
|
|
505
|
-
.or_insert(0) += 1;
|
|
506
|
-
filtered_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
507
|
-
return Ok(None);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Update counters for included MAF records
|
|
511
|
-
case_details.maf.total_included += 1;
|
|
512
|
-
included_maf_records.fetch_add(1, Ordering::Relaxed);
|
|
513
|
-
|
|
514
|
-
Ok(Some(out_lst))
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Process a single row of CNV file
|
|
518
|
-
async fn process_cnvline(
|
|
519
|
-
line: &str,
|
|
520
|
-
case_id: &str,
|
|
521
|
-
data_type: &str,
|
|
522
|
-
header: &[String],
|
|
523
|
-
columns_indices: &[usize],
|
|
524
|
-
gain_threshold: f32,
|
|
525
|
-
loss_threshold: f32,
|
|
526
|
-
seg_length: i32,
|
|
527
|
-
chromosomes: &HashSet<String>,
|
|
528
|
-
filtered_records: &Arc<Mutex<HashMap<String, FilteredCaseDetails>>>,
|
|
529
|
-
filtered_cnv_records: &AtomicUsize,
|
|
530
|
-
included_cnv_records: &AtomicUsize,
|
|
531
|
-
) -> Result<Option<Vec<String>>, (String, String, String)> {
|
|
532
|
-
let cont_lst: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
|
|
533
|
-
let mut out_lst = vec![case_id.to_string()];
|
|
534
|
-
|
|
535
|
-
// Initialize or update case details
|
|
536
|
-
let mut filtered_map = filtered_records.lock().await;
|
|
537
|
-
filtered_map
|
|
538
|
-
.entry(case_id.to_string())
|
|
539
|
-
.or_insert_with(|| FilteredCaseDetails {
|
|
540
|
-
maf: FilteredMafDetails::default(),
|
|
541
|
-
cnv: FilteredCnvDetails::default(),
|
|
542
|
-
});
|
|
543
|
-
let case_details = filtered_map.get_mut(case_id).unwrap();
|
|
544
|
-
|
|
545
|
-
// Track total processed records
|
|
546
|
-
case_details.cnv.total_processed += 1;
|
|
547
|
-
|
|
548
|
-
// Extract relevant columns
|
|
549
|
-
for &x in columns_indices {
|
|
550
|
-
if x >= cont_lst.len() {
|
|
551
|
-
case_details.cnv.invalid_rows += 1;
|
|
552
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
553
|
-
return Ok(None); // Invalid row
|
|
554
|
-
}
|
|
555
|
-
let mut element = cont_lst[x].to_string();
|
|
556
|
-
if header[x] == "Segment_Mean" {
|
|
557
|
-
element = process_segment_mean(&element, case_id, data_type, gain_threshold, loss_threshold)?;
|
|
558
|
-
if element.is_empty() {
|
|
559
|
-
case_details.cnv.segment_mean += 1;
|
|
560
|
-
let seg_mean = cont_lst[x].parse::<f32>().unwrap_or(0.0);
|
|
561
|
-
if seg_mean > loss_threshold && seg_mean < gain_threshold {
|
|
562
|
-
// Between thresholds - not a significant gain or loss
|
|
563
|
-
if seg_mean >= 0.0 {
|
|
564
|
-
case_details.cnv.excluded_by_gain_threshold += 1;
|
|
565
|
-
} else {
|
|
566
|
-
case_details.cnv.excluded_by_loss_threshold += 1;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
570
|
-
return Ok(None);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
out_lst.push(element);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// filter cnvs based on segment length. Default: 0 (no filtering)
|
|
577
|
-
// calculate segment length (End_Position - Start_Position)
|
|
578
|
-
let end_position = out_lst[3].parse::<i32>().map_err(|_| {
|
|
579
|
-
case_details.cnv.invalid_rows += 1;
|
|
580
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
581
|
-
(
|
|
582
|
-
case_id.to_string(),
|
|
583
|
-
data_type.to_string(),
|
|
584
|
-
"Failed to convert End Position of cnv to integer.".to_string(),
|
|
585
|
-
)
|
|
586
|
-
})?;
|
|
587
|
-
|
|
588
|
-
let start_position = out_lst[2].parse::<i32>().map_err(|_| {
|
|
589
|
-
case_details.cnv.invalid_rows += 1;
|
|
590
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
591
|
-
(
|
|
592
|
-
case_id.to_string(),
|
|
593
|
-
data_type.to_string(),
|
|
594
|
-
"Failed to convert Start Position of cnv to integer.".to_string(),
|
|
595
|
-
)
|
|
596
|
-
})?;
|
|
597
|
-
let cnv_length = end_position - start_position;
|
|
598
|
-
if seg_length > 0 && cnv_length > seg_length {
|
|
599
|
-
case_details.cnv.seg_length += 1;
|
|
600
|
-
case_details.cnv.excluded_by_segment_length += 1;
|
|
601
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
602
|
-
return Ok(None);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// adding 'chr' to chromosome if it is not start with 'chr'
|
|
606
|
-
if !out_lst[1].starts_with("chr") {
|
|
607
|
-
out_lst[1] = format!("chr{}", out_lst[1]);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Chromosome filtering
|
|
611
|
-
if !chromosomes.is_empty() && !chromosomes.contains(&out_lst[1]) {
|
|
612
|
-
*case_details
|
|
613
|
-
.cnv
|
|
614
|
-
.skipped_chromosomes
|
|
615
|
-
.entry(out_lst[1].clone())
|
|
616
|
-
.or_insert(0) += 1;
|
|
617
|
-
filtered_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
618
|
-
return Ok(None);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Update counters for included MAF records
|
|
622
|
-
case_details.cnv.total_included += 1;
|
|
623
|
-
included_cnv_records.fetch_add(1, Ordering::Relaxed);
|
|
624
|
-
|
|
625
|
-
Ok(Some(out_lst))
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Process Segment_Mean for CNV files
|
|
629
|
-
fn process_segment_mean(
|
|
630
|
-
element: &str,
|
|
631
|
-
case_id: &str,
|
|
632
|
-
data_type: &str,
|
|
633
|
-
gain_threshold: f32,
|
|
634
|
-
loss_threshold: f32,
|
|
635
|
-
) -> Result<String, (String, String, String)> {
|
|
636
|
-
let seg_mean = element.parse::<f32>().map_err(|_| {
|
|
637
|
-
(
|
|
638
|
-
case_id.to_string(),
|
|
639
|
-
data_type.to_string(),
|
|
640
|
-
"Segment_Mean in cnv file is not float".to_string(),
|
|
641
|
-
)
|
|
642
|
-
})?;
|
|
643
|
-
|
|
644
|
-
if seg_mean >= gain_threshold {
|
|
645
|
-
Ok("gain".to_string())
|
|
646
|
-
} else if seg_mean <= loss_threshold {
|
|
647
|
-
Ok("loss".to_string())
|
|
648
|
-
} else {
|
|
649
|
-
Ok(String::new())
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/// Updated helper function to normalize MAF consequence types to frontend format
|
|
654
|
-
/// Downloads a single file with minimal retry logic for transient failures
|
|
655
|
-
async fn download_single_file(
|
|
656
|
-
case_id: String,
|
|
657
|
-
data_type: String,
|
|
658
|
-
url: String,
|
|
659
|
-
max_attempts: u32,
|
|
660
|
-
) -> Result<(String, String, String), (String, String, String, u32)> {
|
|
661
|
-
let mut last_error = String::new();
|
|
662
|
-
let mut error_type = String::new();
|
|
663
|
-
|
|
664
|
-
for attempt in 0..max_attempts {
|
|
665
|
-
// Build HTTP client with aggressive timeouts for real-time processing
|
|
666
|
-
let client = match reqwest::Client::builder()
|
|
667
|
-
.timeout(Duration::from_secs(10)) // 10 second timeout per request
|
|
668
|
-
.connect_timeout(Duration::from_secs(3)) // 3 second connect timeout
|
|
669
|
-
.build()
|
|
670
|
-
{
|
|
671
|
-
Ok(client) => client,
|
|
672
|
-
Err(e) => {
|
|
673
|
-
last_error = format!("Client build error: {}", e);
|
|
674
|
-
error_type = "client_build_error".to_string();
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
// Attempt download with tight timeout - fail fast if server is slow
|
|
680
|
-
match timeout(Duration::from_secs(12), client.get(&url).send()).await {
|
|
681
|
-
Ok(Ok(resp)) if resp.status().is_success() => {
|
|
682
|
-
match resp.bytes().await {
|
|
683
|
-
Ok(content) => {
|
|
684
|
-
// Handle both compressed and uncompressed content
|
|
685
|
-
let text = if memchr(0x00, &content).is_some() {
|
|
686
|
-
// Likely compressed (gzipped) content
|
|
687
|
-
let mut decoder = GzDecoder::new(&content[..]);
|
|
688
|
-
let mut decompressed_content = Vec::new();
|
|
689
|
-
match decoder.read_to_end(&mut decompressed_content) {
|
|
690
|
-
Ok(_) => String::from_utf8_lossy(&decompressed_content).to_string(),
|
|
691
|
-
Err(e) => {
|
|
692
|
-
last_error = format!("Decompression failed: {}", e);
|
|
693
|
-
error_type = "decompression_error".to_string();
|
|
694
|
-
continue; // Retry on decompression failure
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
} else {
|
|
698
|
-
// Plain text content
|
|
699
|
-
String::from_utf8_lossy(&content).to_string()
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
// Success! Return immediately
|
|
703
|
-
return Ok((case_id, data_type, text));
|
|
704
|
-
}
|
|
705
|
-
Err(e) => {
|
|
706
|
-
last_error = format!("Failed to read response bytes: {}", e);
|
|
707
|
-
error_type = "connection_error".to_string();
|
|
708
|
-
// This could be "connection closed before message completed"
|
|
709
|
-
// Worth retrying for transient network issues
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
Ok(Ok(resp)) => {
|
|
714
|
-
last_error = format!(
|
|
715
|
-
"HTTP error {}: {}",
|
|
716
|
-
resp.status(),
|
|
717
|
-
resp.status().canonical_reason().unwrap_or("Unknown")
|
|
718
|
-
);
|
|
719
|
-
error_type = if resp.status().is_client_error() {
|
|
720
|
-
"client_error".to_string()
|
|
721
|
-
} else {
|
|
722
|
-
"server_error".to_string()
|
|
723
|
-
};
|
|
724
|
-
// Don't retry 4xx errors (client errors), but retry 5xx (server errors)
|
|
725
|
-
if resp.status().is_client_error() {
|
|
726
|
-
break; // No point retrying client errors
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
Ok(Err(e)) => {
|
|
730
|
-
last_error = format!("Request error: {}", e);
|
|
731
|
-
error_type = "network_error".to_string();
|
|
732
|
-
// Network errors are worth retrying
|
|
733
|
-
}
|
|
734
|
-
Err(_) => {
|
|
735
|
-
last_error = "Request timeout (12s) - server too slow".to_string();
|
|
736
|
-
error_type = "timeout_error".to_string();
|
|
737
|
-
// Timeouts might be transient, worth a quick retry
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// If this isn't the last attempt, wait briefly before retrying
|
|
742
|
-
if attempt < max_attempts - 1 {
|
|
743
|
-
// Silent retry - no stderr noise
|
|
744
|
-
tokio::time::sleep(Duration::from_secs(1)).await; // 1 second between retries
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
Err((
|
|
749
|
-
case_id,
|
|
750
|
-
data_type,
|
|
751
|
-
format!("{}: {}", error_type, last_error),
|
|
752
|
-
max_attempts,
|
|
753
|
-
))
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/// Downloading from GDC
|
|
757
|
-
/// Outputs JSONL format: one JSON object per line
|
|
758
|
-
async fn download_data(
|
|
759
|
-
data4dl: HashMap<String, DataType>,
|
|
760
|
-
host: &str,
|
|
761
|
-
min_total_depth: i32,
|
|
762
|
-
min_alt_allele_count: i32,
|
|
763
|
-
maf_hyper_mutator: i32,
|
|
764
|
-
consequences: &Option<Vec<String>>,
|
|
765
|
-
gain_threshold: f32,
|
|
766
|
-
loss_threshold: f32,
|
|
767
|
-
seg_length: i32,
|
|
768
|
-
cnv_hyper_mutator: i32,
|
|
769
|
-
chromosomes: &HashSet<String>,
|
|
770
|
-
max_record: usize,
|
|
771
|
-
) {
|
|
772
|
-
let data_urls: Vec<(String, String, String)> = data4dl
|
|
773
|
-
.into_iter()
|
|
774
|
-
.flat_map(|(case_id, data_types)| {
|
|
775
|
-
let mut urls = Vec::new();
|
|
776
|
-
if let Some(cnv_uuid) = &data_types.cnv {
|
|
777
|
-
urls.push((case_id.clone(), "cnv".to_string(), format!("{}{}", host, cnv_uuid)));
|
|
778
|
-
}
|
|
779
|
-
if let Some(maf_uuid) = &data_types.maf {
|
|
780
|
-
urls.push((case_id.clone(), "maf".to_string(), format!("{}{}", host, maf_uuid)));
|
|
781
|
-
}
|
|
782
|
-
urls
|
|
783
|
-
})
|
|
784
|
-
.collect();
|
|
785
|
-
|
|
786
|
-
let total_files = data_urls.len();
|
|
787
|
-
|
|
788
|
-
// Counters for final summary
|
|
789
|
-
let successful_downloads = Arc::new(AtomicUsize::new(0));
|
|
790
|
-
let failed_downloads = Arc::new(AtomicUsize::new(0));
|
|
791
|
-
let filtered_maf_records = Arc::new(AtomicUsize::new(0));
|
|
792
|
-
let filtered_cnv_records = Arc::new(AtomicUsize::new(0));
|
|
793
|
-
let filtered_records = Arc::new(Mutex::new(HashMap::<String, FilteredCaseDetails>::new()));
|
|
794
|
-
let hyper_mutator_records = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
|
|
795
|
-
let excluded_by_max_record = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
|
|
796
|
-
let included_maf_records = Arc::new(AtomicUsize::new(0));
|
|
797
|
-
let included_cnv_records = Arc::new(AtomicUsize::new(0));
|
|
798
|
-
let all_records = Arc::new(Mutex::new(Vec::<Vec<String>>::new()));
|
|
799
|
-
let data_count = Arc::new(AtomicUsize::new(0));
|
|
800
|
-
|
|
801
|
-
// Only collect errors (successful data is output immediately)
|
|
802
|
-
let errors = Arc::new(Mutex::new(Vec::<ErrorEntry>::new()));
|
|
803
|
-
|
|
804
|
-
let download_futures = futures::stream::iter(
|
|
805
|
-
data_urls
|
|
806
|
-
.into_iter()
|
|
807
|
-
.map(|(case_id, data_type, url)| async move { download_single_file(case_id, data_type, url, 2).await }),
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
// Process downloads and output results immediately as JSONL
|
|
811
|
-
download_futures
|
|
812
|
-
.buffer_unordered(20) // Increased concurrency for better performance
|
|
813
|
-
.for_each(|download_result| {
|
|
814
|
-
let successful_downloads = Arc::clone(&successful_downloads);
|
|
815
|
-
let failed_downloads = Arc::clone(&failed_downloads);
|
|
816
|
-
let filtered_maf_records = Arc::clone(&filtered_maf_records);
|
|
817
|
-
let filtered_cnv_records = Arc::clone(&filtered_cnv_records);
|
|
818
|
-
let filtered_records = Arc::clone(&filtered_records);
|
|
819
|
-
let included_maf_records = Arc::clone(&included_maf_records);
|
|
820
|
-
let included_cnv_records = Arc::clone(&included_cnv_records);
|
|
821
|
-
let hyper_mutator_records = Arc::clone(&hyper_mutator_records);
|
|
822
|
-
let excluded_by_max_record = Arc::clone(&excluded_by_max_record);
|
|
823
|
-
let errors = Arc::clone(&errors);
|
|
824
|
-
let all_records = Arc::clone(&all_records);
|
|
825
|
-
let data_count = Arc::clone(&data_count);
|
|
826
|
-
|
|
827
|
-
async move {
|
|
828
|
-
let current_count = data_count.load(Ordering::Relaxed);
|
|
829
|
-
if current_count >= max_record {
|
|
830
|
-
// Skip processing and mark as excluded by max_record
|
|
831
|
-
if let Ok((case_id, data_type, _)) = download_result {
|
|
832
|
-
let mut exclud_max_record = excluded_by_max_record.lock().await;
|
|
833
|
-
exclud_max_record
|
|
834
|
-
.entry(data_type.to_string())
|
|
835
|
-
.or_insert_with(Vec::new)
|
|
836
|
-
.push(case_id.to_string());
|
|
837
|
-
successful_downloads.fetch_add(1, Ordering::Relaxed);
|
|
838
|
-
}
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
match download_result {
|
|
842
|
-
Ok((case_id, data_type, content)) => {
|
|
843
|
-
// Try to parse the content
|
|
844
|
-
match parse_content(
|
|
845
|
-
&content,
|
|
846
|
-
&case_id,
|
|
847
|
-
&data_type,
|
|
848
|
-
min_total_depth,
|
|
849
|
-
min_alt_allele_count,
|
|
850
|
-
maf_hyper_mutator,
|
|
851
|
-
&consequences,
|
|
852
|
-
gain_threshold,
|
|
853
|
-
loss_threshold,
|
|
854
|
-
seg_length,
|
|
855
|
-
cnv_hyper_mutator,
|
|
856
|
-
&chromosomes,
|
|
857
|
-
&filtered_records,
|
|
858
|
-
&filtered_maf_records,
|
|
859
|
-
&filtered_cnv_records,
|
|
860
|
-
&included_maf_records,
|
|
861
|
-
&included_cnv_records,
|
|
862
|
-
&hyper_mutator_records,
|
|
863
|
-
)
|
|
864
|
-
.await
|
|
865
|
-
{
|
|
866
|
-
Ok(parsed_data) => {
|
|
867
|
-
let remaining = max_record - current_count;
|
|
868
|
-
if parsed_data.len() <= remaining {
|
|
869
|
-
data_count.fetch_add(parsed_data.len(), Ordering::Relaxed);
|
|
870
|
-
all_records.lock().await.extend(parsed_data);
|
|
871
|
-
} else {
|
|
872
|
-
// Skip file if it would exceed max_record
|
|
873
|
-
let mut exclud_max_record = excluded_by_max_record.lock().await;
|
|
874
|
-
exclud_max_record
|
|
875
|
-
.entry(data_type.to_string())
|
|
876
|
-
.or_insert_with(Vec::new)
|
|
877
|
-
.push(case_id.to_string());
|
|
878
|
-
}
|
|
879
|
-
successful_downloads.fetch_add(1, Ordering::Relaxed);
|
|
880
|
-
}
|
|
881
|
-
Err((cid, dtp, error)) => {
|
|
882
|
-
// Parsing failed - add to errors
|
|
883
|
-
failed_downloads.fetch_add(1, Ordering::Relaxed);
|
|
884
|
-
let error = ErrorEntry {
|
|
885
|
-
case_id: cid,
|
|
886
|
-
data_type: dtp,
|
|
887
|
-
error_type: "parsing_error".to_string(),
|
|
888
|
-
error_details: error,
|
|
889
|
-
attempts_made: 1,
|
|
890
|
-
};
|
|
891
|
-
errors.lock().await.push(error);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
Err((case_id, data_type, error_details, attempts)) => {
|
|
896
|
-
// Download failed - add to errors
|
|
897
|
-
failed_downloads.fetch_add(1, Ordering::Relaxed);
|
|
898
|
-
|
|
899
|
-
let (error_type, clean_details) = if error_details.contains(":") {
|
|
900
|
-
let parts: Vec<&str> = error_details.splitn(2, ": ").collect();
|
|
901
|
-
(parts[0].to_string(), parts[1].to_string())
|
|
902
|
-
} else {
|
|
903
|
-
("unknown_error".to_string(), error_details)
|
|
904
|
-
};
|
|
905
|
-
|
|
906
|
-
let error = ErrorEntry {
|
|
907
|
-
case_id,
|
|
908
|
-
data_type,
|
|
909
|
-
error_type,
|
|
910
|
-
error_details: clean_details,
|
|
911
|
-
attempts_made: attempts,
|
|
912
|
-
};
|
|
913
|
-
errors.lock().await.push(error);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
})
|
|
918
|
-
.await;
|
|
919
|
-
|
|
920
|
-
// Output final summary as the last line
|
|
921
|
-
let success_count = successful_downloads.load(Ordering::Relaxed);
|
|
922
|
-
let failed_count = failed_downloads.load(Ordering::Relaxed);
|
|
923
|
-
let filtered_maf_count = filtered_maf_records.load(Ordering::Relaxed);
|
|
924
|
-
let filtered_cnv_count = filtered_cnv_records.load(Ordering::Relaxed);
|
|
925
|
-
let included_maf_count = included_maf_records.load(Ordering::Relaxed);
|
|
926
|
-
let included_cnv_count = included_cnv_records.load(Ordering::Relaxed);
|
|
927
|
-
|
|
928
|
-
let summary = FinalSummary {
|
|
929
|
-
total_files,
|
|
930
|
-
successful_files: success_count,
|
|
931
|
-
failed_files: failed_count,
|
|
932
|
-
errors: errors.lock().await.clone(),
|
|
933
|
-
filtered_records: filtered_maf_count + filtered_cnv_count,
|
|
934
|
-
filtered_maf_records: filtered_maf_count,
|
|
935
|
-
filtered_cnv_records: filtered_cnv_count,
|
|
936
|
-
filtered_records_by_case: filtered_records.lock().await.clone(),
|
|
937
|
-
included_maf_records: included_maf_count,
|
|
938
|
-
included_cnv_records: included_cnv_count,
|
|
939
|
-
hyper_mutator_records: hyper_mutator_records.lock().await.clone(),
|
|
940
|
-
excluded_by_max_record: excluded_by_max_record.lock().await.clone(),
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
let grin2lesion = serde_json::to_string(&all_records.lock().await.drain(..).collect::<Vec<Vec<String>>>())
|
|
944
|
-
.unwrap_or_else(|_| "[]".to_string());
|
|
945
|
-
let output = Output { grin2lesion, summary };
|
|
946
|
-
|
|
947
|
-
// Output final summary - Node.js will know processing is complete when it sees this
|
|
948
|
-
// if let Ok(json) = serde_json::to_string(&summary) {
|
|
949
|
-
if let Ok(json) = serde_json::to_string(&output) {
|
|
950
|
-
println!("{}", json);
|
|
951
|
-
use std::io::Write;
|
|
952
|
-
let _ = std::io::stdout().flush();
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/// Read data from local file
|
|
957
|
-
async fn localread_data(
|
|
958
|
-
case_files: HashMap<String, DataType>,
|
|
959
|
-
min_total_depth: i32,
|
|
960
|
-
min_alt_allele_count: i32,
|
|
961
|
-
maf_hyper_mutator: i32,
|
|
962
|
-
consequences: &Option<Vec<String>>,
|
|
963
|
-
gain_threshold: f32,
|
|
964
|
-
loss_threshold: f32,
|
|
965
|
-
seg_length: i32,
|
|
966
|
-
cnv_hyper_mutator: i32,
|
|
967
|
-
chromosomes: &HashSet<String>,
|
|
968
|
-
max_record: usize,
|
|
969
|
-
) {
|
|
970
|
-
let data_files: Vec<(String, String, String)> = case_files
|
|
971
|
-
.into_iter()
|
|
972
|
-
.flat_map(|(case_id, data_types)| {
|
|
973
|
-
let mut files = Vec::new();
|
|
974
|
-
if let Some(cnv_file) = &data_types.cnv {
|
|
975
|
-
files.push((case_id.clone(), "cnv".to_string(), cnv_file.clone()));
|
|
976
|
-
}
|
|
977
|
-
if let Some(maf_file) = &data_types.maf {
|
|
978
|
-
files.push((case_id.clone(), "maf".to_string(), maf_file.clone()));
|
|
979
|
-
}
|
|
980
|
-
files
|
|
981
|
-
})
|
|
982
|
-
.collect();
|
|
983
|
-
let total_files = data_files.len();
|
|
984
|
-
|
|
985
|
-
// Counters for final summary
|
|
986
|
-
let successful_reads = Arc::new(AtomicUsize::new(0));
|
|
987
|
-
let failed_reads = Arc::new(AtomicUsize::new(0));
|
|
988
|
-
let filtered_maf_records = Arc::new(AtomicUsize::new(0));
|
|
989
|
-
let filtered_cnv_records = Arc::new(AtomicUsize::new(0));
|
|
990
|
-
let filtered_records = Arc::new(Mutex::new(HashMap::<String, FilteredCaseDetails>::new()));
|
|
991
|
-
let hyper_mutator_records = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
|
|
992
|
-
let excluded_by_max_record = Arc::new(Mutex::new(HashMap::<String, Vec<String>>::new()));
|
|
993
|
-
let included_maf_records = Arc::new(AtomicUsize::new(0));
|
|
994
|
-
let included_cnv_records = Arc::new(AtomicUsize::new(0));
|
|
995
|
-
let errors = Arc::new(Mutex::new(Vec::<ErrorEntry>::new()));
|
|
996
|
-
let all_records = Arc::new(Mutex::new(Vec::<Vec<String>>::new()));
|
|
997
|
-
let data_count = Arc::new(AtomicUsize::new(0));
|
|
998
|
-
|
|
999
|
-
// Process files concurrently
|
|
1000
|
-
let read_futures = futures::stream::iter(data_files.into_iter().map(
|
|
1001
|
-
|(case_id, data_type, file_path)| async move {
|
|
1002
|
-
// read the local file
|
|
1003
|
-
match fs::read_to_string(&file_path) {
|
|
1004
|
-
Ok(content) => Ok((case_id, data_type, content)),
|
|
1005
|
-
Err(e) => Err((
|
|
1006
|
-
case_id,
|
|
1007
|
-
data_type,
|
|
1008
|
-
format!("file_read_error: {}", e),
|
|
1009
|
-
1, // Single attempt for local file readng
|
|
1010
|
-
)),
|
|
1011
|
-
}
|
|
1012
|
-
},
|
|
1013
|
-
));
|
|
1014
|
-
|
|
1015
|
-
// Process files and output results
|
|
1016
|
-
read_futures
|
|
1017
|
-
.buffer_unordered(3)
|
|
1018
|
-
.for_each(|read_result| {
|
|
1019
|
-
let successful_reads = Arc::clone(&successful_reads);
|
|
1020
|
-
let failed_reads = Arc::clone(&failed_reads);
|
|
1021
|
-
let filtered_maf_records = Arc::clone(&filtered_maf_records);
|
|
1022
|
-
let filtered_cnv_records = Arc::clone(&filtered_cnv_records);
|
|
1023
|
-
let filtered_records = Arc::clone(&filtered_records);
|
|
1024
|
-
let included_maf_records = Arc::clone(&included_maf_records);
|
|
1025
|
-
let included_cnv_records = Arc::clone(&included_cnv_records);
|
|
1026
|
-
let hyper_mutator_records = Arc::clone(&hyper_mutator_records);
|
|
1027
|
-
let excluded_by_max_record = Arc::clone(&excluded_by_max_record);
|
|
1028
|
-
let errors = Arc::clone(&errors);
|
|
1029
|
-
let all_records = Arc::clone(&all_records);
|
|
1030
|
-
let data_count = Arc::clone(&data_count);
|
|
1031
|
-
|
|
1032
|
-
async move {
|
|
1033
|
-
let current_count = data_count.load(Ordering::Relaxed);
|
|
1034
|
-
if current_count >= max_record {
|
|
1035
|
-
// Skip processing and mark as excluded by max_record
|
|
1036
|
-
if let Ok((case_id, data_type, _)) = read_result {
|
|
1037
|
-
let mut exclud_max_record = excluded_by_max_record.lock().await;
|
|
1038
|
-
exclud_max_record
|
|
1039
|
-
.entry(data_type.to_string())
|
|
1040
|
-
.or_insert_with(Vec::new)
|
|
1041
|
-
.push(case_id.to_string());
|
|
1042
|
-
successful_reads.fetch_add(1, Ordering::Relaxed);
|
|
1043
|
-
}
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
match read_result {
|
|
1047
|
-
Ok((case_id, data_type, content)) => {
|
|
1048
|
-
match parse_content(
|
|
1049
|
-
&content,
|
|
1050
|
-
&case_id,
|
|
1051
|
-
&data_type,
|
|
1052
|
-
min_total_depth,
|
|
1053
|
-
min_alt_allele_count,
|
|
1054
|
-
maf_hyper_mutator,
|
|
1055
|
-
consequences,
|
|
1056
|
-
gain_threshold,
|
|
1057
|
-
loss_threshold,
|
|
1058
|
-
seg_length,
|
|
1059
|
-
cnv_hyper_mutator,
|
|
1060
|
-
chromosomes,
|
|
1061
|
-
&filtered_records,
|
|
1062
|
-
&filtered_maf_records,
|
|
1063
|
-
&filtered_cnv_records,
|
|
1064
|
-
&included_maf_records,
|
|
1065
|
-
&included_cnv_records,
|
|
1066
|
-
&hyper_mutator_records,
|
|
1067
|
-
)
|
|
1068
|
-
.await
|
|
1069
|
-
{
|
|
1070
|
-
Ok(parsed_data) => {
|
|
1071
|
-
let remaining = max_record - current_count;
|
|
1072
|
-
if parsed_data.len() <= remaining {
|
|
1073
|
-
data_count.fetch_add(parsed_data.len(), Ordering::Relaxed);
|
|
1074
|
-
all_records.lock().await.extend(parsed_data);
|
|
1075
|
-
} else {
|
|
1076
|
-
// Skip file if it would exceed max_record
|
|
1077
|
-
let mut exclud_max_record = excluded_by_max_record.lock().await;
|
|
1078
|
-
exclud_max_record
|
|
1079
|
-
.entry(data_type.to_string())
|
|
1080
|
-
.or_insert_with(Vec::new)
|
|
1081
|
-
.push(case_id.to_string());
|
|
1082
|
-
}
|
|
1083
|
-
successful_reads.fetch_add(1, Ordering::Relaxed);
|
|
1084
|
-
}
|
|
1085
|
-
Err((cid, dtp, error)) => {
|
|
1086
|
-
failed_reads.fetch_add(1, Ordering::Relaxed);
|
|
1087
|
-
let error = ErrorEntry {
|
|
1088
|
-
case_id: cid,
|
|
1089
|
-
data_type: dtp,
|
|
1090
|
-
error_type: "parsing_error".to_string(),
|
|
1091
|
-
error_details: error,
|
|
1092
|
-
attempts_made: 1,
|
|
1093
|
-
};
|
|
1094
|
-
errors.lock().await.push(error);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
Err((case_id, data_type, error_details, attempts)) => {
|
|
1099
|
-
failed_reads.fetch_add(1, Ordering::Relaxed);
|
|
1100
|
-
let (error_type, clean_details) = if error_details.contains(":") {
|
|
1101
|
-
let parts: Vec<&str> = error_details.splitn(2, ": ").collect();
|
|
1102
|
-
(parts[0].to_string(), parts[1].to_string())
|
|
1103
|
-
} else {
|
|
1104
|
-
("unknown_error".to_string(), error_details)
|
|
1105
|
-
};
|
|
1106
|
-
let error = ErrorEntry {
|
|
1107
|
-
case_id,
|
|
1108
|
-
data_type,
|
|
1109
|
-
error_type,
|
|
1110
|
-
error_details: clean_details,
|
|
1111
|
-
attempts_made: attempts,
|
|
1112
|
-
};
|
|
1113
|
-
errors.lock().await.push(error);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
})
|
|
1118
|
-
.await;
|
|
1119
|
-
// Output final summary as the last line
|
|
1120
|
-
let success_count = successful_reads.load(Ordering::Relaxed);
|
|
1121
|
-
let failed_count = failed_reads.load(Ordering::Relaxed);
|
|
1122
|
-
let filtered_maf_count = filtered_maf_records.load(Ordering::Relaxed);
|
|
1123
|
-
let filtered_cnv_count = filtered_cnv_records.load(Ordering::Relaxed);
|
|
1124
|
-
let included_maf_count = included_maf_records.load(Ordering::Relaxed);
|
|
1125
|
-
let included_cnv_count = included_cnv_records.load(Ordering::Relaxed);
|
|
1126
|
-
|
|
1127
|
-
let summary = FinalSummary {
|
|
1128
|
-
total_files,
|
|
1129
|
-
successful_files: success_count,
|
|
1130
|
-
failed_files: failed_count,
|
|
1131
|
-
errors: errors.lock().await.clone(),
|
|
1132
|
-
filtered_records: filtered_maf_count + filtered_cnv_count,
|
|
1133
|
-
filtered_maf_records: filtered_maf_count,
|
|
1134
|
-
filtered_cnv_records: filtered_cnv_count,
|
|
1135
|
-
filtered_records_by_case: filtered_records.lock().await.clone(),
|
|
1136
|
-
included_maf_records: included_maf_count,
|
|
1137
|
-
included_cnv_records: included_cnv_count,
|
|
1138
|
-
hyper_mutator_records: hyper_mutator_records.lock().await.clone(),
|
|
1139
|
-
excluded_by_max_record: excluded_by_max_record.lock().await.clone(),
|
|
1140
|
-
};
|
|
1141
|
-
|
|
1142
|
-
let grin2lesion = serde_json::to_string(&all_records.lock().await.drain(..).collect::<Vec<Vec<String>>>())
|
|
1143
|
-
.unwrap_or_else(|_| "[]".to_string());
|
|
1144
|
-
let output = Output { grin2lesion, summary };
|
|
1145
|
-
|
|
1146
|
-
// Output final JSON array
|
|
1147
|
-
if let Ok(json) = serde_json::to_string(&output) {
|
|
1148
|
-
println!("{}", json);
|
|
1149
|
-
use std::io::Write;
|
|
1150
|
-
let _ = std::io::stdout().flush();
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
#[tokio::main]
|
|
1155
|
-
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
1156
|
-
let args: Vec<String> = env::args().collect();
|
|
1157
|
-
let from_file = args.contains(&"--from-file".to_string());
|
|
1158
|
-
|
|
1159
|
-
const HOST: &str = "https://api.gdc.cancer.gov/data/";
|
|
1160
|
-
|
|
1161
|
-
// Read input with timeout
|
|
1162
|
-
let timeout_duration = Duration::from_secs(10); // Increased timeout for input
|
|
1163
|
-
|
|
1164
|
-
let result = timeout(timeout_duration, async {
|
|
1165
|
-
let mut buffer = String::new();
|
|
1166
|
-
let mut reader = BufReader::new(tokio::io::stdin());
|
|
1167
|
-
reader.read_to_string(&mut buffer).await?;
|
|
1168
|
-
Ok::<String, io::Error>(buffer)
|
|
1169
|
-
})
|
|
1170
|
-
.await;
|
|
1171
|
-
|
|
1172
|
-
// Handle input parsing (silently)
|
|
1173
|
-
let input_js: InputData = match result {
|
|
1174
|
-
Ok(Ok(buffer)) => match serde_json::from_str(&buffer) {
|
|
1175
|
-
Ok(js) => js,
|
|
1176
|
-
Err(_e) => {
|
|
1177
|
-
// Silent failure - exit without stderr
|
|
1178
|
-
std::process::exit(1);
|
|
1179
|
-
}
|
|
1180
|
-
},
|
|
1181
|
-
Ok(Err(_e)) => {
|
|
1182
|
-
// Silent failure - exit without stderr
|
|
1183
|
-
std::process::exit(1);
|
|
1184
|
-
}
|
|
1185
|
-
Err(_) => {
|
|
1186
|
-
// Silent failure - exit without stderr
|
|
1187
|
-
std::process::exit(1);
|
|
1188
|
-
}
|
|
1189
|
-
};
|
|
1190
|
-
|
|
1191
|
-
// Validate input (silently)
|
|
1192
|
-
if input_js.case_files.is_empty() {
|
|
1193
|
-
// Silent failure - exit without stderr
|
|
1194
|
-
std::process::exit(1);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
let case_files = input_js.case_files;
|
|
1198
|
-
let max_record: usize = input_js.max_record;
|
|
1199
|
-
|
|
1200
|
-
// Set default maf_options
|
|
1201
|
-
let (min_total_depth, min_alt_allele_count, maf_hyper_mutator, consequences) = match input_js.maf_options {
|
|
1202
|
-
Some(options) => (
|
|
1203
|
-
options.min_total_depth,
|
|
1204
|
-
options.min_alt_allele_count,
|
|
1205
|
-
options.hyper_mutator,
|
|
1206
|
-
options.consequences.clone(),
|
|
1207
|
-
),
|
|
1208
|
-
None => (10, 2, 8000, None), // Default values
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
// Set default cnv_options
|
|
1212
|
-
let (gain_threshold, loss_threshold, seg_length, cnv_hyper_mutator) = match input_js.cnv_options {
|
|
1213
|
-
Some(options) => (
|
|
1214
|
-
options.gain_threshold,
|
|
1215
|
-
options.loss_threshold,
|
|
1216
|
-
options.seg_length,
|
|
1217
|
-
options.hyper_mutator,
|
|
1218
|
-
),
|
|
1219
|
-
None => (0.3, -0.4, 0, 500), // Default values
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
// Convert Vec<String> to HashSet<String> for faster lookup
|
|
1223
|
-
let chromosomes = input_js.chromosomes.into_iter().collect::<HashSet<String>>();
|
|
1224
|
-
|
|
1225
|
-
if from_file {
|
|
1226
|
-
localread_data(
|
|
1227
|
-
case_files,
|
|
1228
|
-
min_total_depth,
|
|
1229
|
-
min_alt_allele_count,
|
|
1230
|
-
maf_hyper_mutator,
|
|
1231
|
-
&consequences,
|
|
1232
|
-
gain_threshold,
|
|
1233
|
-
loss_threshold,
|
|
1234
|
-
seg_length,
|
|
1235
|
-
cnv_hyper_mutator,
|
|
1236
|
-
&chromosomes,
|
|
1237
|
-
max_record,
|
|
1238
|
-
)
|
|
1239
|
-
.await;
|
|
1240
|
-
} else {
|
|
1241
|
-
// Download data from GDC- this will now handle errors gracefully
|
|
1242
|
-
download_data(
|
|
1243
|
-
case_files,
|
|
1244
|
-
HOST,
|
|
1245
|
-
min_total_depth,
|
|
1246
|
-
min_alt_allele_count,
|
|
1247
|
-
maf_hyper_mutator,
|
|
1248
|
-
&consequences,
|
|
1249
|
-
gain_threshold,
|
|
1250
|
-
loss_threshold,
|
|
1251
|
-
seg_length,
|
|
1252
|
-
cnv_hyper_mutator,
|
|
1253
|
-
&chromosomes,
|
|
1254
|
-
max_record,
|
|
1255
|
-
)
|
|
1256
|
-
.await;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Always exit successfully - individual file failures are logged but don't stop the process
|
|
1260
|
-
Ok(())
|
|
1261
|
-
}
|