1use std::collections::HashMap;
4use std::fmt;
5use std::fs::File;
6use std::io::{self, BufWriter, Write};
7use std::path::Path;
8use std::sync::mpsc;
9use std::sync::{Arc, Mutex};
10use std::thread;
11
12use bigtools::BigWigWrite;
13use bigtools::Value;
14
15use crate::curve::calls::NucleosomeCall;
16use bigtools::beddata::BedParserStreamingIterator;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum OutputFormat {
21 BigWig,
23 BedGraph,
25 Gff,
27}
28
29impl OutputFormat {
30 pub fn from_path(path: &Path) -> Result<Self, UnknownFormat> {
37 let ext = path
38 .extension()
39 .and_then(|e| e.to_str())
40 .unwrap_or_default()
41 .to_ascii_lowercase();
42 match ext.as_str() {
43 "bw" | "bigwig" => Ok(OutputFormat::BigWig),
44 "bedgraph" | "bg" => Ok(OutputFormat::BedGraph),
45 "gff" | "gff2" | "gff3" => Ok(OutputFormat::Gff),
46 _ => Err(UnknownFormat {
47 path: path.display().to_string(),
48 }),
49 }
50 }
51}
52
53#[derive(Debug)]
55pub struct UnknownFormat {
56 path: String,
57}
58
59impl fmt::Display for UnknownFormat {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(
62 f,
63 "cannot tell the output format from {:?}: expected one of .bw, .bigWig, .bedGraph, .bg",
64 self.path
65 )
66 }
67}
68
69impl std::error::Error for UnknownFormat {}
70
71fn interval(name: &str, position: usize, score: f64) -> (String, Value) {
76 let start = (position - 1) as u32;
77 (
78 name.to_string(),
79 Value {
80 start,
81 end: start + 1,
82 value: score as f32,
83 },
84 )
85}
86
87pub fn write_bedgraph_streaming<P>(
91 path: &Path,
92 produce: P,
93) -> Result<(), Box<dyn std::error::Error>>
94where
95 P: FnOnce(&mut dyn FnMut(&str, usize, f64) -> io::Result<()>) -> io::Result<()>,
96{
97 let mut out = BufWriter::new(File::create(path)?);
98 produce(&mut |name, position, score| {
99 let (chrom, value) = interval(name, position, score);
100 writeln!(
101 out,
102 "{}\t{}\t{}\t{}",
103 chrom, value.start, value.end, value.value
104 )
105 })?;
106 out.flush()?;
107 Ok(())
108}
109
110const SEND_BATCH: usize = 8192;
116
117pub fn write_bigwig_streaming<P>(
124 path: &Path,
125 chrom_sizes: Vec<(String, u32)>,
126 queue_depth: usize,
127 produce: P,
128) -> Result<(), Box<dyn std::error::Error>>
129where
130 P: FnOnce(&mut dyn FnMut(&str, usize, f64) -> io::Result<()>) -> io::Result<()>
131 + Send
132 + 'static,
133{
134 let sizes: HashMap<String, u32> = chrom_sizes.into_iter().collect();
135 let writer = BigWigWrite::create_file(path, sizes)?;
136
137 let batches_in_flight = queue_depth.div_ceil(SEND_BATCH).max(1);
138 let (tx, rx) = mpsc::sync_channel::<Vec<(String, Value)>>(batches_in_flight);
139 let failure = Arc::new(Mutex::new(None::<String>));
143 let producer_failure = Arc::clone(&failure);
144
145 let producer = thread::spawn(move || {
146 let mut batch: Vec<(String, Value)> = Vec::with_capacity(SEND_BATCH);
147 let result = (|| {
148 produce(&mut |name, position, score| {
149 batch.push(interval(name, position, score));
150 if batch.len() == SEND_BATCH {
151 let full = std::mem::replace(&mut batch, Vec::with_capacity(SEND_BATCH));
152 tx.send(full)
155 .map_err(|_| io::Error::other("bigWig writer stopped accepting values"))?;
156 }
157 Ok(())
158 })?;
159 if !batch.is_empty() {
160 tx.send(std::mem::take(&mut batch))
161 .map_err(|_| io::Error::other("bigWig writer stopped accepting values"))?;
162 }
163 Ok::<(), io::Error>(())
164 })();
165 if let Err(err) = result {
166 *producer_failure.lock().unwrap() = Some(err.to_string());
167 }
168 });
169
170 let data = BedParserStreamingIterator::wrap_infallible_iter(rx.into_iter().flatten(), false);
171 let runtime = tokio::runtime::Builder::new_multi_thread().build()?;
172 let write_result = writer.write(data, runtime);
173
174 producer.join().map_err(|_| "scoring thread panicked")?;
175 if let Some(err) = failure.lock().unwrap().take() {
176 return Err(err.into());
177 }
178 write_result?;
179 Ok(())
180}
181
182pub fn write_gff_streaming<P>(
193 path: &Path,
194 feature: &str,
195 produce: P,
196) -> Result<(), Box<dyn std::error::Error>>
197where
198 P: FnOnce(
199 &mut dyn FnMut(&str, &[u8], &[NucleosomeCall], usize) -> io::Result<()>,
200 ) -> io::Result<()>,
201{
202 let mut out = BufWriter::new(File::create(path)?);
203 produce(&mut |name, bases, calls, half_width| {
204 for call in calls {
205 let start = call.dyad - half_width;
206 let end = call.dyad + half_width;
207 let sequence = std::str::from_utf8(&bases[start..=end]).unwrap_or("");
208 writeln!(
209 out,
210 "{name}\tevidence\t{feature}\t{start}\t{end}\t{}\t+\t.\t{sequence}",
211 call.reported_score()
212 )?;
213 }
214 Ok(())
215 })?;
216 out.flush()?;
217 Ok(())
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::path::PathBuf;
224
225 fn produce(emit: &mut dyn FnMut(&str, usize, f64) -> io::Result<()>) -> io::Result<()> {
227 emit("chr1", 22, 1.5)?;
228 emit("chr1", 23, 2.5)?;
229 emit("chr1", 24, 3.5)?;
230 emit("chr2", 31, 4.0)?;
231 Ok(())
232 }
233
234 fn sizes() -> Vec<(String, u32)> {
235 vec![("chr1".to_string(), 100), ("chr2".to_string(), 60)]
236 }
237
238 fn tmp(name: &str) -> PathBuf {
239 let mut p = std::env::temp_dir();
240 p.push(format!("symcurve-out-{}-{}", std::process::id(), name));
241 p
242 }
243
244 #[test]
245 fn test_format_from_extension() {
246 let cases = [
247 ("out.bw", Some(OutputFormat::BigWig)),
248 ("out.bigWig", Some(OutputFormat::BigWig)),
249 ("OUT.BW", Some(OutputFormat::BigWig)),
250 ("out.bedGraph", Some(OutputFormat::BedGraph)),
251 ("out.bg", Some(OutputFormat::BedGraph)),
252 ("out.txt", None),
253 ("out", None),
254 ];
255 for (name, expected) in cases {
256 assert_eq!(
257 OutputFormat::from_path(Path::new(name)).ok(),
258 expected,
259 "for {name}"
260 );
261 }
262 }
263
264 #[test]
265 fn test_unknown_extension_message_lists_the_options() {
266 let err = OutputFormat::from_path(Path::new("out.txt")).unwrap_err();
267 let msg = err.to_string();
268 assert!(msg.contains("out.txt"), "{msg}");
269 assert!(msg.contains(".bw") && msg.contains(".bedGraph"), "{msg}");
270 }
271
272 #[test]
273 fn test_interval_is_zero_based_half_open() {
274 let (chrom, value) = interval("chr1", 22, 1.5);
276 assert_eq!(chrom, "chr1");
277 assert_eq!((value.start, value.end), (21, 22));
278 assert!((value.value - 1.5).abs() < 1e-6);
279 }
280
281 #[test]
282 fn test_bedgraph_contents() {
283 let path = tmp("out.bedGraph");
284 write_bedgraph_streaming(&path, produce).unwrap();
285 let text = std::fs::read_to_string(&path).unwrap();
286 let lines: Vec<&str> = text.lines().collect();
287 assert_eq!(lines.len(), 4);
288 assert_eq!(lines[0], "chr1\t21\t22\t1.5");
289 assert_eq!(lines[3], "chr2\t30\t31\t4");
290 std::fs::remove_file(&path).ok();
291 }
292
293 #[test]
294 fn test_bigwig_round_trips() {
295 let path = tmp("out.bw");
298 write_bigwig_streaming(&path, sizes(), 8, produce).unwrap();
299
300 let mut read = bigtools::BigWigRead::open_file(&path).unwrap();
301 let mut chroms: Vec<_> = read
302 .chroms()
303 .iter()
304 .map(|c| (c.name.clone(), c.length))
305 .collect();
306 chroms.sort();
307 assert_eq!(
308 chroms,
309 vec![("chr1".to_string(), 100u32), ("chr2".to_string(), 60u32)]
310 );
311
312 let values: Vec<_> = read.values("chr1", 21, 24).unwrap();
313 assert_eq!(values.len(), 3);
314 assert!((values[0] - 1.5).abs() < 1e-6, "{values:?}");
315 assert!((values[2] - 3.5).abs() < 1e-6, "{values:?}");
316 std::fs::remove_file(&path).ok();
317 }
318
319 #[test]
320 fn test_bigwig_survives_a_queue_depth_of_one() {
321 let path = tmp("depth1.bw");
324 write_bigwig_streaming(&path, sizes(), 1, produce).unwrap();
325 let mut read = bigtools::BigWigRead::open_file(&path).unwrap();
326 let values: Vec<_> = read.values("chr1", 21, 24).unwrap();
327 assert_eq!(values.len(), 3);
328 std::fs::remove_file(&path).ok();
329 }
330
331 #[test]
332 fn test_producer_error_is_reported_not_silently_truncated() {
333 let path = tmp("fail.bw");
335 let failing = |emit: &mut dyn FnMut(&str, usize, f64) -> io::Result<()>| {
336 emit("chr1", 22, 1.5)?;
337 Err(io::Error::other("synthetic read failure"))
338 };
339 let err = write_bigwig_streaming(&path, sizes(), 8, failing).unwrap_err();
340 assert!(err.to_string().contains("synthetic read failure"), "{err}");
341 std::fs::remove_file(&path).ok();
342 }
343
344 #[test]
345 fn test_bigwig_streaming_flushes_full_batches() {
346 let n = 2 * SEND_BATCH + 7;
349 let path = tmp("batches.bw");
350 let many = move |emit: &mut dyn FnMut(&str, usize, f64) -> io::Result<()>| {
351 for i in 0..n {
352 emit("chr1", i + 1, i as f64)?;
353 }
354 Ok(())
355 };
356 write_bigwig_streaming(&path, vec![("chr1".to_string(), n as u32)], 8, many).unwrap();
357
358 let mut read = bigtools::BigWigRead::open_file(&path).unwrap();
359 let values: Vec<_> = read.values("chr1", 0, n as u32).unwrap();
360 assert_eq!(values.len(), n);
361 assert_eq!(values[0], 0.0);
362 assert_eq!(values[n - 1], (n - 1) as f32);
363 std::fs::remove_file(&path).ok();
364 }
365}