1use crate::cli::PredictM6AOptions;
2use crate::utils::basemods;
3use crate::utils::bio_io;
4use crate::utils::ma_io;
5use crate::utils::nucleosome;
6use crate::*;
7use bio::alphabets::dna::revcomp;
8use burn::tensor::backend::Backend;
9use fiber::FiberseqData;
10use ordered_float::OrderedFloat;
11use rayon::iter::IndexedParallelIterator;
12use rayon::iter::ParallelIterator;
13use rayon::prelude::IntoParallelRefMutIterator;
14use rust_htslib::{bam, bam::Read};
15use serde::Deserialize;
16use std::collections::BTreeMap;
17use std::sync::Once;
18
19pub const WINDOW: usize = 15;
20pub const LAYERS: usize = 6;
21pub const MIN_F32_PRED: f32 = 1.0e-46;
22static LOG_ONCE: Once = Once::new();
23pub static SEMI_JSON_2_0: &str = include_str!("../../models/2.0_semi_torch.json");
25pub static SEMI_JSON_2_2: &str = include_str!("../../models/2.2_semi_torch.json");
26pub static SEMI_JSON_3_2: &str = include_str!("../../models/3.2_semi_torch.json");
27pub static SEMI_JSON_REVIO: &str = include_str!("../../models/Revio_semi_torch.json");
28
29#[derive(Debug, Deserialize)]
30pub struct PrecisionTable {
31 pub columns: Vec<String>,
32 pub data: Vec<(f32, u8)>,
33}
34
35#[derive(Debug, Clone)]
36pub struct PredictOptions<B>
37where
38 B: Backend<Device = m6a_burn::BurnDevice>,
39{
40 pub keep: bool,
41 pub min_ml_score: Option<u8>,
42 pub all_calls: bool,
43 pub polymerase: PbChem,
44 pub batch_size: usize,
45 map: BTreeMap<OrderedFloat<f32>, u8>,
46 pub model: Vec<u8>,
47 pub min_ml: u8,
48 pub nuc_opts: cli::NucleosomeParameters,
49 pub burn_models: m6a_burn::BurnModels<B>,
50 pub fake: bool,
51}
52
53impl<B> PredictOptions<B>
54where
55 B: Backend<Device = m6a_burn::BurnDevice>,
56{
57 #[allow(clippy::too_many_arguments)]
58 #[allow(clippy::too_many_arguments)]
59 pub fn new(
60 keep: bool,
61 min_ml_score: Option<u8>,
62 all_calls: bool,
63 polymerase: PbChem,
64 batch_size: usize,
65 nuc_opts: cli::NucleosomeParameters,
66 fake: bool,
67 ) -> Self {
68 let mut map = BTreeMap::new();
70 map.insert(OrderedFloat(0.0), 0);
71
72 let mut options = PredictOptions {
74 keep,
75 min_ml_score,
76 all_calls,
77 polymerase: polymerase.clone(),
78 batch_size,
79 map,
80 model: vec![],
81 min_ml: 0,
82 nuc_opts,
83 burn_models: m6a_burn::BurnModels::new(&polymerase),
84 fake,
85 };
86 options.add_model().expect("Error loading model");
87 options
88 }
89
90 fn get_precision_table_and_ml(&self) -> Result<(Option<PrecisionTable>, u8)> {
91 let mut precision_json = "".to_string();
92 let min_ml = if let Ok(_file) = std::env::var("FT_MODEL") {
93 244
94 } else {
95 LOG_ONCE.call_once(|| {
96 log::info!("Using semi-supervised CNN m6A model.");
97 });
98 match self.polymerase {
99 PbChem::Two => {
100 precision_json = SEMI_JSON_2_0.to_string();
101 230
102 }
103 PbChem::TwoPointTwo => {
104 precision_json = SEMI_JSON_2_2.to_string();
105 244
106 }
107 PbChem::ThreePointTwo => {
108 precision_json = SEMI_JSON_3_2.to_string();
109 244
110 }
111 PbChem::Revio => {
112 precision_json = SEMI_JSON_REVIO.to_string();
113 254
114 }
115 }
116 };
117
118 if let Ok(json) = std::env::var("FT_JSON") {
120 log::info!("Loading precision table from environment variable.");
121 precision_json =
122 std::fs::read_to_string(json).expect("Unable to read file specified by FT_JSON");
123 }
124
125 let precision_table: Option<PrecisionTable> = Some(
127 serde_json::from_str(&precision_json)
128 .expect("Precision table JSON was not well-formatted"),
129 );
130
131 let final_min_ml = match self.min_ml_score {
133 Some(x) => {
134 log::info!("Using provided minimum ML tag score: {x}");
135 x
136 }
137 None => min_ml,
138 };
139 Ok((precision_table, final_min_ml))
140 }
141
142 fn add_model(&mut self) -> Result<()> {
143 self.model = vec![];
144
145 let (precision_table, min_ml) = self.get_precision_table_and_ml()?;
146
147 if let Some(precision_table) = precision_table {
149 for (cnn_score, precision) in precision_table.data {
150 self.map.insert(OrderedFloat(cnn_score), precision);
151 }
152 }
153
154 self.min_ml = min_ml;
155 Ok(())
156 }
157
158 pub fn progress_style(&self) -> &str {
159 "[PREDICTING m6A] [Elapsed {elapsed:.yellow} ETA {eta:.yellow}] {bar:30.cyan/blue} {human_pos:>5.cyan}/{human_len:.blue} (batches/s {per_sec:.green})"
161 }
162
163 pub fn precision_from_float(&self, value: f32) -> u8 {
165 let key = OrderedFloat(value);
166 let (less_key, less_val) = self
168 .map
169 .range(..key)
170 .next_back()
171 .unwrap_or((&OrderedFloat(0.0), &0));
172 let (more_key, more_val) = self
174 .map
175 .range(key..)
176 .next()
177 .unwrap_or((&OrderedFloat(1.0), &255));
178 if (more_key - key).abs() < (less_key - key).abs() {
179 *more_val
180 } else {
181 *less_val
182 }
183 }
184
185 pub fn min_ml_value(&self) -> u8 {
186 if self.all_calls {
187 0
188 } else {
189 self.min_ml
190 }
191 }
192
193 pub fn float_to_u8(&self, x: f32) -> u8 {
194 self.precision_from_float(x)
195 }
196
197 pub fn predict_m6a_on_records(
199 opts: &Self,
200 records: Vec<&mut rust_htslib::bam::Record>,
201 ) -> usize {
203 let data: Vec<Option<(DataWidows, DataWidows)>> = records
205 .iter()
206 .map(|rec| get_m6a_data_windows(rec))
207 .collect();
208 let mut all_ml_data = vec![];
210 let mut all_count = 0;
211 data.iter().flatten().for_each(|(a, t)| {
212 all_ml_data.extend(a.windows.clone());
213 all_count += a.count;
214 all_ml_data.extend(t.windows.clone());
215 all_count += t.count;
216 });
217 let predictions = opts.apply_model(&all_ml_data, all_count);
218 assert_eq!(predictions.len(), all_count);
219 assert_eq!(data.len(), records.len());
221 let mut cur_predict_st = 0;
222 for (option_data, record) in data.iter().zip(records) {
223 let mut annot = ma_io::read_record(record).unwrap_or_else(|e| {
226 log::warn!(
227 "read_record failed for {:?}: {e}",
228 String::from_utf8_lossy(record.qname())
229 );
230 molecular_annotation::MolecularAnnotations::from_record(record)
231 });
232 annot
233 .annotation_types
234 .retain(|t| t.name != basemods::M6A_TYPE);
235
236 let (a_data, t_data) = match option_data {
238 Some((a_data, t_data)) => (a_data, t_data),
239 None => continue,
240 };
241 let mut m6a_calls: Vec<(u32, u8, &'static str, molecular_annotation::Strand)> =
249 Vec::new();
250 for (data, base) in [(a_data, b'A'), (t_data, b'T')] {
251 let cur_predict_en = cur_predict_st + data.count;
252 let cur_predictions = &predictions[cur_predict_st..cur_predict_en];
253 cur_predict_st += data.count;
254 let (poss, quals) =
255 opts.basemod_from_ml(record, cur_predictions, &data.positions, &data.base_mod);
256 let (skip_base, strand) = basemods::canonical_basemod(basemods::M6A_TYPE, base)
257 .expect("A/T are canonical m6a bases");
258 m6a_calls.extend(
259 poss.into_iter()
260 .zip(quals)
261 .map(|(p, q)| (p, q, skip_base, strand)),
262 );
263 }
264 if !m6a_calls.is_empty() {
265 m6a_calls.sort_by_key(|&(p, _, _, _)| p);
266 let qspec = "Q"
267 .parse::<molecular_annotation::QualitySpec>()
268 .expect("Q parses");
269 let t = annot.add_annotation_type(
270 basemods::M6A_TYPE,
271 qspec,
272 molecular_annotation::Encoding::mm_ml(),
273 );
274 for (pos, qual, skip_base, strand) in &m6a_calls {
275 t.add(*pos, 1, *strand, vec![*qual], Some(skip_base.to_string()));
276 }
277 }
278
279 let modified_bases_forward: Vec<i64> =
281 m6a_calls.iter().map(|&(p, _, _, _)| p as i64).collect();
282 nucleosome::add_nucleosomes_to_annotations(
283 record,
284 &mut annot,
285 &modified_bases_forward,
286 &opts.nuc_opts,
287 );
288
289 ma_io::write_record_with_basemods(record, &annot);
290
291 if !opts.keep {
293 record.remove_aux(b"fp").unwrap_or(());
294 record.remove_aux(b"fi").unwrap_or(());
295 record.remove_aux(b"rp").unwrap_or(());
296 record.remove_aux(b"ri").unwrap_or(());
297 }
298 }
299 assert_eq!(cur_predict_st, predictions.len());
300 data.iter().flatten().count()
301 }
302
303 pub fn basemod_from_ml(
308 &self,
309 record: &mut bam::Record,
310 predictions: &[f32],
311 positions: &[usize],
312 _base_mod: &str,
313 ) -> (Vec<u32>, Vec<u8>) {
314 let min_pos = (WINDOW / 2) as i64;
316 let max_pos = (record.seq_len() - WINDOW / 2) as i64;
317
318 let mut poss: Vec<u32> = Vec::new();
319 let mut quals: Vec<u8> = Vec::new();
320 let mut low_nonzero = 0usize;
321 let mut zero = 0usize;
322 for (&pred, &pos) in predictions.iter().zip(positions.iter()) {
323 let ml = self.float_to_u8(pred);
324 let pos_i = pos as i64;
325 if pred > 0.0 && pred <= 1.0 / 255.0 {
326 low_nonzero += 1;
327 }
328 if pred <= 0.0 && pred > -0.00000001 {
329 zero += 1;
330 }
331 if ml >= self.min_ml_value() && pos_i >= min_pos && pos_i < max_pos {
332 poss.push(pos as u32);
333 quals.push(ml);
334 }
335 }
336
337 log::debug!(
338 "Low but non zero values: {:?}\tZero values: {:?}\tlength:{:?}",
339 low_nonzero,
340 zero,
341 predictions.len()
342 );
343
344 (poss, quals)
345 }
346
347 pub fn apply_model(&self, windows: &[f32], count: usize) -> Vec<f32> {
348 self.burn_models.forward(self, windows, count)
349 }
350
351 fn _fake_apply_model(&self, _: &[f32], count: usize) -> Vec<f32> {
352 vec![0.0; count]
353 }
354}
355
356pub fn hot_one_dna(seq: &[u8]) -> Vec<f32> {
369 let len = seq.len() * 4;
370 let mut out = vec![0.0; len];
371 for (row, base) in [b'A', b'C', b'G', b'T'].into_iter().enumerate() {
372 let already_done = seq.len() * row;
373 for i in 0..seq.len() {
374 if seq[i] == base {
375 out[already_done + i] = 1.0;
376 }
377 }
378 }
379 out
380}
381
382struct DataWidows {
383 pub windows: Vec<f32>,
384 pub positions: Vec<usize>,
385 pub count: usize,
386 pub base_mod: String,
387}
388
389fn get_m6a_data_windows(record: &bam::Record) -> Option<(DataWidows, DataWidows)> {
390 if record.is_secondary() {
392 log::warn!(
393 "Skipping secondary alignment of {}",
394 String::from_utf8_lossy(record.qname())
395 );
396 return None;
397 }
398
399 let extend = WINDOW / 2;
400 let mut f_ip = bio_io::get_u8_tag(record, b"fi");
401 let r_ip;
402 let f_pw;
403 let r_pw;
404 if f_ip.is_empty() {
406 f_ip = bio_io::get_pb_u16_tag_as_u8(record, b"fi");
407 if f_ip.is_empty() {
408 r_ip = vec![];
410 f_pw = vec![];
411 r_pw = vec![];
412 } else {
413 r_ip = bio_io::get_pb_u16_tag_as_u8(record, b"ri");
414 f_pw = bio_io::get_pb_u16_tag_as_u8(record, b"fp");
415 r_pw = bio_io::get_pb_u16_tag_as_u8(record, b"rp");
416 }
417 } else {
418 r_ip = bio_io::get_u8_tag(record, b"ri");
419 f_pw = bio_io::get_u8_tag(record, b"fp");
420 r_pw = bio_io::get_u8_tag(record, b"rp");
421 }
422 if f_ip.is_empty() || r_ip.is_empty() || f_pw.is_empty() || r_pw.is_empty() {
424 log::debug!(
425 "Hifi kinetics are missing for: {}",
426 String::from_utf8_lossy(record.qname())
427 );
428 return None;
429 }
430 let r_ip = r_ip.into_iter().rev().collect::<Vec<_>>();
432 let r_pw = r_pw.into_iter().rev().collect::<Vec<_>>();
433
434 let mut seq = record.seq().as_bytes();
435 if record.is_reverse() {
436 seq = revcomp(seq);
437 }
438
439 assert_eq!(f_ip.len(), seq.len());
440 let mut a_count = 0;
441 let mut t_count = 0;
442 let mut a_windows = vec![];
443 let mut t_windows = vec![];
444 let mut a_positions = vec![];
445 let mut t_positions = vec![];
446 for (pos, base) in seq.iter().enumerate() {
447 if !((*base == b'A') || (*base == b'T')) {
448 continue;
449 }
450 let data_window = if (pos < extend) || (pos + extend + 1 > record.seq_len()) {
452 vec![0.0; WINDOW * LAYERS]
454 } else {
455 let start = pos - extend;
456 let end = pos + extend + 1;
457 let ip: Vec<f32>;
458 let pw: Vec<f32>;
459 let hot_one;
460 if *base == b'A' {
461 let w_seq = &revcomp(&seq[start..end]);
462 hot_one = hot_one_dna(w_seq);
463 ip = (r_ip[start..end])
464 .iter()
465 .copied()
466 .rev()
467 .map(|x| x as f32 / 255.0)
468 .collect();
469 pw = (r_pw[start..end])
470 .iter()
471 .copied()
472 .rev()
473 .map(|x| x as f32 / 255.0)
474 .collect();
475 } else {
476 let w_seq = &seq[start..end];
477 hot_one = hot_one_dna(w_seq);
478 ip = (f_ip[start..end])
479 .iter()
480 .copied()
481 .map(|x| x as f32 / 255.0)
482 .collect();
483 pw = (f_pw[start..end])
484 .iter()
485 .copied()
486 .map(|x| x as f32 / 255.0)
487 .collect();
488 }
489 let mut data_window = vec![];
490 data_window.extend(hot_one);
491 data_window.extend(ip);
492 data_window.extend(pw);
493 data_window
494 };
495
496 if *base == b'A' {
498 a_windows.extend(data_window);
499 a_count += 1;
500 a_positions.push(pos);
501 } else {
502 t_windows.extend(data_window);
503 t_count += 1;
504 t_positions.push(pos);
505 }
506 }
507 let a_data = DataWidows {
508 windows: a_windows,
509 positions: a_positions,
510 count: a_count,
511 base_mod: "A+a".to_string(),
512 };
513 let t_data = DataWidows {
514 windows: t_windows,
515 positions: t_positions,
516 count: t_count,
517 base_mod: "T-a".to_string(),
518 };
519 Some((a_data, t_data))
520}
521
522pub fn read_bam_into_fiberdata(opts: &mut PredictM6AOptions) {
523 let mut bam = opts.input.bam_reader();
524 let mut out = opts.input.bam_writer(&opts.out);
525 let header = bam::Header::from_template(bam.header());
526 log::info!(
528 "{} reads included at once in batch prediction.",
529 opts.batch_size
530 );
531
532 #[cfg(feature = "tch")]
533 type MlBackend = burn::backend::LibTorch;
534 #[cfg(feature = "tch")]
535 log::info!("Using LibTorch for ML backend.");
536
537 #[cfg(not(feature = "tch"))]
538 type MlBackend = burn::backend::Candle;
539 #[cfg(not(feature = "tch"))]
540 log::info!("Using Candle for ML backend.");
541
542 let predict_options: PredictOptions<MlBackend> = PredictOptions::new(
544 opts.keep,
545 opts.force_min_ml_score,
546 opts.all_calls,
547 find_pb_polymerase(&header),
548 opts.batch_size,
549 opts.nuc.clone(),
550 opts.fake,
551 );
552 let fire_opts = crate::cli::FireOptions::default();
554 let (model, precision_table) = crate::utils::fire::get_model(&fire_opts);
555
556 let bam_chunk_iter = BamChunk::new(bam.records(), None);
558 for mut chunk in bam_chunk_iter {
560 let number_of_reads_with_predictions = chunk
563 .par_iter_mut()
564 .chunks(predict_options.batch_size)
565 .map(|records| {
566 let thread_opts = PredictOptions::<MlBackend>::new(
568 predict_options.all_calls,
569 predict_options.min_ml_score,
570 predict_options.all_calls,
571 predict_options.polymerase.clone(),
572 predict_options.batch_size,
573 predict_options.nuc_opts.clone(),
574 predict_options.fake,
575 );
576 PredictOptions::predict_m6a_on_records(&thread_opts, records)
577 })
578 .sum::<usize>() as f32;
579
580 let frac_called = number_of_reads_with_predictions / chunk.len() as f32;
581 if frac_called < 0.05 {
582 log::warn!("More than 5% ({:.2}%) of reads were not predicted on. Are HiFi kinetics missing from this file? Enable Debug logging level to show which reads lack kinetics.", 100.0-100.0*frac_called);
583 }
584
585 let mut fd_recs =
587 FiberseqData::from_records(chunk, &opts.input.header_view(), &opts.input.filters);
588 fd_recs.par_iter_mut().for_each(|fd| {
589 crate::subcommands::fire::add_fire_to_rec(fd, &fire_opts, &model, &precision_table);
590 });
591
592 fd_recs.iter().for_each(|fd| out.write(&fd.record).unwrap());
594 }
595}
596
597#[cfg(test)]
599mod tests {
600 use super::*;
601 #[test]
602 fn test_precision_json_validity() {
603 for file in [SEMI_JSON_2_0, SEMI_JSON_2_2, SEMI_JSON_3_2, SEMI_JSON_REVIO] {
604 let _p: PrecisionTable =
605 serde_json::from_str(file).expect("Precision table JSON was not well-formatted");
606 }
607 }
608}