1use crate::error::{Error, Result};
24
25#[derive(Debug, Clone, Default)]
30pub struct CooMatrix {
31 pub values: Vec<f32>,
32 pub row: Vec<u32>,
33 pub col: Vec<u32>,
34 pub shape: (usize, usize),
35}
36
37pub fn bilinear(
44 data: &[f32],
45 shape: (usize, usize),
46 new_shape: (usize, usize),
47) -> Result<Vec<f32>> {
48 let (old_rows, old_cols) = shape;
49 let (new_rows, new_cols) = new_shape;
50 if data.len() != old_rows * old_cols {
51 return Err(Error::invalid(
52 "bilinear resize: data size does not match shape",
53 ));
54 }
55 if data.is_empty() && new_rows > 0 && new_cols > 0 {
56 return Err(Error::invalid(
57 "bilinear resize: cannot resize an empty array to a non-empty shape",
58 ));
59 }
60 if new_rows == 0 || new_cols == 0 {
61 return Ok(Vec::new());
62 }
63
64 let (row_scale, col_scale) = scales((old_rows, old_cols), (new_rows, new_cols));
65
66 let mut out = vec![0.0f32; new_rows * new_cols];
67 for i in 0..new_rows {
68 let sr = i as f64 * row_scale;
71 let r0 = sr as usize;
72 let r1 = (r0 + 1).min(old_rows - 1);
73 let rf = sr - r0 as f64;
74 let (top, bottom) = (r0 * old_cols, r1 * old_cols);
75
76 for j in 0..new_cols {
77 let sc = j as f64 * col_scale;
78 let c0 = sc as usize;
79 let c1 = (c0 + 1).min(old_cols - 1);
80 let cf = sc - c0 as f64;
81
82 let tl = data[top + c0] as f64;
83 let tr = data[top + c1] as f64;
84 let bl = data[bottom + c0] as f64;
85 let br = data[bottom + c1] as f64;
86 out[i * new_cols + j] = interpolate(tl, tr, bl, br, rf, cf);
87 }
88 }
89 Ok(out)
90}
91
92pub fn bilinear_sparse(data: &CooMatrix, new_shape: (usize, usize)) -> Result<CooMatrix> {
101 let (old_rows, old_cols) = data.shape;
102 let (new_rows, new_cols) = new_shape;
103
104 if data.row.len() != data.values.len() || data.col.len() != data.values.len() {
105 return Err(Error::invalid(
106 "bilinear resize: values, row and col must have equal length",
107 ));
108 }
109 for i in 0..data.values.len() {
110 if data.row[i] as usize >= old_rows || data.col[i] as usize >= old_cols {
111 return Err(Error::invalid(
112 "bilinear resize: coordinate outside declared shape",
113 ));
114 }
115 }
116 if (old_rows == 0 || old_cols == 0) && new_rows > 0 && new_cols > 0 {
117 return Err(Error::invalid(
118 "bilinear resize: cannot resize an empty array to a non-empty shape",
119 ));
120 }
121 if new_rows == 0 || new_cols == 0 {
124 return Ok(CooMatrix {
125 shape: new_shape,
126 ..Default::default()
127 });
128 }
129
130 let (row_scale, col_scale) = scales((old_rows, old_cols), (new_rows, new_cols));
131
132 let mut sparse_map = std::collections::HashMap::with_capacity(data.values.len());
134 for i in 0..data.values.len() {
135 let key = ((data.row[i] as u64) << 32) | data.col[i] as u64;
136 *sparse_map.entry(key).or_insert(0.0f32) += data.values[i];
137 }
138 let get = |r: usize, c: usize| -> f32 {
139 sparse_map
140 .get(&(((r as u64) << 32) | c as u64))
141 .copied()
142 .unwrap_or(0.0)
143 };
144
145 let mut targets = std::collections::HashSet::new();
150 let full_output = new_rows * new_cols;
151 for idx in 0..data.values.len() {
152 if data.values[idx] == 0.0 {
157 continue;
158 }
159 let (r_min, r_max) = influence(data.row[idx], row_scale, new_rows);
169 let (c_min, c_max) = influence(data.col[idx], col_scale, new_cols);
170 for r in r_min..=r_max {
171 for c in c_min..=c_max {
172 targets.insert(((r as u64) << 32) | c as u64);
173 }
174 }
175 if targets.len() >= full_output {
179 break;
180 }
181 }
182
183 let mut ordered: Vec<u64> = targets.into_iter().collect();
186 ordered.sort_unstable();
187
188 let mut out = CooMatrix {
189 shape: new_shape,
190 values: Vec::with_capacity(ordered.len()),
191 row: Vec::with_capacity(ordered.len()),
192 col: Vec::with_capacity(ordered.len()),
193 };
194 for key in ordered {
195 let (or_, oc) = ((key >> 32) as u32, key as u32);
196 let sr = or_ as f64 * row_scale;
197 let sc = oc as f64 * col_scale;
198 let r0 = sr as usize;
199 let c0 = sc as usize;
200 let r1 = (r0 + 1).min(old_rows - 1);
201 let c1 = (c0 + 1).min(old_cols - 1);
202 let (rf, cf) = (sr - r0 as f64, sc - c0 as f64);
203
204 let interp = interpolate(
205 get(r0, c0) as f64,
206 get(r0, c1) as f64,
207 get(r1, c0) as f64,
208 get(r1, c1) as f64,
209 rf,
210 cf,
211 );
212 if interp != 0.0 {
213 out.row.push(or_);
214 out.col.push(oc);
215 out.values.push(interp);
216 }
217 }
218 Ok(out)
219}
220
221fn interpolate(tl: f64, tr: f64, bl: f64, br: f64, rf: f64, cf: f64) -> f32 {
226 let top = tl.mul_add(1.0 - cf, tr * cf);
227 let bottom = bl.mul_add(1.0 - cf, br * cf);
228 top.mul_add(1.0 - rf, bottom * rf) as f32
229}
230
231fn scales(old: (usize, usize), new: (usize, usize)) -> (f64, f64) {
234 let row = if new.0 > 1 {
235 (old.0 - 1) as f64 / (new.0 - 1) as f64
236 } else {
237 0.0
238 };
239 let col = if new.1 > 1 {
240 (old.1 - 1) as f64 / (new.1 - 1) as f64
241 } else {
242 0.0
243 };
244 (row, col)
245}
246
247fn influence(src: u32, scale: f64, new_len: usize) -> (u32, u32) {
249 if scale == 0.0 {
250 return (0, new_len as u32 - 1);
251 }
252 let out = src as f64 / scale;
253 let radius = 1.0 / scale;
254 let min = (out - radius).floor().max(0.0) as u32;
255 let max = (out + radius).ceil().min((new_len - 1) as f64) as u32;
256 (min, max)
257}
258
259pub fn compress_sparse_by_color(
283 values: &[f32],
284 row: &[u32],
285 col: &[u32],
286 color_count: u32,
287) -> Result<Vec<Vec<u32>>> {
288 if color_count == 0 {
289 return Err(Error::invalid(
290 "compress_sparse_by_color: color_count must be positive",
291 ));
292 }
293 let mut result = vec![Vec::<u32>::new(); color_count as usize];
294 if values.is_empty() {
295 return Ok(result);
296 }
297
298 let min_value = 0.0f32;
302 let mut max_value = min_value;
303 for &value in values {
304 if value.is_finite() && value > max_value {
308 max_value = value;
309 }
310 }
311 if max_value <= min_value {
313 return Ok(result);
314 }
315 let range = max_value - min_value;
316 let scale = color_count as f32;
321 let last_bin = color_count - 1;
322
323 for i in 0..values.len().min(row.len()).min(col.len()) {
324 if !values[i].is_finite() {
325 continue;
326 }
327 let bin = ((values[i] - min_value) / range * scale).floor();
328 let value_bin = if bin <= 0.0 {
331 0
332 } else if bin >= last_bin as f32 {
333 last_bin
334 } else {
335 bin as u32
336 };
337
338 let spans = &mut result[value_bin as usize];
339 let n = spans.len();
340 if n >= 3 && spans[n - 3] == row[i] && spans[n - 2] + spans[n - 1] == col[i] {
341 spans[n - 1] += 1;
342 continue;
343 }
344 spans.extend_from_slice(&[row[i], col[i], 1]);
345 }
346 Ok(result)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn coo(entries: &[(u32, u32, f32)], shape: (usize, usize)) -> CooMatrix {
354 CooMatrix {
355 values: entries.iter().map(|e| e.2).collect(),
356 row: entries.iter().map(|e| e.0).collect(),
357 col: entries.iter().map(|e| e.1).collect(),
358 shape,
359 }
360 }
361
362 #[test]
363 fn resizing_to_its_own_shape_is_the_identity() {
364 let data: Vec<f32> = (0..12).map(|v| v as f32).collect();
365 assert_eq!(bilinear(&data, (3, 4), (3, 4)).unwrap(), data);
366 }
367
368 #[test]
369 fn the_corners_are_kept_whatever_the_new_shape() {
370 let data = vec![1.0f32, 2.0, 3.0, 4.0];
371 let out = bilinear(&data, (2, 2), (5, 5)).unwrap();
372 assert_eq!(out[0], 1.0);
373 assert_eq!(out[4], 2.0);
374 assert_eq!(out[20], 3.0);
375 assert_eq!(out[24], 4.0);
376 }
377
378 #[test]
379 fn a_midpoint_is_the_mean_of_its_four_neighbours() {
380 let data = vec![0.0f32, 10.0, 20.0, 30.0];
381 let out = bilinear(&data, (2, 2), (3, 3)).unwrap();
382 assert_eq!(out[4], 15.0);
384 assert_eq!(out[1], 5.0);
386 assert_eq!(out[3], 10.0);
387 }
388
389 #[test]
390 fn shrinking_samples_rather_than_averages() {
391 let data: Vec<f32> = (0..16).map(|v| v as f32).collect();
392 let out = bilinear(&data, (4, 4), (2, 2)).unwrap();
393 assert_eq!(out, [0.0, 3.0, 12.0, 15.0]);
395 }
396
397 #[test]
398 fn a_single_output_cell_samples_the_first_source_one() {
399 let data = vec![7.0f32, 8.0, 9.0, 10.0];
400 assert_eq!(bilinear(&data, (2, 2), (1, 1)).unwrap(), [7.0]);
401 }
402
403 #[test]
404 fn a_mismatched_shape_is_refused_rather_than_read_past() {
405 let err = bilinear(&[1.0, 2.0], (3, 4), (2, 2))
411 .unwrap_err()
412 .to_string();
413 assert_eq!(err, "bilinear resize: data size does not match shape");
414 let err = bilinear(&[], (0, 0), (2, 2)).unwrap_err().to_string();
415 assert_eq!(
416 err,
417 "bilinear resize: cannot resize an empty array to a non-empty shape"
418 );
419 }
420
421 #[test]
422 fn an_empty_target_is_empty_not_an_error() {
423 assert!(bilinear(&[1.0, 2.0, 3.0, 4.0], (2, 2), (0, 5))
424 .unwrap()
425 .is_empty());
426 }
427
428 #[test]
431 fn a_sparse_resize_agrees_with_the_dense_one_cell_for_cell() {
432 let entries = [(0u32, 0u32, 1.0f32), (1, 2, 5.0), (3, 3, -2.0), (2, 1, 4.5)];
435 let sparse = coo(&entries, (4, 4));
436 let mut dense = vec![0.0f32; 16];
437 for (r, c, v) in entries {
438 dense[r as usize * 4 + c as usize] = v;
439 }
440 for new_shape in [(2, 2), (4, 4), (7, 7), (3, 5)] {
441 let want = bilinear(&dense, (4, 4), new_shape).unwrap();
442 let got = bilinear_sparse(&sparse, new_shape).unwrap();
443 assert_eq!(got.shape, new_shape);
444 for i in 0..got.values.len() {
445 let flat = got.row[i] as usize * new_shape.1 + got.col[i] as usize;
446 assert_eq!(got.values[i], want[flat], "{new_shape:?} entry {i}");
447 }
448 let listed: std::collections::HashSet<usize> = (0..got.values.len())
450 .map(|i| got.row[i] as usize * new_shape.1 + got.col[i] as usize)
451 .collect();
452 for (flat, value) in want.iter().enumerate() {
453 assert!(
454 *value == 0.0 || listed.contains(&flat),
455 "{new_shape:?} {flat}"
456 );
457 }
458 }
459 }
460
461 #[test]
462 fn a_sparse_resize_comes_back_in_row_major_order() {
463 let sparse = coo(
466 &[(5, 5, 1.0), (0, 9, 2.0), (9, 0, 3.0), (2, 2, 4.0)],
467 (10, 10),
468 );
469 let out = bilinear_sparse(&sparse, (6, 6)).unwrap();
470 let keys: Vec<u64> = (0..out.values.len())
471 .map(|i| ((out.row[i] as u64) << 32) | out.col[i] as u64)
472 .collect();
473 assert!(keys.windows(2).all(|w| w[0] < w[1]), "{keys:?}");
474 }
475
476 #[test]
477 fn repeated_sparse_coordinates_accumulate() {
478 let sparse = coo(&[(0, 0, 1.0), (0, 0, 2.0)], (2, 2));
479 let out = bilinear_sparse(&sparse, (2, 2)).unwrap();
480 assert_eq!(out.values[0], 3.0);
481 }
482
483 #[test]
484 fn a_sparse_coordinate_outside_the_shape_is_refused() {
485 let sparse = coo(&[(4, 0, 1.0)], (2, 2));
486 let err = bilinear_sparse(&sparse, (2, 2)).unwrap_err().to_string();
487 assert_eq!(err, "bilinear resize: coordinate outside declared shape");
488 }
489
490 fn cells_of(spans: &[u32]) -> Vec<u32> {
493 spans.chunks(3).flat_map(|s| s[1]..s[1] + s[2]).collect()
494 }
495
496 #[test]
497 fn the_colour_scale_is_split_into_equal_bins() {
498 let values: Vec<f32> = (0..=100).map(|v| v as f32).collect();
501 let row = vec![0u32; 101];
502 let col: Vec<u32> = (0..101).collect();
503 let bins = compress_sparse_by_color(&values, &row, &col, 4).unwrap();
504
505 let counts: Vec<usize> = bins.iter().map(|b| cells_of(b).len()).collect();
506 assert!(
507 counts.iter().max().unwrap() - counts.iter().min().unwrap() <= 1,
508 "{counts:?}"
509 );
510 assert!(
511 counts[3] > 1 && cells_of(&bins[3]).contains(&100),
512 "{counts:?}"
513 );
514 let mut all: Vec<u32> = bins.iter().flat_map(|b| cells_of(b)).collect();
515 all.sort_unstable();
516 assert_eq!(all, (0..101).collect::<Vec<u32>>());
517 }
518
519 #[test]
520 fn a_run_of_one_colour_is_a_single_span() {
521 let values = vec![5.0f32; 200];
522 let row = vec![0u32; 200];
523 let col: Vec<u32> = (0..200).collect();
524 let bins = compress_sparse_by_color(&values, &row, &col, 4).unwrap();
525 let spans: usize = bins.iter().map(|b| b.len() / 3).sum();
526 assert_eq!(spans, 1);
527 }
528
529 #[test]
530 fn a_new_row_opens_a_span_rather_than_extending_the_last() {
531 let values = vec![5.0f32; 4];
532 let bins = compress_sparse_by_color(&values, &[0, 0, 1, 1], &[0, 1, 0, 1], 1).unwrap();
533 assert_eq!(bins[0], [0, 0, 2, 1, 0, 2]);
534 }
535
536 #[test]
537 fn non_finite_values_take_no_part_in_the_scale_or_the_output() {
538 let values = [0.5f32, f32::INFINITY, 2.0, f32::NAN];
539 let bins = compress_sparse_by_color(&values, &[0; 4], &[0, 1, 2, 3], 2).unwrap();
540 let listed: Vec<u32> = bins.iter().flat_map(|b| cells_of(b)).collect();
541 assert_eq!(listed.len(), 2, "{bins:?}");
542 assert_eq!(cells_of(&bins[0]), [0]);
547 assert_eq!(cells_of(&bins[1]), [2]);
548 }
549
550 #[test]
551 fn a_matrix_with_no_scale_comes_back_as_empty_bins() {
552 let bins = compress_sparse_by_color(&[0.0, -3.0], &[0, 0], &[0, 1], 3).unwrap();
553 assert_eq!(bins.len(), 3);
554 assert!(bins.iter().all(|b| b.is_empty()));
555 assert_eq!(compress_sparse_by_color(&[], &[], &[], 2).unwrap().len(), 2);
556 }
557
558 #[test]
559 fn a_zero_color_count_is_refused() {
560 let err = compress_sparse_by_color(&[1.0], &[0], &[0], 0)
561 .unwrap_err()
562 .to_string();
563 assert!(err.contains("must be positive"), "{err}");
564 }
566}