1use rayon::prelude::*;
9
10use crate::allocation::{
11 checked_add_allocation_bytes, checked_allocation_bytes, checked_allocation_len,
12 checked_capacity_bytes, try_vec_filled, try_vec_reserve_len, try_vec_resize_with,
13 TranscodeAllocationError,
14};
15use crate::dct_grid::{high_len, idct8_basis_table, low_len, validate_dct_block_grid};
16use crate::{DctTransformError, Dwt97TwoDimensional};
17
18#[cfg(test)]
19use crate::dct_grid::idct8_basis;
20
21const ALPHA: f64 = j2k_codec_math::dwt::DWT97_ALPHA_F64;
22const BETA: f64 = j2k_codec_math::dwt::DWT97_BETA_F64;
23const GAMMA: f64 = j2k_codec_math::dwt::DWT97_GAMMA_F64;
24const DELTA: f64 = j2k_codec_math::dwt::DWT97_DELTA_F64;
25const KAPPA: f64 = j2k_codec_math::dwt::DWT97_KAPPA_F64;
26const INV_KAPPA: f64 = j2k_codec_math::dwt::DWT97_INV_KAPPA_F64;
27const PARALLEL_IDCT_MIN_SAMPLES: usize = 64 * 64;
28
29#[cfg(test)]
30impl Dwt97TwoDimensional<f64> {
31 #[must_use]
33 pub(crate) fn max_abs_diff(&self, other: &Self) -> f64 {
34 assert_eq!(self.low_width, other.low_width);
35 assert_eq!(self.low_height, other.low_height);
36 assert_eq!(self.high_width, other.high_width);
37 assert_eq!(self.high_height, other.high_height);
38
39 self.ll
40 .iter()
41 .zip(other.ll.iter())
42 .chain(self.hl.iter().zip(other.hl.iter()))
43 .chain(self.lh.iter().zip(other.lh.iter()))
44 .chain(self.hh.iter().zip(other.hh.iter()))
45 .map(|(actual, expected)| (actual - expected).abs())
46 .fold(0.0, f64::max)
47 }
48}
49
50#[derive(Debug, Default)]
52pub struct Dct97GridScratch {
53 geometry: Option<(usize, usize)>,
54 spatial_samples: Vec<f64>,
55 plane: Dwt97PlaneScratch,
56}
57
58#[derive(Debug, Default)]
59struct Dwt97PlaneScratch {
60 row_low: Vec<f64>,
61 row_high: Vec<f64>,
62 lift_workspace: Vec<f64>,
63}
64
65impl Dct97GridScratch {
66 pub(crate) fn retained_bytes(&self) -> Result<usize, TranscodeAllocationError> {
67 let mut total = checked_capacity_bytes::<f64>(self.spatial_samples.capacity())?;
68 for capacity in [
69 self.plane.row_low.capacity(),
70 self.plane.row_high.capacity(),
71 self.plane.lift_workspace.capacity(),
72 ] {
73 total = checked_add_allocation_bytes(total, checked_capacity_bytes::<f64>(capacity)?)?;
74 }
75 Ok(total)
76 }
77
78 #[cfg(test)]
79 fn spatial_sample_capacity(&self) -> usize {
80 self.spatial_samples.capacity()
81 }
82}
83
84pub fn dct8x8_blocks_then_dwt97_float(
87 blocks: &[[[f64; 8]; 8]],
88 block_cols: usize,
89 block_rows: usize,
90 width: usize,
91 height: usize,
92) -> Result<Dwt97TwoDimensional<f64>, DctTransformError> {
93 let mut scratch = Dct97GridScratch::default();
94 dct8x8_blocks_then_dwt97_float_with_scratch(
95 blocks,
96 block_cols,
97 block_rows,
98 width,
99 height,
100 &mut scratch,
101 )
102}
103
104pub fn dct8x8_blocks_then_dwt97_float_with_scratch(
107 blocks: &[[[f64; 8]; 8]],
108 block_cols: usize,
109 block_rows: usize,
110 width: usize,
111 height: usize,
112 scratch: &mut Dct97GridScratch,
113) -> Result<Dwt97TwoDimensional<f64>, DctTransformError> {
114 validate_grid(blocks.len(), block_cols, block_rows, width, height)?;
115 let sample_count = checked_allocation_len::<f64>(width, height)?;
116 validate_grid_workspace(sample_count, width, height)?;
117 if scratch.geometry != Some((width, height)) {
118 scratch.plane = Dwt97PlaneScratch::default();
119 scratch.geometry = Some((width, height));
120 }
121
122 try_vec_resize_with(&mut scratch.spatial_samples, sample_count, || 0.0)?;
123 let block_row_sample_count = checked_allocation_len::<u8>(width, 8)?;
124 idct8x8_blocks_to_samples(
125 blocks,
126 block_cols,
127 width,
128 height,
129 block_row_sample_count,
130 &mut scratch.spatial_samples,
131 );
132
133 linearized_97_2d_from_plane_with_plane_scratch(
134 &scratch.spatial_samples,
135 width,
136 height,
137 &mut scratch.plane,
138 )
139}
140
141#[cfg(test)]
142pub(crate) fn linearized_97_2d_from_plane(
143 samples: &[f64],
144 width: usize,
145 height: usize,
146) -> Result<Dwt97TwoDimensional<f64>, DctTransformError> {
147 let mut scratch = Dct97GridScratch::default();
148 linearized_97_2d_from_plane_with_scratch(samples, width, height, &mut scratch)
149}
150
151pub(crate) fn linearized_97_2d_from_plane_with_scratch(
152 samples: &[f64],
153 width: usize,
154 height: usize,
155 scratch: &mut Dct97GridScratch,
156) -> Result<Dwt97TwoDimensional<f64>, DctTransformError> {
157 let sample_count = validate_sample_plane(samples, width, height)?;
158 validate_plane_workspace(sample_count, width, height)?;
159 if scratch.geometry == Some((width, height)) {
160 scratch.spatial_samples = Vec::new();
161 } else {
162 *scratch = Dct97GridScratch::default();
163 scratch.geometry = Some((width, height));
164 }
165 linearized_97_2d_from_plane_with_plane_scratch(samples, width, height, &mut scratch.plane)
166}
167
168fn linearized_97_2d_from_plane_with_plane_scratch(
169 samples: &[f64],
170 width: usize,
171 height: usize,
172 scratch: &mut Dwt97PlaneScratch,
173) -> Result<Dwt97TwoDimensional<f64>, DctTransformError> {
174 let low_width = low_len(width);
175 let low_height = low_len(height);
176 let high_width = high_len(width);
177 let high_height = high_len(height);
178
179 try_vec_resize_with(
180 &mut scratch.row_low,
181 checked_allocation_len::<f64>(height, low_width)?,
182 || 0.0,
183 )?;
184 try_vec_resize_with(
185 &mut scratch.row_high,
186 checked_allocation_len::<f64>(height, high_width)?,
187 || 0.0,
188 )?;
189 scratch.lift_workspace.clear();
190 try_vec_reserve_len(&mut scratch.lift_workspace, width.max(height))?;
191
192 for y in 0..height {
193 let start = y * width;
194 let row = &samples[start..start + width];
195 let low_start = y * low_width;
196 let high_start = y * high_width;
197 linearized_97_split_contiguous_into(
198 row,
199 &mut scratch.row_low[low_start..low_start + low_width],
200 &mut scratch.row_high[high_start..high_start + high_width],
201 &mut scratch.lift_workspace,
202 )?;
203 }
204
205 let mut ll = try_vec_filled(checked_allocation_len::<f64>(low_width, low_height)?, 0.0)?;
206 let mut lh = try_vec_filled(checked_allocation_len::<f64>(low_width, high_height)?, 0.0)?;
207 for x in 0..low_width {
208 linearized_97_split_strided_into(
209 Dwt97StridedSplit {
210 samples: &scratch.row_low,
211 stride: low_width,
212 height,
213 band_width: low_width,
214 },
215 x,
216 &mut ll,
217 &mut lh,
218 &mut scratch.lift_workspace,
219 )?;
220 }
221
222 let mut hl = try_vec_filled(checked_allocation_len::<f64>(high_width, low_height)?, 0.0)?;
223 let mut hh = try_vec_filled(checked_allocation_len::<f64>(high_width, high_height)?, 0.0)?;
224 for x in 0..high_width {
225 linearized_97_split_strided_into(
226 Dwt97StridedSplit {
227 samples: &scratch.row_high,
228 stride: high_width,
229 height,
230 band_width: high_width,
231 },
232 x,
233 &mut hl,
234 &mut hh,
235 &mut scratch.lift_workspace,
236 )?;
237 }
238
239 Ok(Dwt97TwoDimensional {
240 ll,
241 hl,
242 lh,
243 hh,
244 low_width,
245 low_height,
246 high_width,
247 high_height,
248 })
249}
250
251#[cfg(test)]
252fn idct8x8_sample(block: &[[f64; 8]; 8], x: usize, y: usize) -> f64 {
253 let mut sample = 0.0;
254 for (freq_y, row) in block.iter().enumerate() {
255 let y_basis = idct8_basis(y, freq_y);
256 for (freq_x, coefficient) in row.iter().copied().enumerate() {
257 sample += coefficient * y_basis * idct8_basis(x, freq_x);
258 }
259 }
260 sample
261}
262
263fn idct8x8_blocks_to_samples(
264 blocks: &[[[f64; 8]; 8]],
265 block_cols: usize,
266 width: usize,
267 height: usize,
268 block_row_sample_count: usize,
269 samples: &mut [f64],
270) {
271 let sample_count = samples.len();
272 let basis = idct8_basis_table();
273 let active_block_cols = width.div_ceil(8);
274 let active_block_rows = height.div_ceil(8);
275 let row_context = Idct8x8RowContext {
276 blocks,
277 block_cols,
278 width,
279 height,
280 basis,
281 active_block_cols,
282 };
283
284 if sample_count >= PARALLEL_IDCT_MIN_SAMPLES {
285 samples
286 .par_chunks_mut(block_row_sample_count)
287 .enumerate()
288 .take(active_block_rows)
289 .for_each(|(block_y, sample_rows)| {
290 idct8x8_block_row_to_samples(&row_context, block_y, sample_rows);
291 });
292 } else {
293 for block_y in 0..active_block_rows {
294 let block_sample_y = block_y * 8;
295 let output_rows = (height - block_sample_y).min(8);
296 let row_start = block_sample_y * width;
297 let row_end = row_start + output_rows * width;
298 idct8x8_block_row_to_samples(&row_context, block_y, &mut samples[row_start..row_end]);
299 }
300 }
301}
302
303#[derive(Clone, Copy)]
304struct Idct8x8RowContext<'a> {
305 blocks: &'a [[[f64; 8]; 8]],
306 block_cols: usize,
307 width: usize,
308 height: usize,
309 basis: &'a [[f64; 8]; 8],
310 active_block_cols: usize,
311}
312
313fn idct8x8_block_row_to_samples(
314 context: &Idct8x8RowContext<'_>,
315 block_y: usize,
316 sample_rows: &mut [f64],
317) {
318 let Idct8x8RowContext {
319 blocks,
320 block_cols,
321 width,
322 height,
323 basis,
324 active_block_cols,
325 } = *context;
326 let block_sample_y = block_y * 8;
327 let output_rows = (height - block_sample_y).min(8);
328 for block_x in 0..active_block_cols {
329 let block_sample_x = block_x * 8;
330 let output_cols = (width - block_sample_x).min(8);
331 let block = &blocks[block_y * block_cols + block_x];
332 let mut vertical = [[0.0; 8]; 8];
333
334 for (local_y, basis_row) in basis.iter().enumerate() {
335 for freq_x in 0..8 {
336 let mut sum = 0.0;
337 for (freq_y, block_row) in block.iter().enumerate() {
338 sum += basis_row[freq_y] * block_row[freq_x];
339 }
340 vertical[local_y][freq_x] = sum;
341 }
342 }
343
344 for (local_y, vertical_row) in vertical.iter().enumerate().take(output_rows) {
345 let row_offset = local_y * width + block_sample_x;
346 for local_x in 0..output_cols {
347 let mut sample = 0.0;
348 for (freq_x, vertical_value) in vertical_row.iter().enumerate() {
349 sample += *vertical_value * basis[local_x][freq_x];
350 }
351 sample_rows[row_offset + local_x] = sample;
352 }
353 }
354 }
355}
356
357#[cfg(test)]
358fn linearized_97_from_sample_slice(samples: &[f64]) -> Dwt97OneDimensional {
359 let mut lifted = samples.to_vec();
360 forward_lift_97(&mut lifted);
361
362 Dwt97OneDimensional {
363 low: lifted.iter().step_by(2).copied().collect(),
364 high: lifted.iter().skip(1).step_by(2).copied().collect(),
365 }
366}
367
368fn forward_lift_97(data: &mut [f64]) {
369 let n = data.len();
370 if n < 2 {
371 return;
372 }
373
374 let last_even = if n.is_multiple_of(2) { n - 2 } else { n - 1 };
375
376 for i in (1..n).step_by(2) {
377 let left = data[i - 1];
378 let right = if i + 1 < n {
379 data[i + 1]
380 } else {
381 data[last_even]
382 };
383 data[i] += ALPHA * (left + right);
384 }
385
386 for i in (0..n).step_by(2) {
387 let left = if i > 0 { data[i - 1] } else { data[1] };
388 let right = if i + 1 < n { data[i + 1] } else { left };
389 data[i] += BETA * (left + right);
390 }
391
392 for i in (1..n).step_by(2) {
393 let left = data[i - 1];
394 let right = if i + 1 < n {
395 data[i + 1]
396 } else {
397 data[last_even]
398 };
399 data[i] += GAMMA * (left + right);
400 }
401
402 for i in (0..n).step_by(2) {
403 let left = if i > 0 { data[i - 1] } else { data[1] };
404 let right = if i + 1 < n { data[i + 1] } else { left };
405 data[i] += DELTA * (left + right);
406 }
407
408 for i in (0..n).step_by(2) {
409 data[i] *= INV_KAPPA;
410 }
411 for i in (1..n).step_by(2) {
412 data[i] *= KAPPA;
413 }
414}
415
416fn linearized_97_split_contiguous_into(
417 samples: &[f64],
418 low: &mut [f64],
419 high: &mut [f64],
420 workspace: &mut Vec<f64>,
421) -> Result<(), DctTransformError> {
422 debug_assert_eq!(low.len(), low_len(samples.len()));
423 debug_assert_eq!(high.len(), high_len(samples.len()));
424
425 workspace.clear();
426 try_vec_reserve_len(workspace, samples.len())?;
427 workspace.extend_from_slice(samples);
428 forward_lift_97(workspace);
429
430 for (target, value) in low.iter_mut().zip(workspace.iter().step_by(2)) {
431 *target = *value;
432 }
433 for (target, value) in high.iter_mut().zip(workspace.iter().skip(1).step_by(2)) {
434 *target = *value;
435 }
436 Ok(())
437}
438
439#[derive(Clone, Copy)]
440struct Dwt97StridedSplit<'a> {
441 samples: &'a [f64],
442 stride: usize,
443 height: usize,
444 band_width: usize,
445}
446
447fn linearized_97_split_strided_into(
448 input: Dwt97StridedSplit<'_>,
449 x: usize,
450 low: &mut [f64],
451 high: &mut [f64],
452 workspace: &mut Vec<f64>,
453) -> Result<(), DctTransformError> {
454 let Dwt97StridedSplit {
455 samples,
456 stride,
457 height,
458 band_width,
459 } = input;
460 debug_assert_eq!(low.len(), band_width * low_len(height));
461 debug_assert_eq!(high.len(), band_width * high_len(height));
462
463 workspace.clear();
464 try_vec_reserve_len(workspace, height)?;
465 for y in 0..height {
466 workspace.push(samples[y * stride + x]);
467 }
468 forward_lift_97(workspace);
469
470 for (low_y, value) in workspace.iter().step_by(2).enumerate() {
471 low[low_y * band_width + x] = *value;
472 }
473 for (high_y, value) in workspace.iter().skip(1).step_by(2).enumerate() {
474 high[high_y * band_width + x] = *value;
475 }
476 Ok(())
477}
478
479fn validate_sample_plane(
480 samples: &[f64],
481 width: usize,
482 height: usize,
483) -> Result<usize, DctTransformError> {
484 if width == 0 || height == 0 {
485 return Err(DctTransformError::InvalidSamplePlaneDimensions { width, height });
486 }
487 let sample_count = checked_allocation_len::<f64>(width, height)?;
488 if samples.len() != sample_count {
489 return Err(DctTransformError::SamplePlaneLengthMismatch {
490 sample_count: samples.len(),
491 width,
492 height,
493 });
494 }
495 Ok(sample_count)
496}
497
498fn validate_grid_workspace(
499 sample_count: usize,
500 width: usize,
501 height: usize,
502) -> Result<(), DctTransformError> {
503 let spatial_bytes = checked_allocation_bytes::<f64>(sample_count)?;
504 checked_add_allocation_bytes(
505 spatial_bytes,
506 plane_workspace_bytes(sample_count, width, height)?,
507 )?;
508 Ok(())
509}
510
511fn validate_plane_workspace(
512 sample_count: usize,
513 width: usize,
514 height: usize,
515) -> Result<(), DctTransformError> {
516 plane_workspace_bytes(sample_count, width, height)?;
517 Ok(())
518}
519
520fn plane_workspace_bytes(
521 sample_count: usize,
522 width: usize,
523 height: usize,
524) -> Result<usize, DctTransformError> {
525 let row_and_output_bytes = allocation_product_bytes::<f64>(sample_count, 2)?;
526 let lift_bytes = checked_allocation_bytes::<f64>(width.max(height))?;
527 Ok(checked_add_allocation_bytes(
528 row_and_output_bytes,
529 lift_bytes,
530 )?)
531}
532
533fn allocation_product_bytes<T>(left: usize, right: usize) -> Result<usize, DctTransformError> {
534 let element_count = checked_allocation_len::<T>(left, right)?;
535 Ok(checked_allocation_bytes::<T>(element_count)?)
536}
537
538fn validate_grid(
539 block_count: usize,
540 block_cols: usize,
541 block_rows: usize,
542 width: usize,
543 height: usize,
544) -> Result<(), DctTransformError> {
545 validate_dct_block_grid(block_count, block_cols, block_rows, width, height)?;
546 Ok(())
547}
548
549#[cfg(test)]
550struct Dwt97OneDimensional {
551 low: Vec<f64>,
552 high: Vec<f64>,
553}
554
555#[cfg(test)]
556mod tests {
557 use core::f64::consts::PI;
558
559 use super::*;
560
561 #[test]
562 fn grid_workspace_rejects_aggregate_before_any_single_vector_hits_cap() {
563 let cap = j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES;
564 let sample_count = cap / core::mem::size_of::<f64>() / 4 + 1;
565 assert!(checked_allocation_bytes::<f64>(sample_count).is_ok());
566 assert!(matches!(
567 validate_grid_workspace(sample_count, sample_count, 1),
568 Err(DctTransformError::MemoryCapExceeded { requested, cap: limit })
569 if requested > limit && limit == cap
570 ));
571 }
572
573 fn assert_all_close(values: &[f64], expected: f64, epsilon: f64) {
574 for &value in values {
575 assert!(
576 (value - expected).abs() < epsilon,
577 "value={value} expected={expected} values={values:?}"
578 );
579 }
580 }
581
582 #[test]
583 fn linearized_97_from_constant_signal_places_dc_in_low_pass() {
584 for len in [2usize, 3, 8, 9, 64, 65] {
585 let samples = vec![50.0; len];
586
587 let transformed = linearized_97_from_sample_slice(&samples);
588
589 assert_all_close(&transformed.low, 50.0, 0.001);
590 assert_all_close(&transformed.high, 0.0, 0.001);
591 }
592 }
593
594 #[test]
595 fn linearized_97_2d_from_constant_plane_places_dc_in_ll() {
596 for (width, height) in [(8usize, 8usize), (9, 7)] {
597 let samples = vec![50.0; width * height];
598
599 let transformed = linearized_97_2d_from_plane(&samples, width, height)
600 .expect("small test plane fits the transform workspace");
601
602 assert_all_close(&transformed.ll, 50.0, 0.001);
603 assert_all_close(&transformed.hl, 0.0, 0.001);
604 assert_all_close(&transformed.lh, 0.0, 0.001);
605 assert_all_close(&transformed.hh, 0.0, 0.001);
606 }
607 }
608
609 const REF_LP: [f64; 9] = [
628 0.026_748_757_410_810,
629 -0.016_864_118_442_875,
630 -0.078_223_266_528_990,
631 0.266_864_118_442_875,
632 0.602_949_018_236_360,
633 0.266_864_118_442_875,
634 -0.078_223_266_528_990,
635 -0.016_864_118_442_875,
636 0.026_748_757_410_810,
637 ];
638
639 const REF_HP: [f64; 7] = [
643 0.091_271_763_114_250,
644 -0.057_543_526_228_500,
645 -0.591_271_763_114_247,
646 1.115_087_052_456_994,
647 -0.591_271_763_114_247,
648 -0.057_543_526_228_500,
649 0.091_271_763_114_250,
650 ];
651
652 fn ws_reflect(i: isize, n: usize) -> usize {
656 debug_assert!(n >= 1);
657 if n == 1 {
658 return 0;
659 }
660 let n = isize::try_from(n).expect("signal length fits in isize");
661 let period = 2 * (n - 1);
662 let mut k = i.rem_euclid(period);
663 if k >= n {
664 k = period - k;
665 }
666 usize::try_from(k).expect("reflected index is non-negative")
667 }
668
669 fn ref_analysis_1d(signal: &[f64]) -> (Vec<f64>, Vec<f64>) {
673 let n = signal.len();
674 if n < 2 {
675 return (signal.to_vec(), Vec::new());
677 }
678 let mut low = vec![0.0; low_len(n)];
679 let mut high = vec![0.0; high_len(n)];
680 for (m, out) in low.iter_mut().enumerate() {
681 let center = 2 * isize::try_from(m).unwrap();
682 *out = REF_LP
683 .iter()
684 .enumerate()
685 .map(|(t, &tap)| {
686 tap * signal[ws_reflect(center + isize::try_from(t).unwrap() - 4, n)]
687 })
688 .sum();
689 }
690 for (m, out) in high.iter_mut().enumerate() {
691 let center = 2 * isize::try_from(m).unwrap() + 1;
692 *out = REF_HP
693 .iter()
694 .enumerate()
695 .map(|(t, &tap)| {
696 tap * signal[ws_reflect(center + isize::try_from(t).unwrap() - 3, n)]
697 })
698 .sum();
699 }
700 (low, high)
701 }
702
703 fn ref_analysis_2d(samples: &[f64], width: usize, height: usize) -> Dwt97TwoDimensional<f64> {
706 let low_width = low_len(width);
707 let high_width = high_len(width);
708 let low_height = low_len(height);
709 let high_height = high_len(height);
710
711 let mut row_low = vec![0.0; height * low_width];
712 let mut row_high = vec![0.0; height * high_width];
713 for y in 0..height {
714 let (lo, hi) = ref_analysis_1d(&samples[y * width..y * width + width]);
715 row_low[y * low_width..y * low_width + low_width].copy_from_slice(&lo);
716 row_high[y * high_width..y * high_width + high_width].copy_from_slice(&hi);
717 }
718
719 let vertical_split = |source: &[f64], band_width: usize| -> (Vec<f64>, Vec<f64>) {
720 let mut low = vec![0.0; band_width * low_height];
721 let mut high = vec![0.0; band_width * high_height];
722 for x in 0..band_width {
723 let column: Vec<f64> = (0..height).map(|y| source[y * band_width + x]).collect();
724 let (lo, hi) = ref_analysis_1d(&column);
725 for (vy, &value) in lo.iter().enumerate() {
726 low[vy * band_width + x] = value;
727 }
728 for (vy, &value) in hi.iter().enumerate() {
729 high[vy * band_width + x] = value;
730 }
731 }
732 (low, high)
733 };
734
735 let (ll, lh) = vertical_split(&row_low, low_width);
736 let (hl, hh) = vertical_split(&row_high, high_width);
737
738 Dwt97TwoDimensional {
739 ll,
740 hl,
741 lh,
742 hh,
743 low_width,
744 low_height,
745 high_width,
746 high_height,
747 }
748 }
749
750 #[expect(
752 clippy::cast_precision_loss,
753 reason = "the deterministic PRNG intentionally maps its upper 53 bits into an f64 fraction"
754 )]
755 fn next_unit(state: &mut u64) -> f64 {
756 *state = state
757 .wrapping_mul(6_364_136_223_846_793_005)
758 .wrapping_add(1_442_695_040_888_963_407);
759 ((*state >> 11) as f64 / (1u64 << 53) as f64).mul_add(2.0, -1.0)
760 }
761
762 fn assert_bands_close(actual: &[f64], expected: &[f64], label: &str, epsilon: f64) {
763 assert_eq!(actual.len(), expected.len(), "{label} band length");
764 for (i, (a, b)) in actual.iter().zip(expected.iter()).enumerate() {
765 assert!(
766 (a - b).abs() <= epsilon,
767 "{label}[{i}] diverged: lifting={a} reference={b} (diff {})",
768 (a - b).abs()
769 );
770 }
771 }
772
773 #[test]
774 #[expect(
775 clippy::cast_precision_loss,
776 reason = "tiny filter-tap indices are exactly representable in f64"
777 )]
778 fn reference_cdf97_taps_satisfy_their_defining_properties() {
779 let lp_dc: f64 = REF_LP.iter().sum();
782 assert!((lp_dc - 1.0).abs() < 1e-9, "low-pass DC gain = {lp_dc}");
783 let hp_dc: f64 = REF_HP.iter().sum();
784 assert!(hp_dc.abs() < 1e-9, "high-pass DC gain = {hp_dc}");
785
786 for k in 0..4 {
788 assert!(
789 (REF_LP[k] - REF_LP[8 - k]).abs() < 1e-15,
790 "low-pass asymmetric at {k}"
791 );
792 }
793 for k in 0..3 {
794 assert!(
795 (REF_HP[k] - REF_HP[6 - k]).abs() < 1e-15,
796 "high-pass asymmetric at {k}"
797 );
798 }
799
800 for m in 1..=3 {
803 let moment: f64 = REF_HP
804 .iter()
805 .enumerate()
806 .map(|(k, &tap)| (k as f64 - 3.0).powi(m) * tap)
807 .sum();
808 assert!(moment.abs() < 1e-9, "high-pass moment {m} = {moment}");
809 }
810 }
811
812 #[test]
813 fn forward_lift_97_matches_independent_filter_bank_1d() {
814 let mut state = 0x1234_5678_9abc_def0u64;
815 for n in [2usize, 3, 4, 5, 8, 9, 12, 15, 16, 23, 32, 33, 64, 65] {
816 let signal: Vec<f64> = (0..n).map(|_| next_unit(&mut state) * 100.0).collect();
817 let lifted = linearized_97_from_sample_slice(&signal);
818 let (low, high) = ref_analysis_1d(&signal);
819 assert_bands_close(&lifted.low, &low, &format!("n={n} low"), 1e-9);
820 assert_bands_close(&lifted.high, &high, &format!("n={n} high"), 1e-9);
821 }
822 }
823
824 #[test]
825 #[expect(
826 clippy::cast_precision_loss,
827 reason = "the fixed forty-sample polynomial domain is exactly representable in f64"
828 )]
829 fn forward_lift_97_annihilates_low_degree_polynomials() {
830 let n = 40usize;
834 let polynomials: [[f64; 4]; 4] = [
835 [5.0, 0.0, 0.0, 0.0],
836 [0.0, 2.5, 0.0, 0.0],
837 [1.0, -0.7, 0.3, 0.0],
838 [0.0, 0.0, 0.0, 0.05],
839 ];
840 for coeffs in polynomials {
841 let signal: Vec<f64> = (0..n)
842 .map(|i| {
843 let x = i as f64;
844 coeffs[3].mul_add(
845 x * x * x,
846 coeffs[2].mul_add(x * x, coeffs[1].mul_add(x, coeffs[0])),
847 )
848 })
849 .collect();
850 let lifted = linearized_97_from_sample_slice(&signal);
851 let interior = &lifted.high[3..lifted.high.len() - 3];
853 assert_all_close(interior, 0.0, 1e-6);
854 }
855 }
856
857 #[test]
858 fn linearized_97_2d_matches_independent_separable_filter_bank() {
859 let mut state = 0xfeed_face_dead_beefu64;
860 for (width, height) in [
861 (8usize, 8usize),
862 (16, 16),
863 (24, 16),
864 (15, 13),
865 (16, 23),
866 (9, 7),
867 (32, 32),
868 ] {
869 let samples: Vec<f64> = (0..width * height)
870 .map(|_| next_unit(&mut state) * 100.0)
871 .collect();
872 let got = linearized_97_2d_from_plane(&samples, width, height)
873 .expect("small test plane fits the transform workspace");
874 let want = ref_analysis_2d(&samples, width, height);
875 assert_eq!(
876 (
877 got.low_width,
878 got.low_height,
879 got.high_width,
880 got.high_height
881 ),
882 (
883 want.low_width,
884 want.low_height,
885 want.high_width,
886 want.high_height
887 ),
888 "band dimensions for {width}x{height}"
889 );
890 assert_bands_close(&got.ll, &want.ll, &format!("{width}x{height} ll"), 1e-9);
891 assert_bands_close(&got.hl, &want.hl, &format!("{width}x{height} hl"), 1e-9);
892 assert_bands_close(&got.lh, &want.lh, &format!("{width}x{height} lh"), 1e-9);
893 assert_bands_close(&got.hh, &want.hh, &format!("{width}x{height} hh"), 1e-9);
894 }
895 }
896
897 #[test]
898 #[expect(
899 clippy::cast_precision_loss,
900 reason = "the fixed sixteen-sample coordinate domain is exactly representable in f64"
901 )]
902 fn linearized_97_2d_separates_horizontal_and_vertical_detail() {
903 let (width, height) = (16usize, 16usize);
907
908 let varies_in_x: Vec<f64> = (0..width * height)
909 .map(|i| ((i % width) as f64).sin().mul_add(30.0, 5.0))
910 .collect();
911 let t = linearized_97_2d_from_plane(&varies_in_x, width, height)
912 .expect("small test plane fits the transform workspace");
913 assert_all_close(&t.lh, 0.0, 1e-9);
914 assert_all_close(&t.hh, 0.0, 1e-9);
915
916 let varies_in_y: Vec<f64> = (0..width * height)
917 .map(|i| ((i / width) as f64).cos().mul_add(30.0, 5.0))
918 .collect();
919 let t = linearized_97_2d_from_plane(&varies_in_y, width, height)
920 .expect("small test plane fits the transform workspace");
921 assert_all_close(&t.hl, 0.0, 1e-9);
922 assert_all_close(&t.hh, 0.0, 1e-9);
923 }
924
925 #[expect(
933 clippy::cast_precision_loss,
934 reason = "IDCT sample and frequency indices are bounded to the 8x8 block domain"
935 )]
936 fn exact_idct_sample(block: &[[f64; 8]; 8], x: usize, y: usize) -> f64 {
937 let alpha = |k: usize| {
938 if k == 0 {
939 (1.0_f64 / 8.0).sqrt()
940 } else {
941 (2.0_f64 / 8.0).sqrt()
942 }
943 };
944 let cos_term = |sample: usize, freq: usize| {
945 (((2 * sample + 1) as f64) * freq as f64 * PI / 16.0).cos()
946 };
947 let mut acc = 0.0;
948 for (v, row) in block.iter().enumerate() {
949 for (u, &coeff) in row.iter().enumerate() {
950 acc += alpha(u) * alpha(v) * coeff * cos_term(x, u) * cos_term(y, v);
951 }
952 }
953 acc
954 }
955
956 #[test]
957 fn idct8x8_sample_matches_exact_cosine_sum() {
958 let mut state = 0x5151_aaaa_bbbb_ccccu64;
959 for _ in 0..64 {
960 let mut block = [[0.0f64; 8]; 8];
961 for row in &mut block {
962 for coeff in row {
963 *coeff = next_unit(&mut state) * 64.0;
964 }
965 }
966 for y in 0..8 {
967 for x in 0..8 {
968 let got = idct8x8_sample(&block, x, y);
969 let want = exact_idct_sample(&block, x, y);
970 assert!(
971 (got - want).abs() < 1e-9,
972 "idct8x8_sample({x},{y})={got} exact={want}"
973 );
974 }
975 }
976 }
977 }
978
979 #[test]
980 fn idct8x8_sample_dc_only_is_uniform() {
981 let mut block = [[0.0f64; 8]; 8];
983 block[0][0] = 320.0;
984 for y in 0..8 {
985 for x in 0..8 {
986 assert!((idct8x8_sample(&block, x, y) - 40.0).abs() < 1e-9);
987 }
988 }
989 }
990
991 #[test]
992 fn dct8x8_grid_to_2d_97_idct_scratch_path_reuses_spatial_storage() {
993 let large_blocks = structured_blocks(32, 32);
994 let small_blocks = structured_blocks(2, 2);
995 let mut scratch = Dct97GridScratch::default();
996
997 let large = dct8x8_blocks_then_dwt97_float_with_scratch(
998 &large_blocks,
999 32,
1000 32,
1001 255,
1002 241,
1003 &mut scratch,
1004 )
1005 .expect("scratch 9/7 IDCT path accepts covered large grid");
1006 let expected_large = dct8x8_blocks_then_dwt97_float(&large_blocks, 32, 32, 255, 241)
1007 .expect("reference 9/7 IDCT path accepts covered large grid");
1008 let capacity_after_large = scratch.spatial_sample_capacity();
1009
1010 let small =
1011 dct8x8_blocks_then_dwt97_float_with_scratch(&small_blocks, 2, 2, 13, 11, &mut scratch)
1012 .expect("scratch 9/7 IDCT path accepts covered small grid");
1013 let expected_small = dct8x8_blocks_then_dwt97_float(&small_blocks, 2, 2, 13, 11)
1014 .expect("reference 9/7 IDCT path accepts covered small grid");
1015
1016 assert!(capacity_after_large > 0);
1017 assert_eq!(scratch.spatial_sample_capacity(), capacity_after_large);
1018 assert!(large.max_abs_diff(&expected_large) < 1.0e-9);
1019 assert!(small.max_abs_diff(&expected_small) < 1.0e-9);
1020 }
1021
1022 #[expect(
1023 clippy::cast_precision_loss,
1024 reason = "small deterministic test-grid indices are exactly representable in f64"
1025 )]
1026 fn structured_blocks(block_cols: usize, block_rows: usize) -> Vec<[[f64; 8]; 8]> {
1027 let mut blocks = Vec::with_capacity(block_cols * block_rows);
1028 for block_y in 0..block_rows {
1029 for block_x in 0..block_cols {
1030 let mut block = [[0.0; 8]; 8];
1031 block[0][0] = 384.0 + (block_x * 19 + block_y * 23) as f64;
1032 block[0][1] = -17.0 + block_x as f64;
1033 block[1][0] = 11.0 - block_y as f64;
1034 block[2][3] = 7.0;
1035 block[4][4] = -3.0;
1036 block[7][7] = 2.0;
1037 blocks.push(block);
1038 }
1039 }
1040 blocks
1041 }
1042}