1use std::collections::HashMap;
10use std::sync::Arc;
11
12use ndarray::Array2;
13use parking_lot::Mutex;
14
15use crate::arrays::CooMatrix;
16use crate::error::{Error, Result};
17use crate::genomic::{ChrMap, Locs};
18use crate::parallel::Executor;
19use crate::source::ByteSource;
20
21use super::block::{block_numbers, read_block, ContactRecord, RecordContext};
22use super::header::{vector_key, HiCFooter, HiCHeader};
23use super::matrix::{matrix_key, parse_loc2d, Loc2D, MatrixMetadata};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum HiCMode {
27 #[default]
28 Observed,
29 Oe,
31 Expected,
32}
33
34impl std::str::FromStr for HiCMode {
35 type Err = Error;
36 fn from_str(s: &str) -> Result<Self> {
37 match s.to_ascii_lowercase().as_str() {
38 "observed" => Ok(HiCMode::Observed),
39 "oe" => Ok(HiCMode::Oe),
40 "expected" => Ok(HiCMode::Expected),
41 o => Err(Error::invalid(format!(
42 "mode {o} invalid (observed, oe or expected)"
43 ))),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum Unit {
50 #[default]
51 Bp,
52 Frag,
53}
54
55impl Unit {
56 pub fn as_str(self) -> &'static str {
57 match self {
58 Unit::Bp => "bp",
59 Unit::Frag => "frag",
60 }
61 }
62}
63
64impl std::str::FromStr for Unit {
65 type Err = Error;
66 fn from_str(s: &str) -> Result<Self> {
67 match s.to_ascii_lowercase().as_str() {
68 "bp" => Ok(Unit::Bp),
69 "frag" => Ok(Unit::Frag),
70 o => Err(Error::invalid(format!("unit {o} invalid (bp or frag)"))),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Default)]
77pub struct Normalizations {
78 pub x: Arc<Vec<f32>>,
79 pub y: Arc<Vec<f32>>,
80 pub expected: Arc<Vec<f32>>,
81}
82
83#[derive(Debug)]
84struct Inner {
85 source: Arc<dyn ByteSource>,
86 executor: Executor,
87}
88
89pub struct HiCReader {
90 inner: Option<Inner>,
91 path: String,
92 header: HiCHeader,
93 footer: HiCFooter,
94
95 expected_cache: Mutex<HashMap<String, Arc<Vec<f32>>>>,
96 norm_cache: Mutex<HashMap<String, Arc<Vec<f32>>>>,
97 matrix_cache: Mutex<HashMap<String, Arc<MatrixMetadata>>>,
98}
99
100impl std::fmt::Debug for HiCReader {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("HiCReader")
103 .field("path", &self.path)
104 .field("version", &self.header.version)
105 .field("chromosomes", &self.header.chr_map.len())
106 .field("closed", &self.is_closed())
107 .finish()
108 }
109}
110
111impl HiCReader {
112 pub fn open(
113 path: &str,
114 parallel: i64,
115 block_size: Option<u64>,
116 max_blocks: Option<usize>,
117 ) -> Result<Self> {
118 let source = crate::source::open(path, block_size, max_blocks)?;
119 Self::from_source(source, path, parallel)
120 }
121
122 pub(crate) fn from_source(
123 source: Arc<dyn ByteSource>,
124 path: &str,
125 parallel: i64,
126 ) -> Result<Self> {
127 let header = super::header::read_header(source.as_ref())?;
128 let footer = super::header::read_footer(source.as_ref(), &header)?;
129 Ok(Self {
130 inner: Some(Inner {
131 source,
132 executor: Executor::new(parallel)?,
133 }),
134 path: path.to_string(),
135 header,
136 footer,
137 expected_cache: Mutex::new(HashMap::new()),
138 norm_cache: Mutex::new(HashMap::new()),
139 matrix_cache: Mutex::new(HashMap::new()),
140 })
141 }
142
143 pub fn header(&self) -> &HiCHeader {
144 &self.header
145 }
146 pub fn footer(&self) -> &HiCFooter {
147 &self.footer
148 }
149 pub fn chr_sizes(&self) -> &ChrMap {
150 &self.header.chr_map
151 }
152 pub fn normalizations(&self) -> &[String] {
153 &self.footer.normalizations
154 }
155 pub fn units(&self) -> &[String] {
156 &self.footer.units
157 }
158 pub fn bin_sizes(&self, unit: Unit) -> &[i64] {
165 self.header.resolutions(unit)
166 }
167 pub fn path(&self) -> &str {
168 &self.path
169 }
170 pub fn is_closed(&self) -> bool {
171 self.inner.is_none()
172 }
173 pub fn parallel(&self) -> usize {
174 self.inner.as_ref().map_or(0, |i| i.executor.parallel())
175 }
176
177 pub fn close(&mut self) {
178 if let Some(inner) = self.inner.take() {
179 inner.source.close();
180 }
181 }
182
183 fn inner(&self) -> Result<&Inner> {
184 self.inner.as_ref().ok_or_else(|| Error::Closed {
185 path: self.path.clone(),
186 })
187 }
188
189 pub fn parse_loc(&self, req: &HiCRequest) -> Result<Loc2D> {
197 let locs = Locs::spans(&req.chr_ids, &req.starts, &req.ends)?;
198 parse_loc2d(
199 &self.header.chr_map,
200 self.header.resolutions(req.unit),
201 &locs.chr_ids,
202 &locs.starts,
203 &locs.ends,
204 req.bin_size,
205 req.bin_count.map(|n| n as i64),
206 req.full_bin,
207 )
208 }
209
210 fn expected_values(
211 &self,
212 chr: i64,
213 normalization: &str,
214 bin_size: i64,
215 unit: Unit,
216 ) -> Result<Arc<Vec<f32>>> {
217 let key = vector_key(normalization, bin_size, unit.as_str(), Some(chr));
218 if let Some(hit) = self.expected_cache.lock().get(&key) {
219 return Ok(hit.clone());
220 }
221 let values = Arc::new(super::header::compute_expected_values(
222 &self.footer,
223 chr,
224 unit.as_str(),
225 bin_size,
226 normalization,
227 )?);
228 self.expected_cache.lock().insert(key, values.clone());
229 Ok(values)
230 }
231
232 fn normalization_vector(
233 &self,
234 inner: &Inner,
235 chr: i64,
236 normalization: &str,
237 bin_size: i64,
238 unit: Unit,
239 ) -> Result<Arc<Vec<f32>>> {
240 let key = vector_key(normalization, bin_size, unit.as_str(), Some(chr));
241 if let Some(hit) = self.norm_cache.lock().get(&key) {
242 return Ok(hit.clone());
243 }
244 let values = Arc::new(super::header::read_normalization_vector(
245 inner.source.as_ref(),
246 &self.footer,
247 self.header.version,
248 chr,
249 unit.as_str(),
250 bin_size,
251 normalization,
252 )?);
253 self.norm_cache.lock().insert(key, values.clone());
254 Ok(values)
255 }
256
257 fn matrix(
274 &self,
275 inner: &Inner,
276 loc: &Loc2D,
277 unit: Unit,
278 ) -> Result<Option<Arc<MatrixMetadata>>> {
279 let (chr1, chr2) = (loc.x.chr.index as i64, loc.y.chr.index as i64);
280 let key = matrix_key(chr1, chr2, loc.bin_size, unit.as_str());
281 if let Some(hit) = self.matrix_cache.lock().get(&key) {
282 return Ok(Some(hit.clone()));
283 }
284
285 let index_key = format!("{chr1}_{chr2}");
286 let Some(item) = self.footer.master_index.get(&index_key).copied() else {
287 return Ok(None);
288 };
289 let matrices =
290 super::matrix::read_matrix_metadata(inner.source.as_ref(), item, chr1, chr2)?;
291
292 let mut available = Vec::new();
293 {
294 let mut cache = self.matrix_cache.lock();
295 for matrix in matrices {
296 available.push(format!("{}{}", matrix.bin_size, matrix.unit));
297 let entry_key = matrix_key(chr1, chr2, matrix.bin_size, &matrix.unit);
298 cache.insert(entry_key, Arc::new(matrix));
299 }
300 if let Some(hit) = cache.get(&key) {
301 return Ok(Some(hit.clone()));
302 }
303 }
304 Err(Error::invalid(format!(
305 "no matrix for {key} (available for this pair: {})",
306 available.join(", ")
307 )))
308 }
309
310 fn records(&self, req: &HiCRequest, loc: &Loc2D) -> Result<Vec<ContactRecord>> {
312 let inner = self.inner()?;
313 let normalization = req.normalization.to_ascii_lowercase();
314 let Some(matrix) = self.matrix(inner, loc, req.unit)? else {
315 return Ok(Vec::new());
318 };
319
320 let mut vectors = Normalizations::default();
325 if normalization != "none" {
326 vectors.x = self.normalization_vector(
327 inner,
328 loc.x.chr.index as i64,
329 &normalization,
330 loc.bin_size,
331 req.unit,
332 )?;
333 vectors.y = self.normalization_vector(
334 inner,
335 loc.y.chr.index as i64,
336 &normalization,
337 loc.bin_size,
338 req.unit,
339 )?;
340 }
341 if req.mode != HiCMode::Observed && loc.is_intra() {
342 vectors.expected = self.expected_values(
343 loc.x.chr.index as i64,
344 &normalization,
345 loc.bin_size,
346 req.unit,
347 )?;
348 }
349
350 let mut average_value = f32::NAN;
354 if !loc.is_intra() {
355 let x_bins = loc.x.chr.size / loc.bin_size;
356 let y_bins = loc.y.chr.size / loc.bin_size;
357 if x_bins > 0 && y_bins > 0 {
358 average_value = matrix.sum_counts / x_bins as f32 / y_bins as f32;
359 }
360 }
361
362 let numbers = block_numbers(
363 loc,
364 &matrix,
365 req.max_distance,
366 self.header.version,
367 req.triangle,
368 )?;
369 let blocks: Vec<_> = numbers
370 .iter()
371 .filter_map(|n| matrix.blocks.get(n).copied())
372 .collect();
373
374 let ctx = RecordContext {
375 loc,
376 normalization: &normalization,
377 mode: req.mode,
378 vectors: &vectors,
379 average_value,
380 min_distance: req.min_distance,
381 max_distance: req.max_distance,
382 };
383 let per_block = inner.executor.map_batches(&blocks, |_, block| {
384 let raw = inner
385 .source
386 .read_exact_at(block.position, block.size.max(0) as usize)?;
387 read_block(raw, self.header.version, *block, &ctx, &self.path)
388 })?;
389 Ok(per_block.into_iter().flatten().collect())
390 }
391
392 pub fn read_values(&self, req: &HiCRequest) -> Result<Array2<f32>> {
402 let loc = self.parse_loc(req)?;
403 let records = self.records(req, &loc)?;
404
405 let rows = (loc.x.bin_end - loc.x.bin_start).max(0) as usize;
406 let cols = (loc.y.bin_end - loc.y.bin_start).max(0) as usize;
407 let mut flat = vec![req.def_value; rows * cols];
408 for record in &records {
409 let r = record.x_bin - loc.x.bin_start;
410 let c = record.y_bin - loc.y.bin_start;
411 if r >= 0 && (r as usize) < rows && c >= 0 && (c as usize) < cols {
412 flat[r as usize * cols + c as usize] = record.value;
413 }
414 if loc.is_intra() && !req.triangle {
417 let r = record.y_bin - loc.x.bin_start;
418 let c = record.x_bin - loc.y.bin_start;
419 if r >= 0 && (r as usize) < rows && c >= 0 && (c as usize) < cols {
420 flat[r as usize * cols + c as usize] = record.value;
421 }
422 }
423 }
424
425 let (mut rows, mut cols, mut flat) = if loc.reversed {
426 (cols, rows, transpose(&flat, rows, cols))
427 } else {
428 (rows, cols, flat)
429 };
430
431 if req.exact_bin_count {
432 if let Some(count) = req.bin_count {
433 flat = crate::arrays::bilinear(&flat, (rows, cols), (count, count))?;
434 rows = count;
435 cols = count;
436 }
437 }
438 Array2::from_shape_vec((rows, cols), flat)
439 .map_err(|e| Error::invalid(format!("output shape {rows}x{cols}: {e}")))
440 }
441
442 pub fn read_sparse_values(&self, req: &HiCRequest) -> Result<CooMatrix> {
447 let loc = self.parse_loc(req)?;
448 let records = self.records(req, &loc)?;
449
450 let rows = (loc.x.bin_end - loc.x.bin_start).max(0) as usize;
451 let cols = (loc.y.bin_end - loc.y.bin_start).max(0) as usize;
452 let mut out = CooMatrix {
453 shape: (rows, cols),
454 ..Default::default()
455 };
456 let push = |r: i64, c: i64, value: f32, out: &mut CooMatrix| {
457 if r >= 0 && (r as usize) < rows && c >= 0 && (c as usize) < cols {
458 out.values.push(value);
459 out.row.push(r as u32);
460 out.col.push(c as u32);
461 }
462 };
463 for record in &records {
464 push(
465 record.x_bin - loc.x.bin_start,
466 record.y_bin - loc.y.bin_start,
467 record.value,
468 &mut out,
469 );
470 if loc.is_intra() && !req.triangle && record.x_bin != record.y_bin {
475 push(
476 record.y_bin - loc.x.bin_start,
477 record.x_bin - loc.y.bin_start,
478 record.value,
479 &mut out,
480 );
481 }
482 }
483
484 let mut out = sort_row_major(out);
487
488 if loc.reversed {
489 std::mem::swap(&mut out.row, &mut out.col);
490 out.shape = (out.shape.1, out.shape.0);
491 out = sort_row_major(out);
496 }
497
498 if req.exact_bin_count {
499 if let Some(count) = req.bin_count {
500 out = crate::arrays::bilinear_sparse(&out, (count, count))?;
501 }
502 }
503 Ok(out)
504 }
505}
506
507fn sort_row_major(coo: CooMatrix) -> CooMatrix {
512 let mut order: Vec<usize> = (0..coo.values.len()).collect();
513 order.sort_by_key(|i| (coo.row[*i], coo.col[*i]));
514 CooMatrix {
515 values: order.iter().map(|i| coo.values[*i]).collect(),
516 row: order.iter().map(|i| coo.row[*i]).collect(),
517 col: order.iter().map(|i| coo.col[*i]).collect(),
518 shape: coo.shape,
519 }
520}
521
522fn transpose(flat: &[f32], rows: usize, cols: usize) -> Vec<f32> {
523 let mut out = vec![0.0f32; flat.len()];
524 for r in 0..rows {
525 for c in 0..cols {
526 out[c * rows + r] = flat[r * cols + c];
527 }
528 }
529 out
530}
531
532#[derive(Debug, Clone)]
533pub struct HiCRequest {
534 pub chr_ids: Vec<String>,
535 pub starts: Vec<i64>,
536 pub ends: Vec<i64>,
537 pub bin_size: Option<i64>,
539 pub bin_count: Option<usize>,
542 pub exact_bin_count: bool,
544 pub full_bin: bool,
545 pub def_value: f32,
546 pub triangle: bool,
550 pub min_distance: Option<i64>,
551 pub max_distance: Option<i64>,
552 pub normalization: String,
553 pub mode: HiCMode,
554 pub unit: Unit,
555}
556
557impl HiCRequest {
558 pub fn new(chr_ids: Vec<String>, starts: Vec<i64>, ends: Vec<i64>) -> Self {
559 Self {
560 chr_ids,
561 starts,
562 ends,
563 bin_size: None,
564 bin_count: None,
565 exact_bin_count: false,
566 full_bin: false,
567 def_value: 0.0,
568 triangle: false,
569 min_distance: None,
570 max_distance: None,
571 normalization: "none".into(),
572 mode: HiCMode::Observed,
573 unit: Unit::Bp,
574 }
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use std::str::FromStr;
582
583 #[test]
584 fn modes_and_units_parse_and_refuse() {
585 assert_eq!(HiCMode::from_str("oe").unwrap(), HiCMode::Oe);
586 assert_eq!(HiCMode::from_str("OBSERVED").unwrap(), HiCMode::Observed);
587 let err = HiCMode::from_str("median").unwrap_err().to_string();
588 assert!(err.contains("mode median invalid"), "{err}");
589
590 assert_eq!(Unit::from_str("BP").unwrap(), Unit::Bp);
591 assert_eq!(Unit::from_str("frag").unwrap(), Unit::Frag);
592 let err = Unit::from_str("kb").unwrap_err().to_string();
593 assert!(err.contains("unit kb invalid (bp or frag)"), "{err}");
594 }
595
596 #[test]
597 fn transposing_swaps_the_axes() {
598 let flat = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
600 assert_eq!(transpose(&flat, 2, 3), [1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
601 }
602}