1use indexmap::IndexMap;
4
5use crate::bytes::LeCursor;
6use crate::error::{Error, Result};
7use crate::genomic::{ChrEntry, ChrMap};
8use crate::source::ByteSource;
9
10use super::header::HiCIndexItem;
11
12#[derive(Debug, Clone)]
14pub struct Side {
15 pub chr: ChrEntry,
16 pub start: i64,
17 pub end: i64,
18 pub binned_start: i64,
19 pub binned_end: i64,
20 pub bin_start: i64,
21 pub bin_end: i64,
22}
23
24#[derive(Debug, Clone)]
26pub struct Loc2D {
27 pub x: Side,
28 pub y: Side,
29 pub bin_size: i64,
30 pub reversed: bool,
34}
35
36impl Loc2D {
37 #[inline]
38 pub fn is_intra(&self) -> bool {
39 self.x.chr.index == self.y.chr.index
40 }
41
42 pub fn distance_from_diagonal(&self, x: i64, y: i64) -> i64 {
49 if self.is_intra() {
50 return (y - x).abs();
51 }
52 let x_span = self.x.binned_end - self.x.binned_start;
53 let y_span = self.y.binned_end - self.y.binned_start;
54 if x_span <= 0 || y_span <= 0 {
55 return 0;
56 }
57 let a = y_span as f64 / x_span as f64;
58 let b = self.y.binned_start as f64 - a * self.x.binned_start as f64;
59 let vertical = (y as f64 - (a * x as f64 + b)).abs();
60 let horizontal = (x as f64 - (y as f64 - b) / a).abs();
61 vertical.min(horizontal).round() as i64
62 }
63}
64
65#[allow(clippy::too_many_arguments)]
76pub fn parse_loc2d(
77 map: &ChrMap,
78 available_bin_sizes: &[i64],
79 chr_ids: &[String],
80 starts: &[i64],
81 ends: &[i64],
82 bin_size: Option<i64>,
83 bin_count: Option<i64>,
84 full_bin: bool,
85) -> Result<Loc2D> {
86 if available_bin_sizes.is_empty() {
90 return Err(Error::invalid("file has no resolution for this unit"));
91 }
92 if bin_count == Some(0) {
93 return Err(Error::invalid(
94 "bin count must be positive, or negative to disregard it",
95 ));
96 }
97 if bin_size == Some(0) {
100 return Err(Error::invalid(
101 "bin size must be positive, or negative to use the finest available",
102 ));
103 }
104
105 let pair = |values: &[i64], what: &str| -> Result<(i64, i64)> {
106 match values {
107 [only] => Ok((*only, *only)),
108 [a, b] => Ok((*a, *b)),
109 _ => Err(Error::invalid(format!("1 or 2 {what} must be specified"))),
110 }
111 };
112 let ids = match chr_ids {
113 [only] => (only.clone(), only.clone()),
114 [a, b] => (a.clone(), b.clone()),
115 _ => return Err(Error::invalid("1 or 2 chromosomes must be specified")),
116 };
117 let (x_start, y_start) = pair(starts, "start positions")?;
118 let (x_end, y_end) = pair(ends, "end positions")?;
119
120 let make = |id: &str, start: i64, end: i64| -> Result<Side> {
121 let chr = map.resolve(id)?.clone();
122 if start > end {
123 return Err(Error::invalid(format!(
124 "window {}:{start}-{end} ends before it starts",
125 chr.id
126 )));
127 }
128 Ok(Side {
129 chr,
130 start,
131 end,
132 binned_start: 0,
133 binned_end: 0,
134 bin_start: 0,
135 bin_end: 0,
136 })
137 };
138 let mut x = make(&ids.0, x_start, x_end)?;
139 let mut y = make(&ids.1, y_start, y_end)?;
140
141 let mut reversed = false;
145 if x.chr.index > y.chr.index {
146 std::mem::swap(&mut x, &mut y);
147 reversed = true;
148 }
149
150 let bin_size = match bin_count {
151 Some(count) if count > 0 => {
152 let span = ((x.end - x.start) + (y.end - y.start)) / 2;
153 let wanted = (span + count - 1) / count;
154 *available_bin_sizes
155 .iter()
156 .min_by_key(|available| (*available - wanted).abs())
157 .expect("checked non-empty")
158 }
159 _ => match bin_size {
160 Some(size) if size > 0 => size,
161 _ => *available_bin_sizes.iter().min().expect("checked non-empty"),
162 },
163 };
164
165 for side in [&mut x, &mut y] {
166 side.binned_start = side.start / bin_size * bin_size;
167 side.binned_end = if full_bin {
168 (side.end + bin_size - 1) / bin_size * bin_size
169 } else {
170 side.end / bin_size * bin_size
171 };
172 side.bin_start = side.binned_start / bin_size;
173 side.bin_end = side.binned_end / bin_size;
174 }
175
176 Ok(Loc2D {
177 x,
178 y,
179 bin_size,
180 reversed,
181 })
182}
183
184#[derive(Debug, Clone)]
187pub struct MatrixMetadata {
188 pub chr1_index: i64,
189 pub chr2_index: i64,
190 pub unit: String,
191 pub bin_size: i64,
192 pub sum_counts: f32,
193 pub block_bin_count: i64,
194 pub block_column_count: i64,
195 pub blocks: IndexMap<i64, HiCIndexItem>,
196}
197
198pub fn matrix_key(chr1: i64, chr2: i64, bin_size: i64, unit: &str) -> String {
200 format!("chr_index={chr1}_{chr2}|bin_size={bin_size}|unit={unit}")
201}
202
203pub fn read_matrix_metadata(
204 source: &dyn ByteSource,
205 item: HiCIndexItem,
206 chr1_index: i64,
207 chr2_index: i64,
208) -> Result<Vec<MatrixMetadata>> {
209 let path = source.path();
210 let buf = source.read_at(item.position, item.size.max(0) as usize)?;
213 let mut c = LeCursor::new(&buf, item.position, path);
214
215 let file_chr1 = c.read_i32()? as i64;
216 let file_chr2 = c.read_i32()? as i64;
217 let bin_size_count = c.read_i32()? as i64;
218 if file_chr1 != chr1_index || file_chr2 != chr2_index {
219 return Err(Error::corrupt(
220 path,
221 item.position,
222 "matrix metadata chr indices mismatch",
223 ));
224 }
225
226 let mut matrices = Vec::with_capacity(bin_size_count.clamp(0, 64) as usize);
227 for _ in 0..bin_size_count.max(0) {
228 let unit = c.take_cstr()?.to_ascii_lowercase();
229 c.skip(4)?; let sum_counts = c.read_f32()?;
231 c.skip(4)?; c.skip(4)?; c.skip(4)?; let bin_size = c.read_i32()? as i64;
235 let block_bin_count = c.read_i32()? as i64;
236 let block_column_count = c.read_i32()? as i64;
237 let block_count = c.read_i32()? as i64;
238
239 let mut blocks = IndexMap::with_capacity(block_count.clamp(0, 1 << 16) as usize);
240 for _ in 0..block_count.max(0) {
241 let number = c.read_i32()? as i64;
242 let position = c.read_u64()?;
243 let size = c.read_i32()? as i64;
244 blocks.insert(number, HiCIndexItem { position, size });
245 }
246 matrices.push(MatrixMetadata {
247 chr1_index: file_chr1,
248 chr2_index: file_chr2,
249 unit,
250 bin_size,
251 sum_counts,
252 block_bin_count,
253 block_column_count,
254 blocks,
255 });
256 }
257 Ok(matrices)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn map() -> ChrMap {
265 ChrMap::from_indexed_entries([
266 ("chr1".to_string(), 1_000_000, 0),
267 ("chr2".to_string(), 500_000, 1),
268 ])
269 }
270
271 fn loc(chr_ids: &[&str], starts: &[i64], ends: &[i64], bin: Option<i64>) -> Result<Loc2D> {
272 let ids: Vec<String> = chr_ids.iter().map(|s| s.to_string()).collect();
273 parse_loc2d(
274 &map(),
275 &[5000, 10000, 25000],
276 &ids,
277 starts,
278 ends,
279 bin,
280 None,
281 false,
282 )
283 }
284
285 #[test]
286 fn one_chromosome_is_used_for_both_axes() {
287 let l = loc(&["chr1"], &[0], &[100_000], Some(5000)).unwrap();
288 assert_eq!(l.x.chr.id, "chr1");
289 assert_eq!(l.y.chr.id, "chr1");
290 assert!(l.is_intra());
291 assert_eq!((l.x.bin_start, l.x.bin_end), (0, 20));
292 }
293
294 #[test]
295 fn the_axes_swap_when_the_second_chromosome_sorts_first() {
296 let l = loc(&["chr2", "chr1"], &[0, 0], &[100_000, 100_000], Some(5000)).unwrap();
297 assert!(l.reversed, "the request named them the other way round");
298 assert_eq!(l.x.chr.id, "chr1", "the lower chromosome is read first");
299 assert_eq!(l.y.chr.id, "chr2");
300
301 let l = loc(&["chr1", "chr2"], &[0, 0], &[100_000, 100_000], Some(5000)).unwrap();
303 assert!(!l.reversed);
304 }
305
306 #[test]
307 fn a_negative_bin_size_takes_the_finest_resolution() {
308 assert_eq!(
309 loc(&["chr1"], &[0], &[100_000], Some(-1)).unwrap().bin_size,
310 5000
311 );
312 assert_eq!(
313 loc(&["chr1"], &[0], &[100_000], None).unwrap().bin_size,
314 5000
315 );
316 }
317
318 #[test]
319 fn a_bin_count_picks_the_nearest_available_resolution() {
320 let ids = ["chr1".to_string()];
321 let l = parse_loc2d(
323 &map(),
324 &[5000, 10000, 25000],
325 &ids,
326 &[0],
327 &[100_000],
328 None,
329 Some(10),
330 false,
331 )
332 .unwrap();
333 assert_eq!(l.bin_size, 10000);
334 let l = parse_loc2d(
336 &map(),
337 &[5000, 10000, 25000],
338 &ids,
339 &[0],
340 &[100_000],
341 None,
342 Some(4),
343 false,
344 )
345 .unwrap();
346 assert_eq!(l.bin_size, 25000);
347 let l = parse_loc2d(
349 &map(),
350 &[5000, 10000, 25000],
351 &ids,
352 &[0],
353 &[100_000],
354 None,
355 Some(7),
356 false,
357 )
358 .unwrap();
359 assert_eq!(l.bin_size, 10000);
360 }
361
362 #[test]
363 fn full_bin_rounds_the_end_up_instead_of_down() {
364 let down = loc(&["chr1"], &[0], &[12_000], Some(5000)).unwrap();
365 assert_eq!(down.x.bin_end, 2); let ids = ["chr1".to_string()];
367 let up = parse_loc2d(
368 &map(),
369 &[5000],
370 &ids,
371 &[0],
372 &[12_000],
373 Some(5000),
374 None,
375 true,
376 )
377 .unwrap();
378 assert_eq!(up.x.bin_end, 3);
379 }
380
381 #[test]
382 fn the_refusals_say_what_was_wrong() {
383 let ids = ["chr1".to_string()];
384 let err = parse_loc2d(&map(), &[], &ids, &[0], &[10], None, None, false)
385 .unwrap_err()
386 .to_string();
387 assert!(err.contains("no resolution for this unit"), "{err}");
388
389 let err = parse_loc2d(&map(), &[5000], &ids, &[0], &[10], Some(0), None, false)
390 .unwrap_err()
391 .to_string();
392 assert!(err.contains("bin size must be positive"), "{err}");
393
394 let err = parse_loc2d(&map(), &[5000], &ids, &[0], &[10], None, Some(0), false)
395 .unwrap_err()
396 .to_string();
397 assert!(err.contains("bin count must be positive"), "{err}");
398
399 let err = loc(&["chr1"], &[100], &[10], Some(5000))
400 .unwrap_err()
401 .to_string();
402 assert!(err.contains("ends before it starts"), "{err}");
403
404 let three = ["chr1".to_string(), "chr2".to_string(), "chr1".to_string()];
405 let err = parse_loc2d(&map(), &[5000], &three, &[0], &[10], None, None, false)
406 .unwrap_err()
407 .to_string();
408 assert!(err.contains("1 or 2 chromosomes"), "{err}");
409 }
410
411 #[test]
412 fn distance_from_the_diagonal_is_the_offset_on_one_chromosome() {
413 let l = loc(&["chr1"], &[0], &[100_000], Some(5000)).unwrap();
414 assert_eq!(l.distance_from_diagonal(10_000, 10_000), 0);
415 assert_eq!(l.distance_from_diagonal(10_000, 35_000), 25_000);
416 assert_eq!(l.distance_from_diagonal(35_000, 10_000), 25_000);
417 }
418
419 #[test]
420 fn across_two_chromosomes_it_is_measured_against_the_windows_own_line() {
421 let l = loc(&["chr1", "chr2"], &[0, 0], &[100_000, 100_000], Some(5000)).unwrap();
422 assert!(!l.is_intra());
423 assert_eq!(l.distance_from_diagonal(20_000, 20_000), 0);
425 assert!(l.distance_from_diagonal(20_000, 60_000) > 0);
426 }
427
428 #[test]
429 fn the_matrix_key_is_built_the_same_way_both_ways() {
430 assert_eq!(
431 matrix_key(0, 1, 5000, "bp"),
432 "chr_index=0_1|bin_size=5000|unit=bp"
433 );
434 }
435}