1use std::{collections::HashMap, fmt::Debug, str::FromStr, sync::Arc};
2
3use timsrust_core::{AcquisitionType, FrameIndex, Im, MSLevel, Mz, Rt};
4
5use crate::{Frame2RtConverter, TdfFrameReader};
6
7use super::{
8 TDFPathLike,
9 file_readers::sql_reader::{
10 ReadableSqlHashMap, SqlReader, SqlReaderError, metadata::SqlMetadata,
11 },
12};
13
14#[derive(Clone, Debug)]
16pub struct Metadata {
17 rt_converter: Arc<Frame2RtConverter>,
18 compression_type: u8,
19 acquisition_type: AcquisitionType,
20 lower_rt: Rt,
21 upper_rt: Rt,
22 lower_im: Im,
23 upper_im: Im,
24 lower_mz: Mz,
25 upper_mz: Mz,
26 path: String,
27 max_peaks_per_scan: usize,
28}
29
30const OTOF_CONTROL: &str = "Bruker otofControl";
31
32impl Metadata {
33 pub fn new(path: impl TDFPathLike) -> Result<Self, MetadataReaderError> {
34 let tdf_sql_reader = SqlReader::open(&path)?;
35 let sql_metadata: HashMap<String, String> =
36 SqlMetadata::from_sql_reader(&tdf_sql_reader)?;
37 let compression_type =
38 parse_value(&sql_metadata, "TimsCompressionType")?;
39 let max_peaks_per_scan =
40 parse_value(&sql_metadata, "MaxNumPeaksPerScan")?;
41 let frame_reader = TdfFrameReader::without_metadata(
42 path.as_ref(),
43 compression_type,
44 max_peaks_per_scan,
45 )
46 .unwrap();
47 let (mz_min, mz_max) = get_mz_bounds(&sql_metadata)?;
48 let (im_min, im_max) = get_im_bounds(&sql_metadata)?;
49 let rt_values = frame_reader
53 .iter_indices()
54 .map(|index| {
55 let frame =
56 frame_reader.get_partial_frame_without_ions(index).unwrap();
57 (
58 FrameIndex::try_from(frame.info().index() as u32)
59 .expect("FrameIndex conversion out of bounds"),
60 Rt::from(frame.info().rt_in_seconds()),
61 )
62 })
63 .collect::<HashMap<FrameIndex, Rt>>();
64 let rt_min = rt_values
65 .values()
66 .cloned()
67 .min_by(|a, b| a.partial_cmp(b).unwrap())
68 .unwrap();
69 let rt_max = rt_values
70 .values()
71 .cloned()
72 .max_by(|a, b| a.partial_cmp(b).unwrap())
73 .unwrap();
74 let mut acquisition_type = AcquisitionType::Unknown;
75 for index in frame_reader.iter_indices() {
76 let frame =
77 frame_reader.get_partial_frame_without_ions(index).unwrap();
78 let frame_info = frame.info();
79 if frame_info.ms_level() != MSLevel::MS1 {
80 acquisition_type = frame_info.acquisition_type();
81 break;
82 }
83 }
84 let metadata = Metadata {
85 rt_converter: Arc::new(Frame2RtConverter::from_values(rt_values)),
86 lower_rt: rt_min,
87 upper_rt: rt_max,
88 lower_im: im_min.into(),
89 upper_im: im_max.into(),
90 lower_mz: mz_min.into(),
91 upper_mz: mz_max.into(),
92 compression_type,
93 path: path.as_ref().to_string(),
94 max_peaks_per_scan,
95 acquisition_type,
96 };
97 Ok(metadata)
98 }
99
100 pub fn rt_converter(&self) -> &Arc<Frame2RtConverter> {
101 &self.rt_converter
102 }
103
104 pub fn max_peaks_per_scan(&self) -> usize {
105 self.max_peaks_per_scan
106 }
107
108 pub fn compression_type(&self) -> u8 {
109 self.compression_type
110 }
111
112 pub fn acquisition_type(&self) -> AcquisitionType {
113 self.acquisition_type
114 }
115
116 pub fn lower_rt(&self) -> Rt {
117 self.lower_rt
118 }
119
120 pub fn upper_rt(&self) -> Rt {
121 self.upper_rt
122 }
123
124 pub fn lower_im(&self) -> Im {
125 self.lower_im
126 }
127
128 pub fn upper_im(&self) -> Im {
129 self.upper_im
130 }
131
132 pub fn lower_mz(&self) -> Mz {
133 self.lower_mz
134 }
135
136 pub fn upper_mz(&self) -> Mz {
137 self.upper_mz
138 }
139
140 pub fn path(&self) -> &str {
141 &self.path
142 }
143}
144
145fn get_mz_bounds(
146 sql_metadata: &HashMap<String, String>,
147) -> Result<(f64, f64), MetadataReaderError> {
148 let software = sql_metadata.get("AcquisitionSoftware").ok_or(
149 MetadataReaderError::KeyNotFound("AcquisitionSoftware".to_string()),
150 )?;
151 let mut mz_min: f64 = parse_value(sql_metadata, "MzAcqRangeLower")?;
152 let mut mz_max: f64 = parse_value(sql_metadata, "MzAcqRangeUpper")?;
153 if software == OTOF_CONTROL {
154 mz_min -= 5.0;
155 mz_max += 5.0;
156 }
157 Ok((mz_min, mz_max))
158}
159
160fn get_im_bounds(
161 sql_metadata: &HashMap<String, String>,
162) -> Result<(f64, f64), MetadataReaderError> {
163 let im_min: f64 = parse_value(sql_metadata, "OneOverK0AcqRangeLower")?;
164 let im_max: f64 = parse_value(sql_metadata, "OneOverK0AcqRangeUpper")?;
165 Ok((im_min, im_max))
166}
167
168fn parse_value<T: FromStr>(
169 hash_map: &HashMap<String, String>,
170 key: &str,
171) -> Result<T, MetadataReaderError> {
172 let value: T = hash_map
173 .get(key)
174 .ok_or(MetadataReaderError::KeyNotFound(key.to_string()))?
175 .parse()
176 .map_err(|_| MetadataReaderError::ParseError(key.to_string()))?;
177 Ok(value)
178}
179
180#[allow(private_interfaces)]
181#[derive(Debug, thiserror::Error)]
182pub enum MetadataReaderError {
183 #[error("{0}")]
184 SqlReaderError(#[from] SqlReaderError),
185 #[error("Key not found: {0}")]
186 KeyNotFound(String),
187 #[error("Key not parsable: {0}")]
188 ParseError(String),
189}