use std::collections::HashMap;
use std::path::Path;
use rusqlite::{Connection, OpenFlags};
pub use dnoise_core::convert::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter};
#[derive(Debug, Clone, Copy)]
pub struct Metadata {
pub mz_converter: Tof2MzConverter,
pub im_converter: Scan2ImConverter,
}
#[derive(Debug, thiserror::Error)]
pub enum MetadataReaderError {
#[error("{0}")]
Sql(#[from] rusqlite::Error),
#[error("Key not found: {0}")]
KeyNotFound(String),
#[error("Key not parsable: {0}")]
ParseError(String),
#[error("Frames table is empty")]
NoFrames,
}
pub struct MetadataReader;
const OTOF_CONTROL: &str = "Bruker otofControl";
impl MetadataReader {
#[allow(clippy::new_ret_no_self)]
pub fn new(path: impl AsRef<Path>) -> Result<Metadata, MetadataReaderError> {
let path = path.as_ref();
let tdf = if path.is_dir() {
path.join("analysis.tdf")
} else {
path.to_path_buf()
};
let conn = Connection::open_with_flags(&tdf, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
let mut stmt = conn.prepare("SELECT Key, Value FROM GlobalMetadata")?;
let kv: HashMap<String, String> = stmt
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?
.collect::<Result<_, _>>()?;
fn parse<T: std::str::FromStr>(
kv: &HashMap<String, String>,
key: &str,
) -> Result<T, MetadataReaderError> {
kv.get(key)
.ok_or_else(|| MetadataReaderError::KeyNotFound(key.to_string()))?
.parse()
.map_err(|_| MetadataReaderError::ParseError(key.to_string()))
}
let _compression_type: u8 = parse(&kv, "TimsCompressionType")?;
let software = kv
.get("AcquisitionSoftware")
.ok_or_else(|| MetadataReaderError::KeyNotFound("AcquisitionSoftware".into()))?;
let mut mz_min: f64 = parse(&kv, "MzAcqRangeLower")?;
let mut mz_max: f64 = parse(&kv, "MzAcqRangeUpper")?;
if software == OTOF_CONTROL {
mz_min -= 5.0;
mz_max += 5.0;
}
let tof_max_index: u32 = parse(&kv, "DigitizerNumSamples")?;
let im_min: f64 = parse(&kv, "OneOverK0AcqRangeLower")?;
let im_max: f64 = parse(&kv, "OneOverK0AcqRangeUpper")?;
let scan_max: Option<i64> =
conn.query_row("SELECT MAX(NumScans) FROM Frames", [], |r| r.get(0))?;
let scan_max = scan_max.ok_or(MetadataReaderError::NoFrames)?;
Ok(Metadata {
mz_converter: Tof2MzConverter::from_boundaries(mz_min, mz_max, tof_max_index),
im_converter: Scan2ImConverter::from_boundaries(im_min, im_max, scan_max as u32),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct Frame {
pub index: usize,
pub scan_offsets: Vec<usize>,
pub tof_indices: Vec<u32>,
pub intensities: Vec<u32>,
}
impl dnoise_core::frame::CsrFrame for Frame {
fn frame_id(&self) -> usize {
self.index
}
fn scan_offsets(&self) -> &[usize] {
&self.scan_offsets
}
fn tof_indices(&self) -> &[u32] {
&self.tof_indices
}
fn intensities(&self) -> &[u32] {
&self.intensities
}
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct FrameReaderError(String);
pub struct FrameReader {
inner: sage_plus_tdf::TdfReader,
ids: Vec<u64>,
}
impl std::fmt::Debug for FrameReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameReader")
.field("frames", &self.ids.len())
.finish()
}
}
impl FrameReader {
pub fn new(path: impl AsRef<Path>) -> Result<Self, FrameReaderError> {
let p = path.as_ref();
let dir = if p.is_dir() {
p
} else {
p.parent().unwrap_or(p)
};
let inner =
sage_plus_tdf::TdfReader::open(dir).map_err(|e| FrameReaderError(e.to_string()))?;
let compression_type = inner.compression_type();
if compression_type != 2 {
return Err(FrameReaderError(format!(
"Compression type {compression_type} not understood"
)));
}
let ids = inner.frames().map(|f| f.id).collect();
Ok(Self { inner, ids })
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn get(&self, i: usize) -> Result<Frame, FrameReaderError> {
let id = *self
.ids
.get(i)
.ok_or_else(|| FrameReaderError(format!("frame index {i} out of bounds")))?;
let f = self
.inner
.read_frame(id)
.map_err(|e| FrameReaderError(e.to_string()))?;
Ok(Frame {
index: id as usize,
scan_offsets: f.scan_offsets,
tof_indices: f.tof_indices,
intensities: f.intensities,
})
}
}