1use crate::cli::FireOptions;
2use crate::fiber::FiberseqData;
3use crate::utils::bamannotations::AnnotationTypeView;
4use crate::utils::platform::SeqPlatform;
5use crate::*;
6use anyhow;
7use derive_builder::Builder;
8use gbdt::decision_tree::{Data, DataVec};
9use gbdt::gradient_boost::GBDT;
10use itertools::Itertools;
11use ordered_float::OrderedFloat;
12use serde::Deserialize;
13use std::collections::BTreeMap;
14use std::fs;
15use tempfile::NamedTempFile;
16
17pub static FIRE_MODEL: &str = include_str!("../../models/FIRE.gbdt.json");
18pub static FIRE_CONF_JSON: &str = include_str!("../../models/FIRE.conf.json");
19
20pub fn get_model(fire_opts: &FireOptions) -> (GBDT, MapPrecisionValues) {
21 let mut remove_temp_file = false;
22 let (model_file, fdr_table_file) = match (&fire_opts.model, &fire_opts.fdr_table) {
24 (Some(model_file), Some(b)) => {
25 let fdr_table = fs::read_to_string(b).expect("Unable to read file");
26 (model_file.clone(), fdr_table)
27 }
28 _ => {
29 let temp_file = NamedTempFile::new().expect("Unable to make a temp file");
30 let (mut temp_file, path) = temp_file.keep().expect("Unable to keep temp file");
31 let temp_file_name = path
32 .as_os_str()
33 .to_str()
34 .expect("Unable to convert the path of the named temp file to an &str.");
35 temp_file
36 .write_all(FIRE_MODEL.as_bytes())
37 .expect("Unable to write file");
38 remove_temp_file = true;
40 (temp_file_name.to_string(), FIRE_CONF_JSON.to_string())
41 }
42 };
43 log::info!("Using model: {model_file}");
44 let model =
46 GBDT::from_xgboost_dump(&model_file, "binary:logistic").expect("failed to load FIRE model");
47 if remove_temp_file {
48 fs::remove_file(model_file).expect("Unable to remove temp file");
49 }
50
51 let precision_table: PrecisionTable =
53 serde_json::from_str(&fdr_table_file).expect("Precision table JSON was not well-formatted");
54 let precision_converter = MapPrecisionValues::new(&precision_table);
55
56 (model, precision_converter)
58}
59
60fn get_mid_point(start: i64, end: i64) -> i64 {
61 (start + end) / 2
62}
63
64pub fn get_bins(mid_point: i64, bin_num: i64, bin_width: i64, max_end: i64) -> Vec<(i64, i64)> {
70 let mut bins = Vec::new();
71 for i in 0..bin_num {
72 let mut bin_start = mid_point - (bin_num / 2 - i) * bin_width - bin_width / 2;
73 let mut bin_end = bin_start + bin_width;
74 if bin_start < 0 {
75 bin_start = 0;
76 }
77 if bin_end < 0 {
78 bin_end = 0;
79 }
80 if bin_start > max_end {
81 bin_start = max_end - 1;
82 }
83 if bin_end > max_end {
84 bin_end = max_end;
85 }
86 bins.push((bin_start, bin_end));
87 }
88 bins
89}
90
91fn get_m6a_rle_data(m6a_view: &AnnotationTypeView, start: i64, end: i64) -> (f32, f32) {
93 let mut m6a_rles = vec![];
94 let mut max = 0;
95 let mut _max_pos = 0;
96 let mut weighted_rle = 0.0;
98 for (m6a_1, m6a_2) in m6a_view
99 .infos()
100 .iter()
101 .map(|a| a.query_start as i64)
102 .tuple_windows()
103 {
104 if m6a_1 < start || m6a_1 > end || m6a_2 < start || m6a_2 > end {
106 continue;
107 }
108 let rle = (m6a_2 - m6a_1).abs();
110 m6a_rles.push(rle);
111 if rle > max {
113 max = rle;
114 _max_pos = ((m6a_1 + m6a_2) / 2 - (end + start) / 2).abs();
115 }
116 weighted_rle += (rle * rle) as f32;
118 }
119 weighted_rle /= (end - start) as f32;
120
121 if m6a_rles.is_empty() {
122 return (-1.0, -1.0);
123 }
124 let mid_length = m6a_rles.len() / 2;
125 let (_, median, _) = m6a_rles.select_nth_unstable(mid_length);
126 (weighted_rle, *median as f32)
127}
128
129const FEATS_IN_USE: [&str; 3] = [
130 "m6a_count",
131 "frac_m6a",
134 "m6a_fc",
135 ];
140#[derive(Debug, Clone, Builder)]
141pub struct FireFeatsInRange {
142 pub m6a_count: f32,
143 pub at_count: f32,
144 #[allow(unused)]
145 pub count_5mc: f32,
146 pub frac_m6a: f32,
147 pub m6a_fc: f32,
148 pub weighted_m6a_rle: f32,
149 pub median_m6a_rle: f32,
150}
151
152impl FireFeatsInRange {
153 pub fn header(tag: &str) -> String {
154 let mut out = "".to_string();
155 for col in FEATS_IN_USE.iter() {
156 out += &format!("\t{tag}_{col}");
157 }
158 out
159 }
160}
161
162#[derive(Debug)]
163pub struct FireFeats<'a> {
164 rec: &'a FiberseqData,
165 m6a_view: AnnotationTypeView<'a>,
169 cpg_view: AnnotationTypeView<'a>,
170 #[allow(unused)]
171 at_count: usize,
172 m6a_count: usize,
173 frac_m6a: f32,
174 fire_opts: &'a FireOptions,
176 is_ont: bool,
179 seq: Vec<u8>,
180 fire_feats: Vec<(i64, i64, Vec<f32>)>,
181}
182
183impl<'a> FireFeats<'a> {
184 pub fn new(rec: &'a FiberseqData, fire_opts: &'a FireOptions) -> Self {
185 let seq_len = rec.record.seq_len();
186 let seq = rec.record.seq().as_bytes();
187
188 let is_ont = fire_opts.ont || matches!(rec.platform(), SeqPlatform::Ont);
192
193 let mut rtn = Self {
194 rec,
195 m6a_view: rec.m6a(),
196 cpg_view: rec.cpg(),
197 at_count: 0,
198 m6a_count: 0,
199 frac_m6a: 0.0,
200 fire_opts,
202 is_ont,
203 seq,
204 fire_feats: vec![],
205 };
206
207 rtn.at_count = rtn.get_at_count(0, seq_len as i64);
209 rtn.m6a_count = rtn.get_m6a_count(0, seq_len as i64);
210 rtn.frac_m6a = if rtn.at_count > 0 {
211 rtn.m6a_count as f32 / rtn.at_count as f32
212 } else {
213 0.0
214 };
215
216 rtn.get_fire_features();
217 if rtn.is_ont {
218 rtn.validate_that_ont_is_single_strand();
219 }
220 rtn
221 }
222
223 fn validate_that_ont_is_single_strand(&self) {
224 let sequenced_bp = if self.rec.record.is_reverse() {
225 b'T'
226 } else {
227 b'A'
228 };
229 for info in self.m6a_view.infos() {
230 let m6a_st = info.query_start as i64;
231 let m6a_bp = self.seq[m6a_st as usize];
232 if m6a_bp != sequenced_bp {
233 log::warn!(
234 "m6A site at {} is not the same as the sequenced base {}",
235 m6a_st,
236 sequenced_bp as char
237 );
238 }
239 }
240 }
241
242 fn get_bp_count(&self, start: i64, end: i64, bp: u8) -> usize {
243 let subseq = &self.seq[start as usize..end as usize];
244 subseq.iter().filter(|&&b| b == bp).count()
245 }
246
247 fn get_at_count(&self, start: i64, end: i64) -> usize {
248 self.get_bp_count(start, end, b'A') + self.get_bp_count(start, end, b'T')
249 }
250
251 fn get_5mc_count(&self, start: i64, end: i64) -> usize {
252 self.cpg_view.count_query_in(start, end)
253 }
254
255 fn get_m6a_count(&self, start: i64, end: i64) -> usize {
256 let mut m6a_count = self.m6a_view.count_query_in(start, end);
257
258 if self.is_ont {
260 let mut sequenced_bp = self.get_bp_count(start, end, b'A');
261 let mut un_sequenced_bp = self.get_bp_count(start, end, b'T');
262 if self.rec.record.is_reverse() {
263 std::mem::swap(&mut sequenced_bp, &mut un_sequenced_bp);
265 }
266 let m6a_frac = if sequenced_bp > 0 {
267 m6a_count as f32 / sequenced_bp as f32
268 } else {
269 0.0
270 };
271 m6a_count += (un_sequenced_bp as f32 * m6a_frac).round() as usize;
272 }
273 m6a_count
274 }
275
276 fn m6a_fc_over_expected(&self, m6a_count: usize, at_count: usize) -> f32 {
277 let expected = self.frac_m6a * at_count as f32;
280 let observed = m6a_count as f32;
281 if expected == 0.0 || observed == 0.0 {
282 return 0.0;
283 }
284 let fc = observed / expected;
285 fc.log2()
286 }
287
288 fn feats_in_range(&self, start: i64, end: i64) -> FireFeatsInRange {
289 let m6a_count = self.get_m6a_count(start, end);
290 let at_count = self.get_at_count(start, end);
291 let count_5mc = self.get_5mc_count(start, end);
292 let frac_m6a = if at_count > 0 {
293 m6a_count as f32 / at_count as f32
294 } else {
295 0.0
296 };
297 let m6a_fc = self.m6a_fc_over_expected(m6a_count, at_count);
298 let (weighted_m6a_rle, median_m6a_rle) = get_m6a_rle_data(&self.m6a_view, start, end);
299
300 FireFeatsInRange {
301 m6a_count: m6a_count as f32,
302 at_count: at_count as f32,
303 count_5mc: count_5mc as f32,
304 frac_m6a,
305 m6a_fc,
306 weighted_m6a_rle,
307 median_m6a_rle,
308 }
309 }
310
311 pub fn fire_feats_header(fire_opts: &FireOptions) -> String {
312 let mut out = "#chrom\tstart\tend\tfiber".to_string();
313 out += "\tmsp_len\tmsp_len_times_m6a_fc\tccs_passes";
314 out += "\tfiber_m6a_count\tfiber_m6a_frac";
315 out += &FireFeatsInRange::header("msp");
316 out += &FireFeatsInRange::header("best");
317 out += &FireFeatsInRange::header("worst");
318 for bin_num in 0..fire_opts.bin_num {
319 out += &FireFeatsInRange::header(&format!("bin_{bin_num}"));
320 }
321 out += "\n";
322 out
323 }
324
325 fn msp_get_fire_features(&self, start: i64, end: i64) -> Vec<f32> {
326 let msp_len = end - start;
327 if msp_len < self.fire_opts.min_msp_length_for_positive_fire_call {
329 return vec![];
330 }
331 let ccs_passes = if self.is_ont { 4.0 } else { self.rec.ec };
332
333 let mut max_m6a_count = 0;
335 let mut max_m6a_start = 0;
336 let mut max_m6a_end = 0;
337 let mut min_m6a_count = usize::MAX;
338 let mut min_m6a_start = 0;
339 let mut min_m6a_end = 0;
340 let mut centering_pos = get_mid_point(start, end);
341 for st_idx in start..end {
342 let en_idx = (st_idx + self.fire_opts.best_window_size).min(end);
343 if (end - start) < (2 * self.fire_opts.best_window_size) {
345 log::trace!("MSP window is not large enough for best and worst window analysis");
346 break;
347 }
348 let m6a_count = self.get_m6a_count(st_idx, en_idx);
349 if m6a_count > max_m6a_count {
350 max_m6a_count = m6a_count;
351 max_m6a_start = st_idx;
352 max_m6a_end = en_idx;
353 centering_pos = get_mid_point(st_idx, en_idx);
355 }
356 if m6a_count < min_m6a_count {
357 min_m6a_count = m6a_count;
358 min_m6a_start = st_idx;
359 min_m6a_end = en_idx;
360 }
361 if en_idx == end {
362 break;
363 }
364 }
365 let best_fire_feats = self.feats_in_range(max_m6a_start, max_m6a_end);
366 let worst_fire_feats = self.feats_in_range(min_m6a_start, min_m6a_end);
367
368 let msp_feats = self.feats_in_range(start, end);
369 let bins = get_bins(
370 centering_pos,
371 self.fire_opts.bin_num,
372 self.fire_opts.width_bin,
373 self.rec.record.seq_len() as i64,
374 );
375 let bin_feats = bins
376 .into_iter()
377 .map(|(start, end)| self.feats_in_range(start, end))
378 .collect::<Vec<FireFeatsInRange>>();
379 let msp_len_times_m6a_fc = msp_feats.m6a_fc * (msp_len as f32);
380 let mut rtn = vec![
381 msp_len as f32,
382 msp_len_times_m6a_fc,
383 ccs_passes,
384 self.m6a_count as f32,
385 self.frac_m6a,
386 ];
387 let feat_sets = vec![&msp_feats, &best_fire_feats, &worst_fire_feats]
388 .into_iter()
389 .chain(bin_feats.iter());
390 for feat_set in feat_sets {
391 rtn.push(feat_set.m6a_count);
392 rtn.push(feat_set.frac_m6a);
395 rtn.push(feat_set.m6a_fc);
396 }
399 rtn
400 }
401
402 pub fn get_fire_features(&mut self) {
403 let msp_data: Vec<_> = self.rec.msp().into_iter().collect();
404 self.fire_feats = msp_data
405 .into_iter()
406 .map(|annotation| {
407 let s = annotation.query_start as i64;
408 let e = annotation.query_end as i64;
409 let (rs, re) = match (annotation.ref_start, annotation.ref_end) {
410 (Some(rs), Some(re)) => (rs as i64, re as i64),
411 _ => (0, 0),
412 };
413 (rs, re, self.msp_get_fire_features(s, e))
414 })
415 .collect();
416 }
417
418 pub fn dump_fire_feats(&self, out_buffer: &mut Box<dyn Write>) -> Result<(), anyhow::Error> {
419 for (s, e, row) in self.fire_feats.iter() {
420 if row.is_empty() {
421 continue;
422 }
423 let lead_feats = format!(
424 "{}\t{}\t{}\t{}\t",
425 self.rec.target_name,
426 s,
427 e,
428 String::from_utf8_lossy(self.rec.record.qname())
429 );
430 out_buffer.write_all(lead_feats.as_bytes())?;
431 out_buffer.write_all(row.iter().join("\t").as_bytes())?;
432 out_buffer.write_all(b"\n")?;
433 }
434 Ok(())
435 }
436
437 pub fn predict_with_xgb(
438 &self,
439 gbdt_model: &GBDT,
440 precision_converter: &MapPrecisionValues,
441 ) -> Vec<u8> {
442 let count = self.fire_feats.len();
443 if count == 0 {
444 return vec![];
445 }
446 let mut gbdt_data: DataVec = Vec::new();
448 for (_st, _en, window) in self.fire_feats.iter() {
449 if window.is_empty() {
450 continue;
451 }
452 let d = Data::new_test_data(window.to_vec(), None);
453 gbdt_data.push(d);
454 }
455 let predictions_without_short_ones = gbdt_model.predict(&gbdt_data);
456
457 let mut precisions = Vec::with_capacity(count);
459 let mut cur_pos = 0;
460 for (_st, _en, window) in self.fire_feats.iter() {
461 if window.is_empty() {
462 precisions.push(0);
463 } else {
464 let precision = precision_converter
465 .precision_from_float(predictions_without_short_ones[cur_pos]);
466 precisions.push(precision);
467 cur_pos += 1;
468 }
469 }
470 assert_eq!(cur_pos, predictions_without_short_ones.len());
472 assert_eq!(precisions.len(), count);
473 precisions
474 }
475}
476
477#[derive(Debug, Deserialize)]
478pub struct PrecisionTable {
479 pub columns: Vec<String>,
480 pub data: Vec<(f32, f32)>,
482}
483
484pub struct MapPrecisionValues {
485 pub map: BTreeMap<OrderedFloat<f32>, u8>,
486}
487
488impl MapPrecisionValues {
489 pub fn new(pt: &PrecisionTable) -> Self {
490 let mut map = BTreeMap::new();
492
493 for (mokapot_score, mokapot_q_value) in pt.data.iter() {
494 let precision = ((1.0 - mokapot_q_value) * 255.0).round() as u8;
495 map.insert(OrderedFloat(*mokapot_score), precision);
496 }
497 map.insert(
499 OrderedFloat(0.0),
500 *map.get(&OrderedFloat(0.0)).unwrap_or(&0),
501 );
502 Self { map }
503 }
504
505 pub fn precision_from_float(&self, value: f32) -> u8 {
507 let key = OrderedFloat(value);
508 let (less_key, less_val) = self
510 .map
511 .range(..key)
512 .next_back()
513 .unwrap_or((&OrderedFloat(0.0), &0));
514 let (more_key, more_val) = self
516 .map
517 .range(key..)
518 .next()
519 .unwrap_or((&OrderedFloat(1.0), &255));
520 if (more_key - key).abs() < (less_key - key).abs() {
521 *more_val
522 } else {
523 *less_val
524 }
525 }
526}