pod5.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. use std::{
  2. collections::HashSet,
  3. fmt, fs,
  4. path::{Path, PathBuf},
  5. };
  6. use chrono::{DateTime, Utc};
  7. use serde::{Deserialize, Serialize};
  8. use crate::{
  9. collection::flowcells::IdInput,
  10. helpers::{human_size, list_files_with_ext},
  11. io::pod5_infos::Pod5Info,
  12. };
  13. #[derive(Debug, Clone, Serialize, Deserialize)]
  14. pub struct Pod5 {
  15. pub name: String,
  16. pub file_size: u64,
  17. pub path: PathBuf,
  18. pub acquisition_id: String,
  19. pub acquisition_start_time: DateTime<Utc>,
  20. pub adc_max: i16,
  21. pub adc_min: i16,
  22. pub experiment_name: String,
  23. pub flow_cell_id: String,
  24. pub flow_cell_product_code: String,
  25. pub protocol_name: String,
  26. pub protocol_run_id: String,
  27. pub protocol_start_time: DateTime<Utc>,
  28. pub sample_id: String,
  29. pub sample_rate: u16,
  30. pub sequencing_kit: String,
  31. pub sequencer_position: String,
  32. pub sequencer_position_type: String,
  33. pub software: String,
  34. pub system_name: String,
  35. pub system_type: String,
  36. }
  37. impl fmt::Display for Pod5 {
  38. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  39. writeln!(f, "📁 {}", self.name)?;
  40. writeln!(f, " size : {} bytes", self.file_size)?;
  41. writeln!(f, " path : {}", self.path.display())?;
  42. writeln!(f, " experiment: {}", self.experiment_name)?;
  43. writeln!(
  44. f,
  45. " flow cell : {} ({})",
  46. self.flow_cell_id, self.flow_cell_product_code
  47. )?;
  48. writeln!(f, "🧪 Sample")?;
  49. writeln!(f, " id : {}", self.sample_id)?;
  50. writeln!(f, " kit : {}", self.sequencing_kit)?;
  51. writeln!(f, " rate : {} Hz", self.sample_rate)?;
  52. writeln!(f, "🔬 Acquisition")?;
  53. writeln!(f, " id : {}", self.acquisition_id)?;
  54. writeln!(f, " start : {}", self.acquisition_start_time)?;
  55. writeln!(f, " ADC : [{} .. {}]", self.adc_min, self.adc_max)?;
  56. writeln!(f, "⚙️ Protocol")?;
  57. writeln!(f, " name : {}", self.protocol_name)?;
  58. writeln!(f, " run id : {}", self.protocol_run_id)?;
  59. writeln!(f, " started : {}", self.protocol_start_time)?;
  60. writeln!(f, "🖥️ System")?;
  61. writeln!(f, " {} / {}", self.system_name, self.system_type)?;
  62. writeln!(f, " software : {}", self.software)
  63. }
  64. }
  65. impl Pod5 {
  66. /// Construct a `Pod5` from a filesystem path.
  67. ///
  68. /// This loads the metadata using `Pod5Info::from_pod5` and fills the
  69. /// corresponding fields in `Pod5`.
  70. pub fn from_path<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
  71. let path_ref = path.as_ref();
  72. // Convert path to string, returning an error if it contains invalid UTF-8
  73. let path_str = path_ref.to_str().ok_or_else(|| {
  74. anyhow::anyhow!("Path contains invalid UTF-8: {}", path_ref.display())
  75. })?;
  76. // Pod5Info::from_pod5 now returns Result
  77. let info = Pod5Info::from_pod5(path_str)?;
  78. let file_size = std::fs::metadata(path_ref)
  79. .map_err(|e| {
  80. anyhow::anyhow!(
  81. "Failed to read metadata for '{}': {}",
  82. path_ref.display(),
  83. e
  84. )
  85. })?
  86. .len();
  87. Ok(Self {
  88. name: path_ref
  89. .file_name()
  90. .and_then(|s| s.to_str())
  91. .unwrap_or("")
  92. .to_string(),
  93. file_size,
  94. path: PathBuf::from(path_ref),
  95. acquisition_id: info.acquisition_id,
  96. acquisition_start_time: info.acquisition_start_time,
  97. adc_max: info.adc_max,
  98. adc_min: info.adc_min,
  99. experiment_name: info.experiment_name,
  100. flow_cell_id: info.flow_cell_id,
  101. flow_cell_product_code: info.flow_cell_product_code,
  102. protocol_name: info.protocol_name,
  103. protocol_run_id: info.protocol_run_id,
  104. protocol_start_time: info.protocol_start_time,
  105. sample_id: info.sample_id,
  106. sample_rate: info.sample_rate,
  107. sequencing_kit: info.sequencing_kit,
  108. sequencer_position: info.sequencer_position,
  109. sequencer_position_type: info.sequencer_position_type,
  110. software: info.software,
  111. system_name: info.system_name,
  112. system_type: info.system_type,
  113. })
  114. }
  115. }
  116. #[derive(Debug, Serialize, Deserialize, Clone)]
  117. pub struct Pod5sRun {
  118. pub run_id: String,
  119. pub flow_cell_id: String,
  120. pub sequencing_kit: String,
  121. pub cases: Vec<IdInput>,
  122. pub pod5s: Vec<Pod5>,
  123. pub bams_pass: Option<PathBuf>,
  124. }
  125. impl fmt::Display for Pod5sRun {
  126. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  127. writeln!(f, "🚀 Run {}", self.run_id)?;
  128. writeln!(f, " Flow cell : {}", self.flow_cell_id)?;
  129. writeln!(f, " Sequencing kit : {}", self.sequencing_kit)?;
  130. writeln!(f, " Cases : {}", self.cases.len())?;
  131. for c in &self.cases {
  132. writeln!(f, " • {}", c)?;
  133. }
  134. writeln!(
  135. f,
  136. " Pod5 files : {} (showing 0 details)",
  137. self.pod5s.len()
  138. )?;
  139. if let Some(ref bam) = self.bams_pass {
  140. writeln!(f, " BAM pass : {}", bam.display())?;
  141. }
  142. Ok(())
  143. }
  144. }
  145. impl Pod5sRun {
  146. /// Load all `.pod5` files from a directory and build a collection.
  147. pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
  148. Self::load_from_dirs(std::iter::once(dir))
  149. }
  150. /// Load all `.pod5` files from multiple directories and build a collection.
  151. ///
  152. /// All files must share the same `run_id`, `flow_cell_id` and `sequencing_kit`.
  153. pub fn load_from_dirs<P, I>(dirs: I) -> anyhow::Result<Self>
  154. where
  155. P: AsRef<Path>,
  156. I: IntoIterator<Item = P>,
  157. {
  158. let mut pod5s = Vec::new();
  159. let mut flow_cell_id: Option<String> = None;
  160. let mut sequencing_kit: Option<String> = None;
  161. let mut run_id: Option<String> = None;
  162. let mut skipped = 0usize;
  163. let mut any_pod_files = false;
  164. for dir in dirs {
  165. let dir = dir.as_ref();
  166. let pod_paths = list_files_with_ext(dir, "pod5")?;
  167. if pod_paths.is_empty() {
  168. log::debug!("No .pod5 files found in directory: {}", dir.display());
  169. continue;
  170. }
  171. any_pod_files = true;
  172. for p in pod_paths.iter() {
  173. let pod = match Pod5::from_path(p) {
  174. Ok(pod) => pod,
  175. Err(e) => {
  176. log::debug!("Skipping corrupted POD5 file '{}': {}", p.display(), e);
  177. skipped += 1;
  178. continue;
  179. }
  180. };
  181. // run_id uniqueness check
  182. match &run_id {
  183. None => run_id = Some(pod.protocol_run_id.clone()),
  184. Some(exp) if &pod.protocol_run_id != exp => {
  185. anyhow::bail!(
  186. "Mixed run IDs: expected '{}', found '{}' (file: {})",
  187. exp,
  188. pod.protocol_run_id,
  189. pod.path.display()
  190. );
  191. }
  192. _ => {}
  193. }
  194. // flow_cell_id uniqueness check
  195. match &flow_cell_id {
  196. None => flow_cell_id = Some(pod.flow_cell_id.clone()),
  197. Some(exp) if &pod.flow_cell_id != exp => {
  198. anyhow::bail!(
  199. "Mixed flow cells: expected '{}', found '{}' (file: {})",
  200. exp,
  201. pod.flow_cell_id,
  202. pod.path.display()
  203. );
  204. }
  205. _ => {}
  206. }
  207. // sequencing_kit uniqueness check
  208. match &sequencing_kit {
  209. None => sequencing_kit = Some(pod.sequencing_kit.clone()),
  210. Some(exp) if &pod.sequencing_kit != exp => {
  211. anyhow::bail!(
  212. "Mixed sequencing kits: expected '{}', found '{}' (file: {})",
  213. exp,
  214. pod.sequencing_kit,
  215. pod.path.display()
  216. );
  217. }
  218. _ => {}
  219. }
  220. pod5s.push(pod);
  221. }
  222. }
  223. if !any_pod_files {
  224. anyhow::bail!("No .pod5 files found in any directory");
  225. }
  226. let run_id = run_id.ok_or_else(|| anyhow::anyhow!("No valid pod5 files loaded"))?;
  227. let flow_cell_id =
  228. flow_cell_id.ok_or_else(|| anyhow::anyhow!("No valid pod5 files loaded"))?;
  229. let sequencing_kit =
  230. sequencing_kit.ok_or_else(|| anyhow::anyhow!("No valid pod5 files loaded"))?;
  231. if skipped > 0 {
  232. log::debug!(
  233. "Skipped {} corrupted POD5 file(s) across directories",
  234. skipped
  235. );
  236. }
  237. Ok(Self {
  238. run_id,
  239. flow_cell_id,
  240. sequencing_kit,
  241. cases: Vec::new(),
  242. pod5s,
  243. bams_pass: None,
  244. })
  245. }
  246. pub fn add_id_input(&mut self, id_input: IdInput) {
  247. self.cases.push(id_input);
  248. }
  249. /// Compute summary statistics for the collection.
  250. pub fn stats(&self) -> Pod5sFlowCellStats {
  251. if self.pod5s.is_empty() {
  252. return Pod5sFlowCellStats {
  253. run_id: self.run_id.clone(),
  254. flow_cell_id: self.flow_cell_id.clone(),
  255. sequencing_kit: self.sequencing_kit.clone(),
  256. count: 0,
  257. total_size: 0,
  258. min_acq: None,
  259. max_acq: None,
  260. min_protocol: None,
  261. max_protocol: None,
  262. avg_sample_rate: None,
  263. };
  264. }
  265. let count = self.pod5s.len();
  266. let total_size = self.pod5s.iter().map(|p| p.file_size).sum();
  267. let (min_acq, max_acq) = self.pod5s.iter().map(|p| p.acquisition_start_time).fold(
  268. (
  269. self.pod5s[0].acquisition_start_time,
  270. self.pod5s[0].acquisition_start_time,
  271. ),
  272. |(minv, maxv), t| (minv.min(t), maxv.max(t)),
  273. );
  274. let (min_protocol, max_protocol) = self.pod5s.iter().map(|p| p.protocol_start_time).fold(
  275. (
  276. self.pod5s[0].protocol_start_time,
  277. self.pod5s[0].protocol_start_time,
  278. ),
  279. |(minv, maxv), t| (minv.min(t), maxv.max(t)),
  280. );
  281. let avg_sample_rate =
  282. Some(self.pod5s.iter().map(|p| p.sample_rate as f64).sum::<f64>() / count as f64);
  283. Pod5sFlowCellStats {
  284. run_id: self.run_id.clone(),
  285. flow_cell_id: self.flow_cell_id.clone(),
  286. sequencing_kit: self.sequencing_kit.clone(),
  287. count,
  288. total_size,
  289. min_acq: Some(min_acq),
  290. max_acq: Some(max_acq),
  291. min_protocol: Some(min_protocol),
  292. max_protocol: Some(max_protocol),
  293. avg_sample_rate,
  294. }
  295. }
  296. }
  297. #[derive(Debug, Clone)]
  298. pub struct Pod5sFlowCellStats {
  299. pub run_id: String,
  300. pub flow_cell_id: String,
  301. pub sequencing_kit: String,
  302. pub count: usize,
  303. pub total_size: u64,
  304. pub min_acq: Option<DateTime<Utc>>,
  305. pub max_acq: Option<DateTime<Utc>>,
  306. pub min_protocol: Option<DateTime<Utc>>,
  307. pub max_protocol: Option<DateTime<Utc>>,
  308. pub avg_sample_rate: Option<f64>,
  309. }
  310. impl fmt::Display for Pod5sFlowCellStats {
  311. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  312. writeln!(f, "Pod5 Flow Cell Stats")?;
  313. writeln!(f, "---------------------")?;
  314. writeln!(f, "Run ID: {}", self.run_id)?;
  315. writeln!(f, "Flow Cell ID: {}", self.flow_cell_id)?;
  316. writeln!(f, "Sequencing kit: {}", self.sequencing_kit)?;
  317. writeln!(f, "Count: {}", self.count)?;
  318. writeln!(
  319. f,
  320. "Total Size: {} ({} bytes)",
  321. human_size(self.total_size),
  322. self.total_size
  323. )?;
  324. if let Some(t) = self.min_acq {
  325. writeln!(f, "Acquisition Start (min): {}", t)?;
  326. }
  327. if let Some(t) = self.max_acq {
  328. writeln!(f, "Acquisition Start (max): {}", t)?;
  329. }
  330. if let Some(t) = self.min_protocol {
  331. writeln!(f, "Protocol Start (min): {}", t)?;
  332. }
  333. if let Some(t) = self.max_protocol {
  334. writeln!(f, "Protocol Start (max): {}", t)?;
  335. }
  336. if let Some(avg) = self.avg_sample_rate {
  337. writeln!(f, "Average Sample Rate: {:.2}", avg)?;
  338. }
  339. Ok(())
  340. }
  341. }
  342. #[derive(Debug, Default, Serialize, Deserialize, Clone)]
  343. pub struct Pod5sRuns {
  344. pub data: Vec<Pod5sRun>,
  345. }
  346. impl Pod5sRuns {
  347. pub fn new() -> Self {
  348. Self::default()
  349. }
  350. /// Add a new `Pod5sRun` by scanning a directory of `.pod5` files.
  351. ///
  352. /// - Builds a `Pod5sRun` via [`Pod5sRun::load_from_dir`].
  353. /// - If **no** existing run has the same `(run_id, flow_cell_id, sequencing_kit)`,
  354. /// the new run is appended to `data`.
  355. /// - If a run **already exists** with these three identifiers, its `pod5s` list is
  356. /// **merged** with the new one:
  357. /// - New `Pod5` entries are only added if their file name (from `pod.path.file_name()`)
  358. /// does **not** already exist in the run.
  359. /// - Duplicate file names are silently skipped (no error).
  360. /// Add a new `Pod5sRun` by scanning a directory of `.pod5` files.
  361. ///
  362. /// - Builds a `Pod5sRun` via [`Pod5sRun::load_from_dir`].
  363. /// - Optionally attaches a `bams_pass` directory to the run.
  364. /// - If **no** existing run has the same `(run_id, flow_cell_id, sequencing_kit)`,
  365. /// the new run is appended to `data`.
  366. /// - If a run **already exists** with these three identifiers:
  367. /// - `pod5s` are merged by file name (duplicates skipped).
  368. /// - `bams_pass`:
  369. /// * if existing has `Some` and new is `None` → keep existing.
  370. /// * if existing is `None` and new is `Some` → set existing to new.
  371. /// * if both `Some` but different → error (conflicting BAM-pass roots).
  372. pub fn add_from_dir<P, Q>(&mut self, pod_dir: P, bams_pass: Option<Q>) -> anyhow::Result<()>
  373. where
  374. P: AsRef<Path>,
  375. Q: AsRef<Path>,
  376. {
  377. let mut new_run = Pod5sRun::load_from_dir(&pod_dir)?;
  378. new_run.bams_pass = bams_pass.map(|p| p.as_ref().to_path_buf());
  379. // Try to find an existing run with same identifiers
  380. if let Some(existing) = self.data.iter_mut().find(|r| {
  381. r.run_id == new_run.run_id
  382. && r.flow_cell_id == new_run.flow_cell_id
  383. && r.sequencing_kit == new_run.sequencing_kit
  384. }) {
  385. // --- merge bams_pass ---
  386. match (&existing.bams_pass, &new_run.bams_pass) {
  387. (Some(old), Some(new)) if old != new => {
  388. anyhow::bail!(
  389. "Conflicting bam_pass for run {} (flowcell {}): \
  390. existing='{}', new='{}'",
  391. existing.run_id,
  392. existing.flow_cell_id,
  393. old.display(),
  394. new.display()
  395. );
  396. }
  397. (None, Some(new)) => {
  398. existing.bams_pass = Some(new.clone());
  399. }
  400. _ => {
  401. // (Some, None) or (None, None) or (Some, Some equal): nothing to do
  402. }
  403. }
  404. // Build a set of existing Pod5 file names
  405. let mut existing_names: HashSet<String> = existing
  406. .pod5s
  407. .iter()
  408. .filter_map(|p| p.path.file_name().map(|n| n.to_string_lossy().into_owned()))
  409. .collect();
  410. // Keep only Pod5 entries with a new file name
  411. new_run.pod5s.retain(|p| {
  412. if let Some(name_os) = p.path.file_name() {
  413. let name = name_os.to_string_lossy().to_string();
  414. if existing_names.contains(&name) {
  415. // duplicate -> skip
  416. false
  417. } else {
  418. existing_names.insert(name);
  419. true
  420. }
  421. } else {
  422. // No file name, keep it (or change to false if you prefer to drop these)
  423. true
  424. }
  425. });
  426. // Merge the unique new Pod5s into the existing run
  427. existing.pod5s.extend(new_run.pod5s);
  428. Ok(())
  429. } else {
  430. // No matching run: add as a new entry
  431. self.data.push(new_run);
  432. Ok(())
  433. }
  434. }
  435. /// Save metadata (not raw POD5s) as JSON.
  436. pub fn save_json<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
  437. let s = serde_json::to_string_pretty(self)?;
  438. fs::write(path, s)?;
  439. Ok(())
  440. }
  441. /// Load metadata from a saved JSON file.
  442. pub fn load_json<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
  443. let data = std::fs::read_to_string(path)?;
  444. Ok(serde_json::from_str(&data)?)
  445. }
  446. /// Add a run from an ONT run directory.
  447. ///
  448. /// Layout assumed:
  449. /// - `bam_pass/` → attached to `bams_pass` if present.
  450. /// - `pod5_pass/barcode*/*.pod5` and `pod5_recovered/*.pod5` → added to `pod5s`.
  451. pub fn add_run_dir<P: AsRef<Path>>(&mut self, run_dir: P) -> anyhow::Result<()> {
  452. let run_dir = run_dir.as_ref();
  453. // --- collect POD5 directories ---
  454. let mut pod_dirs: Vec<PathBuf> = Vec::new();
  455. // pod5_pass/barcode*/ subdirectories
  456. let pod5_pass = run_dir.join("pod5_pass");
  457. if pod5_pass.is_dir() {
  458. for entry in fs::read_dir(&pod5_pass)? {
  459. let entry = entry?;
  460. if entry.file_type()?.is_dir() {
  461. let name = entry.file_name();
  462. if name.to_string_lossy().starts_with("barcode") {
  463. pod_dirs.push(entry.path());
  464. }
  465. }
  466. }
  467. }
  468. // pod5_recovered/
  469. let pod5_recovered = run_dir.join("pod5_recovered");
  470. if pod5_recovered.is_dir() {
  471. pod_dirs.push(pod5_recovered);
  472. }
  473. if pod_dirs.is_empty() {
  474. anyhow::bail!(
  475. "No POD5 directories (pod5_pass/barcode*/ or pod5_recovered/) found under {}",
  476. run_dir.display()
  477. );
  478. }
  479. // --- bam_pass directory (optional) ---
  480. let bam_pass_dir = run_dir.join("bam_pass");
  481. let bams_pass = if bam_pass_dir.is_dir() {
  482. Some(bam_pass_dir)
  483. } else {
  484. None
  485. };
  486. // Build the new run from all POD5 directories
  487. let mut new_run = Pod5sRun::load_from_dirs(&pod_dirs)?;
  488. new_run.bams_pass = bams_pass;
  489. // --- merge logic identical to `add_from_dir` ---
  490. if let Some(existing) = self.data.iter_mut().find(|r| {
  491. r.run_id == new_run.run_id
  492. && r.flow_cell_id == new_run.flow_cell_id
  493. && r.sequencing_kit == new_run.sequencing_kit
  494. }) {
  495. // merge bams_pass
  496. match (&existing.bams_pass, &new_run.bams_pass) {
  497. (Some(old), Some(new)) if old != new => {
  498. anyhow::bail!(
  499. "Conflicting bam_pass for run {} (flowcell {}): \
  500. existing='{}', new='{}'",
  501. existing.run_id,
  502. existing.flow_cell_id,
  503. old.display(),
  504. new.display()
  505. );
  506. }
  507. (None, Some(new)) => {
  508. existing.bams_pass = Some(new.clone());
  509. }
  510. _ => {
  511. // (Some, None) or (None, None) or (Some, Some equal): nothing to do
  512. }
  513. }
  514. // merge pod5s by file name, skipping duplicates
  515. use std::collections::HashSet;
  516. let mut existing_names: HashSet<String> = existing
  517. .pod5s
  518. .iter()
  519. .filter_map(|p| p.path.file_name().map(|n| n.to_string_lossy().into_owned()))
  520. .collect();
  521. new_run.pod5s.retain(|p| {
  522. if let Some(name_os) = p.path.file_name() {
  523. let name = name_os.to_string_lossy().to_string();
  524. if existing_names.contains(&name) {
  525. false
  526. } else {
  527. existing_names.insert(name);
  528. true
  529. }
  530. } else {
  531. true
  532. }
  533. });
  534. existing.pod5s.extend(new_run.pod5s);
  535. Ok(())
  536. } else {
  537. // No matching run: add as new entry
  538. self.data.push(new_run);
  539. Ok(())
  540. }
  541. }
  542. }
  543. #[cfg(test)]
  544. mod tests {
  545. use crate::helpers::test_init;
  546. use super::*;
  547. #[test]
  548. fn load_pod5s() -> anyhow::Result<()> {
  549. test_init();
  550. let dir = "/mnt/beegfs02/scratch/t_steimle/prom_runs/A/20251117_0915_P2I-00461-A_PBI55810_22582b29/pod5_recovered";
  551. let flow_cell = Pod5sRun::load_from_dir(dir)?;
  552. println!("{:#?}", flow_cell.pod5s.first());
  553. let stats = flow_cell.stats();
  554. println!("{stats}");
  555. Ok(())
  556. }
  557. #[test]
  558. fn load_prom_run() -> anyhow::Result<()> {
  559. test_init();
  560. let dir = "/mnt/beegfs02/scratch/t_steimle/test_data/inputs/test_run_A";
  561. let mut runs = Pod5sRuns::new();
  562. runs.add_run_dir(dir)?;
  563. let stats = runs.data[0].stats();
  564. println!("{runs:#?}");
  565. println!("{stats}");
  566. Ok(())
  567. }
  568. }