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: timsrust_tdf::TdfFrameReader,
ids: Vec<usize>,
}
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 s = p
.to_str()
.ok_or_else(|| FrameReaderError(format!("non UTF-8 path {}", p.display())))?;
let tdf = if p.is_dir() {
p.join("analysis.tdf")
} else {
p.to_path_buf()
};
let compression_type = read_compression_type(&tdf)?;
if compression_type != 2 {
return Err(FrameReaderError(format!(
"Compression type {compression_type} not understood"
)));
}
let inner = timsrust_tdf::TdfFrameReader::without_metadata(s, compression_type, 0)
.map_err(|e| FrameReaderError(e.to_string()))?;
let mut ids: Vec<usize> = inner.iter_indices().collect();
ids.sort_unstable();
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 ions = self
.inner
.get_ions(id)
.map_err(|e| FrameReaderError(e.to_string()))?;
Ok(Frame {
index: id,
scan_offsets: ions.scan_offsets().clone(),
tof_indices: ions.tof_indices().iter().map(|&t| u32::from(t)).collect(),
intensities: ions.intensities().iter().map(|&x| u32::from(x)).collect(),
})
}
}
fn read_compression_type(tdf: &Path) -> Result<u8, FrameReaderError> {
let err = |e: &dyn std::fmt::Display| FrameReaderError(e.to_string());
let conn =
Connection::open_with_flags(tdf, OpenFlags::SQLITE_OPEN_READ_ONLY).map_err(|e| err(&e))?;
let value: String = conn
.query_row(
"SELECT Value FROM GlobalMetadata WHERE Key='TimsCompressionType'",
[],
|r| r.get(0),
)
.map_err(|_| err(&"Key not found: TimsCompressionType"))?;
value
.parse()
.map_err(|_| err(&"Key not parsable: TimsCompressionType"))
}