mzdeisotope_map/
feature_fit.rs1use std::cmp::Ordering;
3
4use chemical_elements::isotopic_pattern::TheoreticalIsotopicPattern;
5
6use mzsignal::smooth::moving_average;
7use mzdeisotope::scorer::ScoreType;
8use mzpeaks::{feature::TimeInterval, feature_map::FeatureMapLike, CoordinateRange};
9
10use crate::traits::FeatureMapType;
11
12#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
13pub struct MapCoordinate {
14 pub coord: f64,
15 pub time: f64,
16}
17
18impl MapCoordinate {
19 pub fn new(coord: f64, time: f64) -> Self {
20 Self { coord, time }
21 }
22}
23
24#[derive(Debug, Clone)]
25pub struct FeatureSetFit {
26 pub features: Vec<Option<usize>>,
27 pub start: MapCoordinate,
28 pub end: MapCoordinate,
29 pub score: ScoreType,
30 pub theoretical: TheoreticalIsotopicPattern,
31 pub mz: f64,
32 pub neutral_mass: f64,
33 pub charge: i32,
34 pub missing_features: usize,
35 pub n_points: usize,
36 pub monoisotopic_index: usize,
37 pub scores: Vec<ScoreType>,
38 pub times: Vec<f64>,
39}
40
41impl PartialOrd for FeatureSetFit {
42 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43 Some(self.cmp(other))
44 }
45}
46
47impl Ord for FeatureSetFit {
48 fn cmp(&self, other: &Self) -> Ordering {
49 self.score.total_cmp(&other.score)
50 }
51}
52
53impl PartialEq for FeatureSetFit {
54 fn eq(&self, other: &Self) -> bool {
55 self.features == other.features
56 && self.score == other.score
57 && self.theoretical == other.theoretical
58 && self.charge == other.charge
59 }
60}
61
62impl Eq for FeatureSetFit {}
63
64impl FeatureSetFit {
65 #[allow(clippy::too_many_arguments)]
66 pub fn new(
67 features: Vec<Option<usize>>,
68 theoretical: TheoreticalIsotopicPattern,
69 start: MapCoordinate,
70 end: MapCoordinate,
71 score: ScoreType,
72 charge: i32,
73 missing_features: usize,
74 neutral_mass: f64,
75 n_points: usize,
76 scores: Vec<ScoreType>,
77 times: Vec<f64>,
78 ) -> Self {
79 let mz = theoretical.peaks.first().map(|p| p.mz).unwrap();
80 Self {
81 features,
82 theoretical,
83 start,
84 end,
85 score,
86 charge,
87 missing_features,
88 monoisotopic_index: 0,
89 mz,
90 neutral_mass,
91 n_points,
92 scores,
93 times,
94 }
95 }
96
97 pub fn count_null_features(&self) -> usize {
98 self.features.iter().map(|f| f.is_none() as usize).sum()
99 }
100
101 pub fn has_multiple_real_features(&self) -> bool {
102 (self.features.len() - self.count_null_features()) > 1
103 }
104
105 pub fn len(&self) -> usize {
106 self.features.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
110 self.features.is_empty()
111 }
112
113 pub fn find_bounds<Y: Clone>(&self, feature_map: &FeatureMapType<Y>, detection_threshold: f32) -> CoordinateRange<Y> {
114 let mut start_time: f64 = f64::INFINITY;
115 let mut end_time: f64 = f64::INFINITY;
116
117 for (f, p) in self.features.iter().zip(self.theoretical.iter()) {
118 if let Some(i) = f {
119 let f = feature_map.get_item(*i);
120 if p.intensity() >= detection_threshold {
121 start_time = f.start_time().unwrap().min(start_time);
122 end_time = f.end_time().unwrap().min(end_time);
123 }
124
125 } else {
126 continue;
127 }
128 }
129
130 CoordinateRange::new(Some(start_time), Some(end_time))
131 }
132
133 pub fn find_separation<Y: Clone>(&self, feature_map: &FeatureMapType<Y>, detection_threshold: f32) -> (CoordinateRange<Y>, Vec<ScoreSegment>) {
134 let mut time_range = self.find_bounds(feature_map, detection_threshold);
135 if self.n_points > 0 {
136 let mut segments = Vec::new();
137 let mut last_score = ScoreType::INFINITY;
138 let mut scores = Vec::with_capacity(self.scores.len());
139 scores.resize(self.scores.len(), 0.0);
140 moving_average::<f32, 1>(&self.scores, &mut scores);
141 let mut begin_i = 0;
142 for (i, score) in scores.iter().copied().enumerate() {
143 if score > 0.0 && last_score < 0.0 {
144 begin_i = i
145 } else if score < 0.0 && last_score > 0.0 {
146 let end_i = i;
147 segments.push(ScoreSegment::new(
148 begin_i, end_i,
149 scores[begin_i..end_i].iter().copied().sum::<f32>()
150 ));
151 begin_i = i;
152 }
153 last_score = score;
154 }
155 let end_i = scores.len().saturating_sub(1);
156 segments.push(ScoreSegment::new(
157 begin_i,
158 end_i,
159 scores[begin_i..end_i].iter().sum()
160 ));
161 let segment = segments.iter().max_by(|a, b| {a.score.total_cmp(&b.score)}).copied().unwrap();
162 if time_range.start.unwrap() < self.times[segment.start] && segment.start != 0 {
163 time_range.start = Some(self.times[segment.start]);
164 }
165 if time_range.end.unwrap() > self.times[segment.end] {
166 time_range.end = Some(self.times[segment.end])
167 }
168 (time_range, segments)
169 } else {
170 (time_range, Vec::new())
171 }
172 }
173}
174
175
176#[derive(Debug, Default, Clone, Copy, PartialEq)]
177pub struct ScoreSegment {
178 pub start: usize,
179 pub end: usize,
180 pub score: f32
181}
182
183impl ScoreSegment {
184 pub fn new(start: usize, end: usize, score: f32) -> Self {
185 Self { start, end, score }
186 }
187}