Skip to main content

threecrate_reconstruction/
parallel.rs

1//! Parallel processing utilities for surface reconstruction algorithms
2//!
3//! This module provides configurable thread pool management and parallel processing
4//! optimizations for all reconstruction algorithms in the crate.
5
6use rayon::prelude::*;
7use rayon::{ThreadPool, ThreadPoolBuilder};
8use std::sync::{Arc, Mutex, OnceLock};
9use threecrate_core::Result;
10
11/// Global thread pool configuration for reconstruction algorithms
12static GLOBAL_THREAD_POOL: OnceLock<Arc<ThreadPool>> = OnceLock::new();
13static THREAD_POOL_CONFIG: Mutex<ThreadPoolConfig> = Mutex::new(ThreadPoolConfig::new());
14
15/// Thread pool configuration for parallel processing
16#[derive(Debug, Clone)]
17pub struct ThreadPoolConfig {
18    /// Number of threads to use (None = automatic)
19    pub num_threads: Option<usize>,
20    /// Thread stack size in bytes
21    pub stack_size: Option<usize>,
22    /// Thread name prefix
23    pub thread_name_prefix: String,
24    /// Enable parallel processing (can be disabled for debugging)
25    pub enabled: bool,
26    /// Minimum chunk size for parallel iteration
27    pub min_chunk_size: usize,
28    /// Maximum chunk size for parallel iteration
29    pub max_chunk_size: usize,
30    /// Adaptive chunk sizing based on workload
31    pub adaptive_chunks: bool,
32}
33
34impl ThreadPoolConfig {
35    /// Create a new thread pool configuration with defaults
36    const fn new() -> Self {
37        Self {
38            num_threads: None,
39            stack_size: None,
40            thread_name_prefix: String::new(),
41            enabled: true,
42            min_chunk_size: 100,
43            max_chunk_size: 10000,
44            adaptive_chunks: true,
45        }
46    }
47
48    /// Create default configuration
49    pub fn default() -> Self {
50        Self {
51            num_threads: None,
52            stack_size: Some(8 * 1024 * 1024), // 8MB stack
53            thread_name_prefix: "threecrate-recon".to_string(),
54            enabled: true,
55            min_chunk_size: 100,
56            max_chunk_size: 10000,
57            adaptive_chunks: true,
58        }
59    }
60
61    /// Set number of threads
62    pub fn with_threads(mut self, num_threads: usize) -> Self {
63        self.num_threads = Some(num_threads);
64        self
65    }
66
67    /// Set stack size
68    pub fn with_stack_size(mut self, stack_size: usize) -> Self {
69        self.stack_size = Some(stack_size);
70        self
71    }
72
73    /// Enable or disable parallel processing
74    pub fn with_enabled(mut self, enabled: bool) -> Self {
75        self.enabled = enabled;
76        self
77    }
78
79    /// Set chunk size range
80    pub fn with_chunk_size_range(mut self, min: usize, max: usize) -> Self {
81        self.min_chunk_size = min;
82        self.max_chunk_size = max;
83        self
84    }
85
86    /// Enable adaptive chunk sizing
87    pub fn with_adaptive_chunks(mut self, adaptive: bool) -> Self {
88        self.adaptive_chunks = adaptive;
89        self
90    }
91}
92
93/// Initialize the global thread pool with custom configuration
94pub fn init_thread_pool(config: ThreadPoolConfig) -> Result<()> {
95    if GLOBAL_THREAD_POOL.get().is_some() {
96        return Ok(()); // Already initialized
97    }
98
99    let mut builder = ThreadPoolBuilder::new();
100
101    if let Some(num_threads) = config.num_threads {
102        builder = builder.num_threads(num_threads);
103    }
104
105    if let Some(stack_size) = config.stack_size {
106        builder = builder.stack_size(stack_size);
107    }
108
109    if !config.thread_name_prefix.is_empty() {
110        let prefix = config.thread_name_prefix.clone();
111        builder = builder.thread_name(move |index| format!("{}-{}", prefix, index));
112    }
113
114    let pool = builder.build().map_err(|e| {
115        threecrate_core::Error::Algorithm(format!("Failed to create thread pool: {}", e))
116    })?;
117
118    // Store configuration
119    if let Ok(mut global_config) = THREAD_POOL_CONFIG.lock() {
120        *global_config = config;
121    }
122
123    GLOBAL_THREAD_POOL.set(Arc::new(pool)).map_err(|_| {
124        threecrate_core::Error::Algorithm("Thread pool already initialized".to_string())
125    })?;
126
127    Ok(())
128}
129
130/// Get the global thread pool, initializing with defaults if needed
131pub fn get_thread_pool() -> Arc<ThreadPool> {
132    GLOBAL_THREAD_POOL
133        .get_or_init(|| {
134            let config = ThreadPoolConfig::default();
135            let pool = ThreadPoolBuilder::new()
136                .num_threads(config.num_threads.unwrap_or_else(num_cpus::get))
137                .stack_size(config.stack_size.unwrap_or(8 * 1024 * 1024))
138                .thread_name(|index| format!("threecrate-recon-{}", index))
139                .build()
140                .expect("Failed to create default thread pool");
141            Arc::new(pool)
142        })
143        .clone()
144}
145
146/// Get current thread pool configuration
147pub fn get_config() -> ThreadPoolConfig {
148    THREAD_POOL_CONFIG
149        .lock()
150        .map(|config| config.clone())
151        .unwrap_or_else(|_| ThreadPoolConfig::default())
152}
153
154/// Check if parallel processing is enabled
155pub fn is_parallel_enabled() -> bool {
156    get_config().enabled
157}
158
159/// Compute optimal chunk size based on data size and configuration
160pub fn compute_chunk_size(data_size: usize) -> usize {
161    let config = get_config();
162
163    if !config.adaptive_chunks {
164        return config
165            .min_chunk_size
166            .max(data_size / num_cpus::get())
167            .min(config.max_chunk_size);
168    }
169
170    let num_threads = get_thread_pool().current_num_threads();
171    let base_chunk_size = data_size / (num_threads * 4); // Aim for 4 chunks per thread
172
173    base_chunk_size
174        .max(config.min_chunk_size)
175        .min(config.max_chunk_size)
176}
177
178/// Execute a parallel operation with the global thread pool
179pub fn execute_parallel<F, R>(op: F) -> R
180where
181    F: FnOnce() -> R + Send,
182    R: Send,
183{
184    if is_parallel_enabled() {
185        get_thread_pool().install(op)
186    } else {
187        op()
188    }
189}
190
191/// Parallel map operation with optimal chunking
192pub fn parallel_map<T, U, F>(data: &[T], f: F) -> Vec<U>
193where
194    T: Sync,
195    U: Send,
196    F: Fn(&T) -> U + Sync + Send,
197{
198    if !is_parallel_enabled() || data.len() < get_config().min_chunk_size {
199        return data.iter().map(f).collect();
200    }
201
202    execute_parallel(|| data.par_iter().map(f).collect())
203}
204
205/// Parallel map with index
206pub fn parallel_map_indexed<T, U, F>(data: &[T], f: F) -> Vec<U>
207where
208    T: Sync,
209    U: Send,
210    F: Fn(usize, &T) -> U + Sync + Send,
211{
212    if !is_parallel_enabled() || data.len() < get_config().min_chunk_size {
213        return data.iter().enumerate().map(|(i, x)| f(i, x)).collect();
214    }
215
216    execute_parallel(|| data.par_iter().enumerate().map(|(i, x)| f(i, x)).collect())
217}
218
219/// Parallel filter operation
220pub fn parallel_filter<T, F>(data: &[T], predicate: F) -> Vec<T>
221where
222    T: Clone + Sync + Send,
223    F: Fn(&T) -> bool + Sync + Send,
224{
225    if !is_parallel_enabled() || data.len() < get_config().min_chunk_size {
226        return data.iter().filter(|x| predicate(*x)).cloned().collect();
227    }
228
229    execute_parallel(|| data.par_iter().filter(|x| predicate(*x)).cloned().collect())
230}
231
232/// Parallel reduce operation
233pub fn parallel_reduce<T, U, F, R>(data: &[T], identity: U, map_op: F, reduce_op: R) -> U
234where
235    T: Sync,
236    U: Clone + Send + Sync,
237    F: Fn(&T) -> U + Sync + Send,
238    R: Fn(U, U) -> U + Sync + Send,
239{
240    if !is_parallel_enabled() || data.len() < get_config().min_chunk_size {
241        return data.iter().map(map_op).fold(identity, reduce_op);
242    }
243
244    execute_parallel(|| {
245        data.par_iter()
246            .map(map_op)
247            .reduce(|| identity.clone(), reduce_op)
248    })
249}
250
251/// Parallel processing for point cloud operations
252pub mod point_cloud {
253    use super::*;
254    use threecrate_core::Point3f;
255
256    /// Parallel normal estimation for point clouds
257    pub fn parallel_compute_normals<F>(
258        points: &[Point3f],
259        radius: f32,
260        compute_normal: F,
261    ) -> Vec<Point3f>
262    where
263        F: Fn(&Point3f, &[Point3f], f32) -> Point3f + Sync + Send,
264    {
265        parallel_map(points, |point| {
266            // Find neighbors within radius (simplified for parallel processing)
267            let neighbors: Vec<Point3f> = points
268                .iter()
269                .filter(|p| (*p - *point).magnitude() <= radius)
270                .cloned()
271                .collect();
272
273            compute_normal(point, &neighbors, radius)
274        })
275    }
276
277    /// Parallel distance computation between point clouds
278    pub fn parallel_point_distances(points1: &[Point3f], points2: &[Point3f]) -> Vec<f32> {
279        parallel_map(points1, |p1| {
280            points2
281                .iter()
282                .map(|p2| (p1 - p2).magnitude())
283                .fold(f32::INFINITY, f32::min)
284        })
285    }
286
287    /// Parallel bounding box computation
288    pub fn parallel_bounding_box(points: &[Point3f]) -> Option<(Point3f, Point3f)> {
289        if points.is_empty() {
290            return None;
291        }
292
293        let (min_vals, max_vals) = parallel_reduce(
294            points,
295            (
296                Point3f::new(f32::INFINITY, f32::INFINITY, f32::INFINITY),
297                Point3f::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
298            ),
299            |point| (*point, *point),
300            |(min1, max1), (min2, max2)| {
301                (
302                    Point3f::new(min1.x.min(min2.x), min1.y.min(min2.y), min1.z.min(min2.z)),
303                    Point3f::new(max1.x.max(max2.x), max1.y.max(max2.y), max1.z.max(max2.z)),
304                )
305            },
306        );
307
308        Some((min_vals, max_vals))
309    }
310}
311
312/// Parallel processing for mesh operations
313pub mod mesh {
314    use super::*;
315    use threecrate_core::Point3f;
316
317    /// Parallel triangle normal computation
318    pub fn parallel_triangle_normals(
319        vertices: &[Point3f],
320        faces: &[[usize; 3]],
321    ) -> Vec<nalgebra::Vector3<f32>> {
322        parallel_map(faces, |face| {
323            let v1 = &vertices[face[0]];
324            let v2 = &vertices[face[1]];
325            let v3 = &vertices[face[2]];
326
327            let edge1 = v2 - v1;
328            let edge2 = v3 - v1;
329            let normal = edge1.cross(&edge2).normalize();
330
331            nalgebra::Vector3::new(normal.x, normal.y, normal.z)
332        })
333    }
334
335    /// Parallel vertex normal computation (angle-weighted)
336    pub fn parallel_vertex_normals(
337        vertices: &[Point3f],
338        faces: &[[usize; 3]],
339    ) -> Vec<nalgebra::Vector3<f32>> {
340        let triangle_normals = parallel_triangle_normals(vertices, faces);
341
342        parallel_map_indexed(vertices, |vertex_idx, _vertex| {
343            let mut normal = nalgebra::Vector3::zeros();
344            let mut weight_sum = 0.0f32;
345
346            for (face_idx, face) in faces.iter().enumerate() {
347                if face.contains(&vertex_idx) {
348                    // Compute angle weight for this vertex in this triangle
349                    let face_normal = triangle_normals[face_idx];
350
351                    // Find the position of vertex_idx in the face
352                    let local_idx = face.iter().position(|&v| v == vertex_idx).unwrap();
353                    let v1_idx = face[local_idx];
354                    let v2_idx = face[(local_idx + 1) % 3];
355                    let v3_idx = face[(local_idx + 2) % 3];
356
357                    let v1 = &vertices[v1_idx];
358                    let v2 = &vertices[v2_idx];
359                    let v3 = &vertices[v3_idx];
360
361                    // Compute angle at vertex
362                    let edge1 = (v2 - v1).normalize();
363                    let edge2 = (v3 - v1).normalize();
364                    let angle = edge1.dot(&edge2).clamp(-1.0, 1.0).acos();
365
366                    normal += face_normal * angle;
367                    weight_sum += angle;
368                }
369            }
370
371            if weight_sum > 1e-6 {
372                normal / weight_sum
373            } else {
374                nalgebra::Vector3::new(0.0, 0.0, 1.0)
375            }
376        })
377    }
378}
379
380/// Example usage of parallel processing configuration
381///
382/// ```rust
383/// use threecrate_reconstruction::parallel::{init_thread_pool, ThreadPoolConfig};
384///
385/// // Configure thread pool with 4 threads and larger stack
386/// let config = ThreadPoolConfig::default()
387///     .with_threads(4)
388///     .with_stack_size(16 * 1024 * 1024)
389///     .with_chunk_size_range(200, 5000);
390///
391/// init_thread_pool(config).expect("Failed to initialize thread pool");
392///
393/// // Now all reconstruction algorithms will use parallel processing
394/// ```
395///
396/// For performance optimization, you can also disable parallel processing for small datasets:
397///
398/// ```rust
399/// use threecrate_reconstruction::parallel::ThreadPoolConfig;
400///
401/// let config = ThreadPoolConfig::default()
402///     .with_enabled(false); // Disable for debugging or small datasets
403/// ```
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use threecrate_core::Point3f;
409
410    #[test]
411    fn test_thread_pool_config() {
412        let config = ThreadPoolConfig::default()
413            .with_threads(4)
414            .with_stack_size(16 * 1024 * 1024)
415            .with_enabled(true);
416
417        assert_eq!(config.num_threads, Some(4));
418        assert_eq!(config.stack_size, Some(16 * 1024 * 1024));
419        assert!(config.enabled);
420    }
421
422    #[test]
423    fn test_chunk_size_computation() {
424        let data_size = 10000;
425        let chunk_size = compute_chunk_size(data_size);
426        let config = get_config();
427
428        assert!(chunk_size >= config.min_chunk_size);
429        assert!(chunk_size <= config.max_chunk_size);
430    }
431
432    #[test]
433    fn test_parallel_map() {
434        let data = vec![1, 2, 3, 4, 5];
435        let result = parallel_map(&data, |x| x * 2);
436        assert_eq!(result, vec![2, 4, 6, 8, 10]);
437    }
438
439    #[test]
440    fn test_parallel_filter() {
441        let data = vec![1, 2, 3, 4, 5, 6];
442        let result = parallel_filter(&data, |x| *x % 2 == 0);
443        assert_eq!(result, vec![2, 4, 6]);
444    }
445
446    #[test]
447    fn test_parallel_reduce() {
448        let data = vec![1, 2, 3, 4, 5];
449        let sum = parallel_reduce(&data, 0, |x| *x, |a, b| a + b);
450        assert_eq!(sum, 15);
451    }
452
453    #[test]
454    fn test_point_cloud_bounding_box() {
455        let points = vec![
456            Point3f::new(0.0, 0.0, 0.0),
457            Point3f::new(1.0, 1.0, 1.0),
458            Point3f::new(-1.0, -1.0, -1.0),
459            Point3f::new(2.0, 0.5, -0.5),
460        ];
461
462        let (min_pt, max_pt) = point_cloud::parallel_bounding_box(&points).unwrap();
463
464        assert_eq!(min_pt.x, -1.0);
465        assert_eq!(min_pt.y, -1.0);
466        assert_eq!(min_pt.z, -1.0);
467        assert_eq!(max_pt.x, 2.0);
468        assert_eq!(max_pt.y, 1.0);
469        assert_eq!(max_pt.z, 1.0);
470    }
471}