1use indexmap::IndexMap;
8
9use crate::error::{Error, Result};
10use crate::genomic::ChrMap;
11use crate::source::ByteSource;
12
13use super::Unit;
14
15const MAX_RESERVE: usize = 4096;
23
24fn clamp_reserve(count: i64) -> usize {
25 count.clamp(0, MAX_RESERVE as i64) as usize
26}
27
28#[derive(Debug, Clone)]
29pub struct HiCHeader {
30 pub version: i64,
31 pub footer_position: u64,
32 pub genome_id: String,
33 pub nvi_position: i64,
36 pub nvi_length: i64,
37 pub attributes: IndexMap<String, String>,
38 pub chr_map: ChrMap,
39 pub bp_resolutions: Vec<i64>,
40 pub frag_resolutions: Vec<i64>,
41 pub sites: IndexMap<String, Vec<i64>>,
44}
45
46impl HiCHeader {
47 pub fn resolutions(&self, unit: Unit) -> &[i64] {
49 match unit {
50 Unit::Bp => &self.bp_resolutions,
51 Unit::Frag => &self.frag_resolutions,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct HiCIndexItem {
59 pub position: u64,
60 pub size: i64,
61}
62
63#[derive(Debug, Clone)]
64pub struct ExpectedValueVector {
65 pub normalization: String,
66 pub unit: String,
67 pub bin_size: i64,
68 pub values: Vec<f32>,
69 pub chr_scale_factors: IndexMap<i64, f32>,
70}
71
72#[derive(Debug, Clone)]
73pub struct NormalizationVector {
74 pub normalization: String,
75 pub chr_index: i64,
76 pub unit: String,
77 pub bin_size: i64,
78 pub position: u64,
79 pub byte_count: i64,
80}
81
82#[derive(Debug, Clone, Default)]
83pub struct HiCFooter {
84 pub byte_count_v5: i64,
88 pub master_index: IndexMap<String, HiCIndexItem>,
89 pub expected_value_vectors: IndexMap<String, ExpectedValueVector>,
90 pub normalization_vectors: IndexMap<String, NormalizationVector>,
91 pub normalizations: Vec<String>,
93 pub units: Vec<String>,
94}
95
96pub fn vector_key(normalization: &str, bin_size: i64, unit: &str, chr: Option<i64>) -> String {
99 let base = format!("normalization={normalization}|bin_size={bin_size}|unit={unit}");
100 match chr {
101 Some(index) => format!("chr_index={index}|{base}"),
102 None => base,
103 }
104}
105
106struct Cursor<'a> {
109 source: &'a dyn ByteSource,
110 offset: u64,
111 buffer: bytes::Bytes,
112 consumed: usize,
113}
114
115impl<'a> Cursor<'a> {
116 fn new(source: &'a dyn ByteSource, offset: u64) -> Self {
117 Self {
118 source,
119 offset,
120 buffer: bytes::Bytes::new(),
121 consumed: 0,
122 }
123 }
124
125 fn fill(&mut self, wanted: usize) -> Result<()> {
135 if self.buffer.len() - self.consumed >= wanted {
136 return Ok(());
137 }
138 const CHUNK: usize = 1 << 16;
139 self.buffer = self.buffer.slice(self.consumed..);
140 self.offset += self.consumed as u64;
141 self.consumed = 0;
142
143 while self.buffer.len() < wanted {
144 let at = self.offset + self.buffer.len() as u64;
145 let more = self
146 .source
147 .read_at(at, CHUNK.max(wanted - self.buffer.len()))?;
148 if more.is_empty() {
149 return Err(Error::corrupt(
150 self.source.path(),
151 at,
152 "hic header ended early",
153 ));
154 }
155 let mut joined = bytes::BytesMut::with_capacity(self.buffer.len() + more.len());
156 joined.extend_from_slice(&self.buffer);
157 joined.extend_from_slice(&more);
158 self.buffer = joined.freeze();
159 }
160 Ok(())
161 }
162
163 fn take(&mut self, n: usize) -> Result<bytes::Bytes> {
164 self.fill(n)?;
165 let out = self.buffer.slice(self.consumed..self.consumed + n);
166 self.consumed += n;
167 Ok(out)
168 }
169
170 fn i32(&mut self) -> Result<i32> {
171 let b = self.take(4)?;
172 Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
173 }
174 fn i64(&mut self) -> Result<i64> {
175 let b = self.take(8)?;
176 Ok(i64::from_le_bytes([
177 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
178 ]))
179 }
180 fn f32(&mut self) -> Result<f32> {
181 let b = self.take(4)?;
182 Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
183 }
184 fn f64(&mut self) -> Result<f64> {
185 let b = self.take(8)?;
186 Ok(f64::from_le_bytes([
187 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
188 ]))
189 }
190
191 fn cstr(&mut self) -> Result<String> {
193 let mut wanted = 64usize;
194 loop {
195 let _ = self.fill(wanted);
199 let rest = &self.buffer[self.consumed..];
200 if let Some(at) = memchr::memchr(0, rest) {
201 let out = String::from_utf8_lossy(&rest[..at]).into_owned();
202 self.consumed += at + 1;
203 return Ok(out);
204 }
205 if rest.len() < wanted {
206 return Err(Error::corrupt(
207 self.source.path(),
208 self.offset,
209 "hic header string is not NUL-terminated",
210 ));
211 }
212 wanted *= 2;
213 }
214 }
215}
216
217pub fn read_header(source: &dyn ByteSource) -> Result<HiCHeader> {
218 let path = source.path();
219 let mut c = Cursor::new(source, 0);
220
221 let magic = c.take(4)?;
222 if &magic[..3] != b"HIC" {
223 return Err(Error::format(
224 path,
225 format!(
226 "not a hic file (magic: '{}')",
227 String::from_utf8_lossy(&magic[..3])
228 ),
229 ));
230 }
231 let version = c.i32()? as i64;
232 if !(6..=9).contains(&version) {
235 return Err(Error::format(
236 path,
237 format!("hic version {version} unsupported (6 to 9)"),
238 ));
239 }
240 let footer_position = c.i64()? as u64;
241 let genome_id = c.cstr()?;
242 let (nvi_position, nvi_length) = if version > 8 {
245 (c.i64()?, c.i64()?)
246 } else {
247 (-1, -1)
248 };
249
250 let attribute_count = c.i32()?;
251 let mut attributes = IndexMap::with_capacity(clamp_reserve(attribute_count as i64));
252 for _ in 0..attribute_count.max(0) {
253 let key = c.cstr()?;
254 attributes.insert(key, c.cstr()?);
255 }
256
257 let chr_count = c.i32()?;
258 let mut chrs = Vec::with_capacity(clamp_reserve(chr_count as i64));
259 for index in 0..chr_count.max(0) {
260 let id = c.cstr()?;
261 let size = if version > 8 {
262 c.i64()?
263 } else {
264 c.i32()? as i64
265 };
266 chrs.push((id, size, index as usize));
267 }
268
269 let bp_count = c.i32()?;
270 let mut bp_resolutions = Vec::with_capacity(clamp_reserve(bp_count as i64));
271 for _ in 0..bp_count.max(0) {
272 bp_resolutions.push(c.i32()? as i64);
273 }
274 let frag_count = c.i32()?;
275 let mut frag_resolutions = Vec::with_capacity(clamp_reserve(frag_count as i64));
276 for _ in 0..frag_count.max(0) {
277 frag_resolutions.push(c.i32()? as i64);
278 }
279 let mut sites = IndexMap::new();
293 if !frag_resolutions.is_empty() {
294 for (id, _, _) in &chrs {
295 let count = c.i32()?;
296 let mut chr_sites = Vec::with_capacity(clamp_reserve(count as i64));
297 for _ in 0..count.max(0) {
298 chr_sites.push(c.i32()? as i64);
299 }
300 sites.insert(id.clone(), chr_sites);
301 }
302 }
303
304 Ok(HiCHeader {
305 version,
306 footer_position,
307 genome_id,
308 nvi_position,
309 nvi_length,
310 attributes,
311 chr_map: ChrMap::from_indexed_entries(chrs),
312 bp_resolutions,
313 frag_resolutions,
314 sites,
315 })
316}
317
318pub fn read_footer(source: &dyn ByteSource, header: &HiCHeader) -> Result<HiCFooter> {
319 let version = header.version;
320 let mut c = Cursor::new(source, header.footer_position);
321
322 let byte_count_v5 = if version > 8 {
326 c.i64()?
327 } else {
328 c.i32()? as i64
329 };
330 let mut footer = HiCFooter {
331 byte_count_v5,
332 ..Default::default()
333 };
334
335 let master_count = c.i32()?;
336 for _ in 0..master_count.max(0) {
337 let key = c.cstr()?;
338 let position = c.i64()? as u64;
339 let size = c.i32()? as i64;
340 footer
341 .master_index
342 .insert(key, HiCIndexItem { position, size });
343 }
344
345 let mut normalizations: Vec<String> = Vec::new();
346 let mut units: Vec<String> = Vec::new();
347 let note = |set: &mut Vec<String>, value: &str| {
348 if !set.iter().any(|v| v == value) {
349 set.push(value.to_string());
350 }
351 };
352
353 for is_normalized in [false, true] {
355 let count = c.i32()?;
356 for _ in 0..count.max(0) {
357 let normalization = if is_normalized {
358 c.cstr()?.to_ascii_lowercase()
359 } else {
360 "none".to_string()
361 };
362 note(&mut normalizations, &normalization);
363 let unit = c.cstr()?.to_ascii_lowercase();
364 note(&mut units, &unit);
365 let bin_size = c.i32()? as i64;
366
367 let values = if version > 8 {
368 let n = c.i64()?;
369 let mut values = Vec::with_capacity(clamp_reserve(n));
370 for _ in 0..n.max(0) {
371 values.push(c.f32()?);
372 }
373 values
374 } else {
375 let n = c.i32()? as i64;
376 let mut values = Vec::with_capacity(clamp_reserve(n));
377 for _ in 0..n.max(0) {
378 values.push(c.f64()? as f32);
379 }
380 values
381 };
382
383 let factor_count = c.i32()?;
384 let mut chr_scale_factors = IndexMap::with_capacity(clamp_reserve(factor_count as i64));
385 for _ in 0..factor_count.max(0) {
386 let chr_index = c.i32()? as i64;
387 let factor = if version > 8 {
388 c.f32()?
389 } else {
390 c.f64()? as f32
391 };
392 chr_scale_factors.insert(chr_index, factor);
393 }
394
395 let key = vector_key(&normalization, bin_size, &unit, None);
396 footer.expected_value_vectors.insert(
397 key,
398 ExpectedValueVector {
399 normalization,
400 unit,
401 bin_size,
402 values,
403 chr_scale_factors,
404 },
405 );
406 }
407 }
408
409 let vector_count = c.i32()?;
410 for _ in 0..vector_count.max(0) {
411 let normalization = c.cstr()?.to_ascii_lowercase();
412 note(&mut normalizations, &normalization);
413 let chr_index = c.i32()? as i64;
414 let unit = c.cstr()?.to_ascii_lowercase();
415 note(&mut units, &unit);
416 let bin_size = c.i32()? as i64;
417 let position = c.i64()? as u64;
418 let byte_count = if version > 8 {
419 c.i64()?
420 } else {
421 c.i32()? as i64
422 };
423 let key = vector_key(&normalization, bin_size, &unit, Some(chr_index));
424 footer.normalization_vectors.insert(
425 key,
426 NormalizationVector {
427 normalization,
428 chr_index,
429 unit,
430 bin_size,
431 position,
432 byte_count,
433 },
434 );
435 }
436
437 normalizations.sort();
438 units.sort();
439 footer.normalizations = normalizations;
440 footer.units = units;
441 Ok(footer)
442}
443
444pub fn compute_expected_values(
450 footer: &HiCFooter,
451 chr_index: i64,
452 unit: &str,
453 bin_size: i64,
454 normalization: &str,
455) -> Result<Vec<f32>> {
456 let key = vector_key(normalization, bin_size, unit, None);
457 let vector = footer
458 .expected_value_vectors
459 .get(&key)
460 .ok_or_else(|| Error::invalid(format!("expected value vector {key} not found")))?;
461 let factor = vector.chr_scale_factors.get(&chr_index).ok_or_else(|| {
462 Error::invalid(format!(
463 "expected value vector {} not found",
464 vector_key(normalization, bin_size, unit, Some(chr_index))
465 ))
466 })?;
467 let scale = 1.0f32 / factor;
468 Ok(vector.values.iter().map(|v| v * scale).collect())
469}
470
471pub fn read_normalization_vector(
472 source: &dyn ByteSource,
473 footer: &HiCFooter,
474 version: i64,
475 chr_index: i64,
476 unit: &str,
477 bin_size: i64,
478 normalization: &str,
479) -> Result<Vec<f32>> {
480 let key = vector_key(normalization, bin_size, unit, Some(chr_index));
481 let vector = footer
482 .normalization_vectors
483 .get(&key)
484 .ok_or_else(|| Error::invalid(format!("normalization vector {key} not found")))?;
485 let buffer = source.read_at(vector.position, vector.byte_count.max(0) as usize)?;
486
487 let (header_size, value_size) = if version > 8 {
490 (8usize, 4usize)
491 } else {
492 (4, 8)
493 };
494 if buffer.len() < header_size {
495 return Err(Error::corrupt(
496 source.path(),
497 vector.position,
498 format!("normalization vector {key} is truncated"),
499 ));
500 }
501 let count = if version > 8 {
502 i64::from_le_bytes(buffer[..8].try_into().expect("checked length"))
503 } else {
504 i32::from_le_bytes(buffer[..4].try_into().expect("checked length")) as i64
505 };
506 let declared = (count.max(0) as u64)
512 .checked_mul(value_size as u64)
513 .and_then(|bytes| bytes.checked_add(header_size as u64));
514 if count < 0 || declared.is_none_or(|needed| needed > buffer.len() as u64) {
515 return Err(Error::corrupt(
516 source.path(),
517 vector.position,
518 format!(
519 "normalization vector {key} declares {count} values, which do not fit its {} bytes",
520 buffer.len()
521 ),
522 ));
523 }
524 let body = &buffer[header_size..];
525 Ok(if version > 8 {
526 body.chunks_exact(4)
527 .take(count as usize)
528 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
529 .collect()
530 } else {
531 body.chunks_exact(8)
532 .take(count as usize)
533 .map(|c| f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]) as f32)
534 .collect()
535 })
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::source::testing::MemorySource;
542
543 fn header_bytes(version: i32, frag: bool) -> Vec<u8> {
545 let mut b = b"HIC\0".to_vec();
546 b.extend_from_slice(&version.to_le_bytes());
547 b.extend_from_slice(&4096i64.to_le_bytes()); b.extend_from_slice(b"mm10\0");
549 if version > 8 {
550 b.extend_from_slice(&0i64.to_le_bytes());
551 b.extend_from_slice(&0i64.to_le_bytes());
552 }
553 b.extend_from_slice(&1i32.to_le_bytes()); b.extend_from_slice(b"software\0made-up\0");
555 b.extend_from_slice(&2i32.to_le_bytes()); for (name, size) in [("chr1", 1000i64), ("chr2", 2000)] {
557 b.extend_from_slice(name.as_bytes());
558 b.push(0);
559 if version > 8 {
560 b.extend_from_slice(&size.to_le_bytes());
561 } else {
562 b.extend_from_slice(&(size as i32).to_le_bytes());
563 }
564 }
565 b.extend_from_slice(&2i32.to_le_bytes()); b.extend_from_slice(&5000i32.to_le_bytes());
567 b.extend_from_slice(&10000i32.to_le_bytes());
568 if frag {
569 b.extend_from_slice(&1i32.to_le_bytes());
570 b.extend_from_slice(&1i32.to_le_bytes());
571 b.extend_from_slice(&2i32.to_le_bytes());
574 b.extend_from_slice(&100i32.to_le_bytes());
575 b.extend_from_slice(&250i32.to_le_bytes());
576 b.extend_from_slice(&0i32.to_le_bytes());
577 } else {
578 b.extend_from_slice(&0i32.to_le_bytes());
579 }
580 b
581 }
582
583 #[test]
584 fn reads_a_v8_header() {
585 let source = MemorySource::new(header_bytes(8, false));
586 let h = read_header(&source).unwrap();
587 assert_eq!(h.version, 8);
588 assert_eq!(h.genome_id, "mm10");
589 assert_eq!(h.footer_position, 4096);
590 assert_eq!(
591 h.attributes.get("software").map(String::as_str),
592 Some("made-up")
593 );
594 assert_eq!(h.chr_map.names(), ["chr1", "chr2"]);
595 assert_eq!(h.chr_map.resolve("chr2").unwrap().size, 2000);
596 assert_eq!(h.bp_resolutions, [5000, 10000]);
597 assert!(h.frag_resolutions.is_empty());
598 }
599
600 #[test]
601 fn a_v9_header_widens_its_chromosome_sizes() {
602 let source = MemorySource::new(header_bytes(9, false));
603 let h = read_header(&source).unwrap();
604 assert_eq!(h.version, 9);
605 assert_eq!(h.chr_map.resolve("chr2").unwrap().size, 2000);
606 }
607
608 #[test]
609 fn resolutions_are_reported_per_unit() {
610 let source = MemorySource::new(header_bytes(8, true));
611 let h = read_header(&source).unwrap();
612 assert_eq!(h.resolutions(Unit::Bp), [5000, 10000]);
613 assert_eq!(h.resolutions(Unit::Frag), [1]);
614 assert_eq!(h.sites.keys().collect::<Vec<_>>(), ["chr1", "chr2"]);
616 assert_eq!(h.sites["chr1"], [100, 250]);
617 assert!(h.sites["chr2"].is_empty());
618 }
619
620 #[test]
621 fn a_file_without_fragment_resolutions_reads_no_site_section_at_all() {
622 let source = MemorySource::new(header_bytes(8, false));
625 assert!(read_header(&source).unwrap().sites.is_empty());
626 }
627
628 #[test]
629 fn only_a_v9_header_carries_a_normalization_vector_index() {
630 assert_eq!(
631 read_header(&MemorySource::new(header_bytes(8, false)))
632 .unwrap()
633 .nvi_position,
634 -1
635 );
636 assert_eq!(
637 read_header(&MemorySource::new(header_bytes(9, false)))
638 .unwrap()
639 .nvi_position,
640 0
641 );
642 }
643
644 #[test]
645 fn a_bad_magic_and_an_unsupported_version_are_refused() {
646 let source = MemorySource::new(b"NOPE\x08\0\0\0".to_vec());
647 let err = read_header(&source).unwrap_err().to_string();
648 assert!(err.contains("not a hic file"), "{err}");
649
650 for version in [5i32, 10, 99] {
651 let source = MemorySource::new(header_bytes(version, false));
652 let err = read_header(&source).unwrap_err().to_string();
653 assert!(
654 err.contains("unsupported (6 to 9)"),
655 "version {version}: {err}"
656 );
657 }
658 }
659
660 #[test]
661 fn a_truncated_header_is_corrupt_not_a_panic() {
662 let mut bytes = header_bytes(8, false);
663 bytes.truncate(20);
664 assert!(matches!(
665 read_header(&MemorySource::new(bytes)),
666 Err(Error::Corrupt { .. })
667 ));
668 }
669
670 #[test]
671 fn the_vector_key_is_built_the_same_way_both_ways() {
672 assert_eq!(
673 vector_key("kr", 5000, "bp", None),
674 "normalization=kr|bin_size=5000|unit=bp"
675 );
676 assert_eq!(
677 vector_key("kr", 5000, "bp", Some(3)),
678 "chr_index=3|normalization=kr|bin_size=5000|unit=bp"
679 );
680 }
681
682 #[test]
683 fn expected_values_are_scaled_by_the_chromosomes_own_factor() {
684 let mut footer = HiCFooter::default();
685 let mut factors = IndexMap::new();
686 factors.insert(0i64, 2.0f32);
687 footer.expected_value_vectors.insert(
688 vector_key("none", 5000, "bp", None),
689 ExpectedValueVector {
690 normalization: "none".into(),
691 unit: "bp".into(),
692 bin_size: 5000,
693 values: vec![10.0, 20.0],
694 chr_scale_factors: factors,
695 },
696 );
697 let got = compute_expected_values(&footer, 0, "bp", 5000, "none").unwrap();
699 assert_eq!(got, [5.0, 10.0]);
700 let err = compute_expected_values(&footer, 7, "bp", 5000, "none")
702 .unwrap_err()
703 .to_string();
704 assert!(err.contains("chr_index=7"), "{err}");
705 }
706}