code wiki / (root) / nx_supplement_batch.nx

nx_supplement_batch.nx source

↩ module page · 126 lines · 5140 B

1// nx_supplement_batch.nx -- C5.4 + C4.4: batch CLI with user-extensible 2// compound DB + RT calibration + multi-file processing. 3// 4// USAGE (3 positional args, then peak files): 5// nx_supplement_batch <compounds-or-dash> <cal-or-dash> <peak1> [peak2] ... 6// 7// compounds-or-dash: path to user-defined compound TSV file, or "-" 8// to use only the 24-compound built-in seed DB. 9// Format (one per line, '#' comments): 10// id|name|SMILES|reg_code|citation 11// reg_code: 0=UNK 1=APPROVED 2=RESTRICTED 3=BANNED 4=RX 12// 13// cal-or-dash: path to RT calibration CSV, or "-" for mass-only. 14// Format (per C7.1): 15// # id ref_rt_seconds tolerance_seconds 16// 4 740.0 30.0 17// 18// peak1, peak2, ...: one or more peak text files to process. 19// 20// EXAMPLES: 21// # Built-in DB, no RT cal, 3 samples 22// $ nx_supplement_batch - - lot_A.txt lot_B.txt lot_C.txt 23// 24// # Built-in DB + RT cal, 3 samples 25// $ nx_supplement_batch - rt_cal.csv lot_A.txt lot_B.txt lot_C.txt 26// 27// # Lab's extended DB + RT cal, 50 samples 28// $ nx_supplement_batch lab_compounds.tsv rt_cal.csv samples/*.txt 29// 30// EXIT CODE: 31// Total banned detections summed across all samples. 32// 33// Pipe straight to LIMS: 34// $ nx_supplement_batch - - samples/*.txt | \ 35// jq -r 'select(.verdict.recommendation=="REJECT") | .sample_id' 36 37import "nx_chem.nx" 38import "nx_chem_molecule.nx" 39import "nx_chem_smiles.nx" 40import "nx_chem_periodic.nx" 41import "nx_chem_valence.nx" 42import "nx_chem_mass.nx" 43import "nx_chem_isotope_pattern.nx" 44import "nx_chem_adulterant_db.nx" 45import "nx_chem_peak_list.nx" 46import "nx_chem_report_json.nx" 47 48// Process one sample: read peak file, emit JSON+newline to stdout, 49// return banned-count for the verdict aggregation. 50func process_one_sample(db: *AdulterantDB, peak_path: *u8) -> nx_int { 51 let pl: *PeakList = nx_chem_peak_list_read_file(peak_path) 52 if pl.n == 0 { 53 // Empty/missing file -- emit a minimal error JSON and skip 54 let err_buf: *u8 = sys_mmap(256) 55 var w: nx_int = 0 56 w = w + nx_chem_emit_str(err_buf, w, "{\"sample_id\":" as *u8) 57 let sid: *u8 = sys_mmap(256) 58 let _b1: nx_int = nx_chem_basename(peak_path, sid) 59 w = w + nx_chem_emit_str_escaped(err_buf, w, sid) 60 w = w + nx_chem_emit_str(err_buf, w, ",\"error\":\"empty or unreadable peak file\"}\n" as *u8) 61 let _w: i64 = sys_write(1, err_buf, w as i64) 62 return 0 63 } 64 let sample_id: *u8 = sys_mmap(256) 65 let _bn: nx_int = nx_chem_basename(peak_path, sample_id) 66 let buf: *u8 = sys_mmap(32768) 67 let n_json: nx_int = nx_chem_emit_report_json(buf, pl, db, sample_id, 5) 68 let _w1: i64 = sys_write(1, buf, n_json as i64) 69 let nl: *u8 = sys_mmap(8); nl[0] = 0x0A 70 let _w2: i64 = sys_write(1, nl, 1) 71 // Count banned via re-walk (same as nx_supplement_check) 72 var n_banned: nx_int = 0 73 var pi: nx_int = 0 74 while pi < pl.n { 75 let p: *PeakObservation = ((pl.peaks as nx_int) + (pi * NX_PEAK_OBS_BYTES)) as *PeakObservation 76 let outs: *MatchResult = (sys_mmap((4 * NX_MATCH_RESULT_BYTES) as i64)) as *MatchResult 77 let nm: nx_int = nx_chem_adulterant_db_lookup_with_rt(db, p.mz_q4, p.rt_q3, 5, outs, 4) 78 var mi: nx_int = 0 79 while mi < nm { 80 let rr: *MatchResult = ((outs as nx_int) + (mi * NX_MATCH_RESULT_BYTES)) as *MatchResult 81 let ee: *AdulterantEntry = nx_chem_adulterant_entry_by_id(db, rr.entry_id) 82 if ee.regulatory == NX_REG_BANNED { n_banned = n_banned + 1 } 83 mi = mi + 1 84 } 85 pi = pi + 1 86 } 87 return n_banned 88} 89 90// Treat argv string as "dash sentinel" if it's exactly "-" (single byte). 91// Returns 1 if argv is "-", 0 otherwise (real path). 92func is_dash_sentinel(p: *u8) -> nx_int { 93 if p[0] as nx_int != 45 { return 0 } // not '-' 94 if p[1] as nx_int != 0 { return 0 } // multi-char string starting with '-' 95 return 1 96} 97 98func main(argc: i64, argv: *i64) -> i64 { 99 if argc < 4 { 100 let msg: *u8 = "nx_supplement_batch: usage: <compounds-or-dash> <cal-or-dash> <peak1> [peak2] ...\n" as *u8 101 var n: i64 = 0 102 while msg[n] != 0 { n = n + 1 } 103 let _w: i64 = sys_write(2, msg, n) 104 return 1 105 } 106 let db: *AdulterantDB = nx_chem_adulterant_db_seed() 107 // argv[1]: compound DB extension (or "-" to skip) 108 let cmp_path: *u8 = argv[1] as *u8 109 if is_dash_sentinel(cmp_path) == 0 { 110 let _nc: nx_int = nx_chem_load_compounds_from_file(db, cmp_path) 111 } 112 // argv[2]: RT calibration file (or "-" to skip) 113 let cal_path: *u8 = argv[2] as *u8 114 if is_dash_sentinel(cal_path) == 0 { 115 let _nr: nx_int = nx_chem_load_rt_calibration(db, cal_path) 116 } 117 var total_banned: i64 = 0 118 var i: i64 = 3 119 while i < argc { 120 let peak_path: *u8 = argv[i] as *u8 121 let b: nx_int = process_one_sample(db, peak_path) 122 total_banned = total_banned + (b as i64) 123 i = i + 1 124 } 125 return total_banned 126}