Skip to main content

jugar_probar/pixel_coverage/
parallel.rs

1//! Parallel Processing Abstraction (PIXEL-001 v2.1 Phase 9)
2//!
3//! Provides parallel iteration abstractions that work sequentially by default
4//! and can use Rayon when the `parallel` feature is enabled.
5//!
6//! This module allows the metrics code to be written once and automatically
7//! benefit from parallelism when Rayon is available.
8
9use super::config::PerformanceConfig;
10use super::heatmap::Rgb;
11use super::metrics::{CieDe2000Metric, Lab, SsimMetric};
12
13/// Parallel processing context
14#[derive(Debug, Clone, Default)]
15pub struct ParallelContext {
16    /// Configuration
17    config: PerformanceConfig,
18}
19
20impl ParallelContext {
21    /// Create a new parallel context with default config
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Create with custom config
28    #[must_use]
29    pub fn with_config(config: PerformanceConfig) -> Self {
30        Self { config }
31    }
32
33    /// Check if parallel processing is enabled
34    #[must_use]
35    pub fn is_parallel(&self) -> bool {
36        self.config.parallel
37    }
38
39    /// Get thread count (0 = auto)
40    #[must_use]
41    pub fn thread_count(&self) -> usize {
42        if self.config.threads == 0 {
43            num_cpus()
44        } else {
45            self.config.threads
46        }
47    }
48}
49
50/// Get number of CPUs (fallback implementation)
51#[must_use]
52pub fn num_cpus() -> usize {
53    // Simple fallback - in production this would use num_cpus crate or rayon
54    std::thread::available_parallelism()
55        .map(|n| n.get())
56        .unwrap_or(1)
57}
58
59/// Parallel map operation over slices
60///
61/// When Rayon is available, this uses par_iter().map().
62/// Currently falls back to sequential iteration.
63pub fn parallel_map<T, U, F>(items: &[T], f: F) -> Vec<U>
64where
65    T: Sync,
66    U: Send,
67    F: Fn(&T) -> U + Sync,
68{
69    // Sequential fallback - replace with rayon::par_iter when available
70    items.iter().map(f).collect()
71}
72
73/// Parallel reduce operation
74///
75/// When Rayon is available, this uses par_iter().reduce().
76/// Currently falls back to sequential fold.
77#[allow(dead_code)]
78pub fn parallel_reduce<T, U, M, R>(items: &[T], identity: U, map_fn: M, reduce_fn: R) -> U
79where
80    T: Sync,
81    U: Send + Clone,
82    M: Fn(&T) -> U + Sync,
83    R: Fn(U, U) -> U + Sync,
84{
85    // Sequential fallback
86    items.iter().map(map_fn).fold(identity, reduce_fn)
87}
88
89/// Parallel sum for f32 values
90#[allow(dead_code)]
91pub fn parallel_sum<T, F>(items: &[T], f: F) -> f32
92where
93    T: Sync,
94    F: Fn(&T) -> f32 + Sync,
95{
96    parallel_reduce(items, 0.0, f, |a, b| a + b)
97}
98
99/// Batch processor for pixel operations
100#[derive(Debug)]
101pub struct BatchProcessor {
102    /// Batch size for processing (used when Rayon is enabled)
103    #[allow(dead_code)]
104    batch_size: usize,
105    /// Parallel context
106    ctx: ParallelContext,
107}
108
109impl Default for BatchProcessor {
110    fn default() -> Self {
111        Self {
112            batch_size: 1024,
113            ctx: ParallelContext::default(),
114        }
115    }
116}
117
118impl BatchProcessor {
119    /// Create new batch processor
120    #[must_use]
121    pub fn new(batch_size: usize) -> Self {
122        Self {
123            batch_size,
124            ..Default::default()
125        }
126    }
127
128    /// Set parallel context
129    #[must_use]
130    pub fn with_context(mut self, ctx: ParallelContext) -> Self {
131        self.ctx = ctx;
132        self
133    }
134
135    /// Process pixel pairs computing Delta E values
136    #[must_use]
137    pub fn compute_delta_e_batch(
138        &self,
139        reference: &[Rgb],
140        generated: &[Rgb],
141        metric: &CieDe2000Metric,
142    ) -> DeltaEBatchResult {
143        if reference.len() != generated.len() {
144            return DeltaEBatchResult::default();
145        }
146
147        let pairs: Vec<_> = reference.iter().zip(generated.iter()).collect();
148
149        // Compute all delta E values
150        let delta_es: Vec<f32> = parallel_map(&pairs, |(r, g)| {
151            let lab1 = Lab::from_rgb(r);
152            let lab2 = Lab::from_rgb(g);
153            metric.delta_e(&lab1, &lab2)
154        });
155
156        // Compute statistics
157        let sum: f32 = delta_es.iter().sum();
158        let max = delta_es.iter().cloned().fold(0.0f32, f32::max);
159        let count = delta_es.len();
160
161        let imperceptible = delta_es
162            .iter()
163            .filter(|&&de| de < metric.jnd_threshold)
164            .count();
165        let acceptable = delta_es
166            .iter()
167            .filter(|&&de| de < metric.accept_threshold)
168            .count();
169
170        DeltaEBatchResult {
171            mean: if count > 0 { sum / count as f32 } else { 0.0 },
172            max,
173            count,
174            imperceptible_count: imperceptible,
175            acceptable_count: acceptable,
176        }
177    }
178
179    /// Process image comparison in batches for memory efficiency
180    #[must_use]
181    pub fn compute_ssim_batched(
182        &self,
183        reference: &[Rgb],
184        generated: &[Rgb],
185        width: u32,
186        height: u32,
187        metric: &SsimMetric,
188    ) -> SsimBatchResult {
189        if reference.len() != generated.len() {
190            return SsimBatchResult::default();
191        }
192
193        // For now, use the standard SSIM calculation
194        // Batched processing would split the image into tiles for very large images
195        let result = metric.compare(reference, generated, width, height);
196
197        SsimBatchResult {
198            score: result.score,
199            channel_scores: result.channel_scores,
200            batches_processed: 1,
201        }
202    }
203}
204
205/// Result of batch Delta E computation
206#[derive(Debug, Clone, Default)]
207pub struct DeltaEBatchResult {
208    /// Mean delta E
209    pub mean: f32,
210    /// Maximum delta E
211    pub max: f32,
212    /// Total pixel count
213    pub count: usize,
214    /// Count below JND threshold
215    pub imperceptible_count: usize,
216    /// Count below acceptability threshold
217    pub acceptable_count: usize,
218}
219
220/// Result of batch SSIM computation
221#[derive(Debug, Clone, Default)]
222pub struct SsimBatchResult {
223    /// Overall SSIM score
224    pub score: f32,
225    /// Per-channel scores
226    pub channel_scores: [f32; 3],
227    /// Number of batches processed
228    pub batches_processed: usize,
229}
230
231/// Downscaler for rapid L1 checks
232#[derive(Debug, Clone)]
233pub struct Downscaler {
234    /// Downscale factor (2 = 50% resolution)
235    factor: u32,
236}
237
238impl Default for Downscaler {
239    fn default() -> Self {
240        Self { factor: 2 }
241    }
242}
243
244impl Downscaler {
245    /// Create new downscaler
246    #[must_use]
247    pub fn new(factor: u32) -> Self {
248        Self {
249            factor: factor.max(1),
250        }
251    }
252
253    /// Downscale an image
254    #[must_use]
255    pub fn downscale(&self, image: &[Rgb], width: u32, height: u32) -> (Vec<Rgb>, u32, u32) {
256        let new_width = width / self.factor;
257        let new_height = height / self.factor;
258
259        if new_width == 0 || new_height == 0 {
260            return (image.to_vec(), width, height);
261        }
262
263        let mut result = Vec::with_capacity((new_width * new_height) as usize);
264
265        for y in 0..new_height {
266            for x in 0..new_width {
267                // Simple box filter (average of factor x factor pixels)
268                let mut r_sum = 0u32;
269                let mut g_sum = 0u32;
270                let mut b_sum = 0u32;
271                let mut count = 0u32;
272
273                for dy in 0..self.factor {
274                    for dx in 0..self.factor {
275                        let src_x = x * self.factor + dx;
276                        let src_y = y * self.factor + dy;
277                        if src_x < width && src_y < height {
278                            let idx = (src_y * width + src_x) as usize;
279                            if idx < image.len() {
280                                r_sum += image[idx].r as u32;
281                                g_sum += image[idx].g as u32;
282                                b_sum += image[idx].b as u32;
283                                count += 1;
284                            }
285                        }
286                    }
287                }
288
289                if count > 0 {
290                    result.push(Rgb::new(
291                        (r_sum / count) as u8,
292                        (g_sum / count) as u8,
293                        (b_sum / count) as u8,
294                    ));
295                }
296            }
297        }
298
299        (result, new_width, new_height)
300    }
301}
302
303/// Hash cache for perceptual hashes
304#[derive(Debug, Default)]
305pub struct HashCache {
306    /// Cached hashes (image hash -> perceptual hash)
307    cache: std::collections::HashMap<u64, u64>,
308}
309
310impl HashCache {
311    /// Create new cache
312    #[must_use]
313    pub fn new() -> Self {
314        Self::default()
315    }
316
317    /// Get cached hash
318    #[must_use]
319    pub fn get(&self, image_hash: u64) -> Option<u64> {
320        self.cache.get(&image_hash).copied()
321    }
322
323    /// Store hash in cache
324    pub fn insert(&mut self, image_hash: u64, phash: u64) {
325        self.cache.insert(image_hash, phash);
326    }
327
328    /// Clear cache
329    pub fn clear(&mut self) {
330        self.cache.clear();
331    }
332
333    /// Get cache size
334    #[must_use]
335    pub fn len(&self) -> usize {
336        self.cache.len()
337    }
338
339    /// Check if cache is empty
340    #[must_use]
341    pub fn is_empty(&self) -> bool {
342        self.cache.is_empty()
343    }
344
345    /// Compute simple hash of image data for cache key
346    #[must_use]
347    pub fn compute_image_hash(image: &[Rgb]) -> u64 {
348        // Simple FNV-1a hash for cache lookup
349        let mut hash: u64 = 0xcbf29ce484222325;
350        for pixel in image {
351            hash ^= pixel.r as u64;
352            hash = hash.wrapping_mul(0x100000001b3);
353            hash ^= pixel.g as u64;
354            hash = hash.wrapping_mul(0x100000001b3);
355            hash ^= pixel.b as u64;
356            hash = hash.wrapping_mul(0x100000001b3);
357        }
358        hash
359    }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used)]
364mod tests {
365    use super::*;
366
367    fn test_image(size: usize, value: u8) -> Vec<Rgb> {
368        vec![Rgb::new(value, value, value); size]
369    }
370
371    // =========================================================================
372    // Parallel Context Tests (H0-PAR-XX)
373    // =========================================================================
374
375    #[test]
376    fn h0_par_01_default_context() {
377        let ctx = ParallelContext::new();
378        assert!(ctx.is_parallel());
379        assert!(ctx.thread_count() >= 1);
380    }
381
382    #[test]
383    fn h0_par_02_custom_threads() {
384        let config = PerformanceConfig {
385            threads: 4,
386            ..Default::default()
387        };
388        let ctx = ParallelContext::with_config(config);
389        assert_eq!(ctx.thread_count(), 4);
390    }
391
392    #[test]
393    fn h0_par_03_parallel_map() {
394        let items = vec![1, 2, 3, 4, 5];
395        let result: Vec<i32> = parallel_map(&items, |x| x * 2);
396        assert_eq!(result, vec![2, 4, 6, 8, 10]);
397    }
398
399    #[test]
400    fn h0_par_04_parallel_sum() {
401        let items = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
402        let result = parallel_sum(&items, |&x| x);
403        assert!((result - 15.0).abs() < f32::EPSILON);
404    }
405
406    #[test]
407    fn h0_par_05_parallel_reduce() {
408        let items = vec![1, 2, 3, 4];
409        let result = parallel_reduce(&items, 0, |&x| x, |a, b| a + b);
410        assert_eq!(result, 10);
411    }
412
413    // =========================================================================
414    // Batch Processor Tests (H0-BATCH-XX)
415    // =========================================================================
416
417    #[test]
418    fn h0_batch_01_delta_e_same() {
419        let img = test_image(100, 128);
420        let processor = BatchProcessor::default();
421        let metric = CieDe2000Metric::default();
422        let result = processor.compute_delta_e_batch(&img, &img, &metric);
423        assert!(result.mean < f32::EPSILON);
424        assert_eq!(result.count, 100);
425    }
426
427    #[test]
428    fn h0_batch_02_delta_e_different() {
429        let img1 = test_image(100, 100);
430        let img2 = test_image(100, 150);
431        let processor = BatchProcessor::default();
432        let metric = CieDe2000Metric::default();
433        let result = processor.compute_delta_e_batch(&img1, &img2, &metric);
434        assert!(result.mean > 0.0);
435        assert_eq!(result.count, 100);
436    }
437
438    #[test]
439    fn h0_batch_03_ssim_same() {
440        let img = test_image(100, 128);
441        let processor = BatchProcessor::default();
442        let metric = SsimMetric::default();
443        let result = processor.compute_ssim_batched(&img, &img, 10, 10, &metric);
444        assert!(result.score >= 0.99);
445    }
446
447    // =========================================================================
448    // Downscaler Tests (H0-DOWN-XX)
449    // =========================================================================
450
451    #[test]
452    fn h0_down_01_downscale_2x() {
453        let img = test_image(100, 128); // 10x10
454        let downscaler = Downscaler::new(2);
455        let (result, w, h) = downscaler.downscale(&img, 10, 10);
456        assert_eq!(w, 5);
457        assert_eq!(h, 5);
458        assert_eq!(result.len(), 25);
459    }
460
461    #[test]
462    fn h0_down_02_downscale_preserves_color() {
463        let img = test_image(16, 200); // 4x4
464        let downscaler = Downscaler::new(2);
465        let (result, _, _) = downscaler.downscale(&img, 4, 4);
466        // Average of 200 should still be 200
467        assert_eq!(result[0].r, 200);
468    }
469
470    #[test]
471    fn h0_down_03_downscale_factor_1() {
472        let img = test_image(25, 100);
473        let downscaler = Downscaler::new(1);
474        let (result, w, h) = downscaler.downscale(&img, 5, 5);
475        assert_eq!(w, 5);
476        assert_eq!(h, 5);
477        assert_eq!(result.len(), 25);
478    }
479
480    // =========================================================================
481    // Hash Cache Tests (H0-CACHE-XX)
482    // =========================================================================
483
484    #[test]
485    fn h0_cache_01_insert_get() {
486        let mut cache = HashCache::new();
487        cache.insert(12345, 67890);
488        assert_eq!(cache.get(12345), Some(67890));
489        assert_eq!(cache.get(99999), None);
490    }
491
492    #[test]
493    fn h0_cache_02_clear() {
494        let mut cache = HashCache::new();
495        cache.insert(1, 1);
496        cache.insert(2, 2);
497        assert_eq!(cache.len(), 2);
498        cache.clear();
499        assert!(cache.is_empty());
500    }
501
502    #[test]
503    fn h0_cache_03_image_hash() {
504        let img1 = test_image(100, 128);
505        let img2 = test_image(100, 128);
506        let img3 = test_image(100, 129);
507
508        let hash1 = HashCache::compute_image_hash(&img1);
509        let hash2 = HashCache::compute_image_hash(&img2);
510        let hash3 = HashCache::compute_image_hash(&img3);
511
512        assert_eq!(hash1, hash2); // Same images
513        assert_ne!(hash1, hash3); // Different images
514    }
515
516    #[test]
517    fn h0_cache_04_empty() {
518        let cache = HashCache::new();
519        assert!(cache.is_empty());
520        assert_eq!(cache.len(), 0);
521    }
522
523    // =========================================================================
524    // Num CPUs Test
525    // =========================================================================
526
527    #[test]
528    fn h0_par_06_num_cpus() {
529        let cpus = num_cpus();
530        assert!(cpus >= 1);
531    }
532}