1use crate::Grid;
16
17#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
19#[derive(Clone, Debug)]
20pub struct LbvParams {
21 pub tol: f64,
23 pub max_iter: Option<usize>,
26}
27
28impl Default for LbvParams {
29 fn default() -> Self {
30 Self { tol: 1e-6, max_iter: None }
31 }
32}
33
34pub fn lbv(
49 field: &[f64],
50 mask: &[u8],
51 grid: &Grid,
52 params: &LbvParams,
53 progress: impl FnMut(usize, usize),
54) -> (Vec<f64>, Vec<u8>) {
55 let max_iter = params.max_iter.unwrap_or_else(|| {
57 let (nx, ny, nz) = grid.dims;
58 (3 * nx.max(ny).max(nz)).min(500)
59 });
60 lbv_core(field, mask, grid, params.tol, max_iter, progress)
61}
62
63pub(crate) fn lbv_core(
68 field: &[f64],
69 mask: &[u8],
70 grid: &Grid,
71 tol: f64,
72 max_iter: usize,
73 mut progress: impl FnMut(usize, usize),
74) -> (Vec<f64>, Vec<u8>) {
75 let (nx, ny, nz) = grid.dims;
76 let (vsx, vsy, vsz) = grid.voxel_size;
77 let n_total = nx * ny * nz;
78
79 let dx2_inv = 1.0 / (vsx * vsx);
81 let dy2_inv = 1.0 / (vsy * vsy);
82 let dz2_inv = 1.0 / (vsz * vsz);
83 let diag = -2.0 * (dx2_inv + dy2_inv + dz2_inv);
84
85 let mut interior = vec![false; n_total];
89 let mut boundary = vec![false; n_total];
90
91 for z in 1..(nz - 1) {
92 for y in 1..(ny - 1) {
93 for x in 1..(nx - 1) {
94 let idx = x + y * nx + z * nx * ny;
95 if mask[idx] == 0 {
96 continue;
97 }
98
99 let neighbors = [
101 idx.wrapping_sub(1), idx + 1, idx.wrapping_sub(nx), idx + nx, idx.wrapping_sub(nx * ny), idx + nx * ny, ];
108
109 let all_inside = neighbors.iter().all(|&n| n < n_total && mask[n] != 0);
110
111 if all_inside {
112 interior[idx] = true;
113 } else {
114 boundary[idx] = true;
115 }
116 }
117 }
118 }
119
120 for z in 0..nz {
122 for y in 0..ny {
123 for x in 0..nx {
124 if z == 0 || z == nz - 1 || y == 0 || y == ny - 1 || x == 0 || x == nx - 1 {
125 let idx = x + y * nx + z * nx * ny;
126 if mask[idx] != 0 {
127 boundary[idx] = true;
128 interior[idx] = false;
129 }
130 }
131 }
132 }
133 }
134
135 let mut bg_field = field.to_vec();
138
139 let field_scale = field.iter()
143 .map(|&v| v.abs())
144 .fold(0.0f64, f64::max)
145 .max(1.0); let scaled_tol = tol * field_scale;
147
148 let omega = 1.5; for iter in 0..max_iter {
153 if iter % 10 == 0 {
154 progress(iter, max_iter);
155 }
156
157 let mut max_change = 0.0f64;
158
159 for z in 1..(nz - 1) {
160 for y in 1..(ny - 1) {
161 for x in 1..(nx - 1) {
162 let idx = x + y * nx + z * nx * ny;
163
164 if !interior[idx] {
165 continue;
166 }
167
168 let sum = dx2_inv * (bg_field[idx - 1] + bg_field[idx + 1])
170 + dy2_inv * (bg_field[idx - nx] + bg_field[idx + nx])
171 + dz2_inv * (bg_field[idx - nx * ny] + bg_field[idx + nx * ny]);
172
173 let new_val = -sum / diag;
175
176 let old_val = bg_field[idx];
178 let updated = old_val + omega * (new_val - old_val);
179
180 max_change = max_change.max((updated - old_val).abs());
181 bg_field[idx] = updated;
182 }
183 }
184 }
185
186 if max_change < scaled_tol {
188 progress(iter + 1, iter + 1);
189 break;
190 }
191 }
192
193 progress(max_iter, max_iter);
194
195 let mut local_field = vec![0.0; n_total];
197 let mut eroded_mask = vec![0u8; n_total];
198
199 for i in 0..n_total {
200 if interior[i] {
201 local_field[i] = field[i] - bg_field[i];
202 eroded_mask[i] = 1;
203 }
204 }
205
206 (local_field, eroded_mask)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_lbv_zero_field() {
215 let n = 8;
216 let field = vec![0.0; n * n * n];
217 let mask = vec![1u8; n * n * n];
218 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
219
220 let (local, _) = lbv_core(&field, &mask, &grid, 1e-6, 100, |_, _| {});
221
222 for &val in local.iter() {
223 assert!(val.abs() < 1e-10, "Zero field should give zero local field");
224 }
225 }
226
227 #[test]
228 fn test_lbv_finite() {
229 let n = 16;
230 let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
231
232 let mut mask = vec![0u8; n * n * n];
234 let center = n / 2;
235 let radius = n / 3;
236
237 for z in 0..n {
238 for y in 0..n {
239 for x in 0..n {
240 let dx = (x as i32) - (center as i32);
241 let dy = (y as i32) - (center as i32);
242 let dz = (z as i32) - (center as i32);
243 if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
244 mask[x + y * n + z * n * n] = 1;
245 }
246 }
247 }
248 }
249
250 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
251 let (local, eroded_mask) = lbv_core(&field, &mask, &grid, 1e-5, 100, |_, _| {});
252
253 for (i, &val) in local.iter().enumerate() {
254 assert!(val.is_finite(), "Local field should be finite at index {}", i);
255 }
256
257 let eroded_count: usize = eroded_mask.iter().map(|&x| x as usize).sum();
259 let mask_count: usize = mask.iter().map(|&x| x as usize).sum();
260 assert!(eroded_count <= mask_count, "Eroded mask should be <= original mask");
261 assert!(eroded_count > 0, "Eroded mask should not be empty for reasonable-sized input");
262 }
263
264 #[test]
265 fn test_lbv_harmonic_removal() {
266 let n = 16;
269 let mut field = vec![0.0; n * n * n];
270
271 for z in 0..n {
273 for y in 0..n {
274 for x in 0..n {
275 let idx = x + y * n + z * n * n;
276 field[idx] = (z as f64) * 0.1; }
278 }
279 }
280
281 let mut mask = vec![0u8; n * n * n];
283 let center = n / 2;
284 let radius = n / 3;
285
286 for z in 0..n {
287 for y in 0..n {
288 for x in 0..n {
289 let dx = (x as i32) - (center as i32);
290 let dy = (y as i32) - (center as i32);
291 let dz = (z as i32) - (center as i32);
292 if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
293 mask[x + y * n + z * n * n] = 1;
294 }
295 }
296 }
297 }
298
299 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
300 let (local, eroded_mask) = lbv_core(&field, &mask, &grid, 1e-6, 500, |_, _| {});
301
302 let mut max_local: f64 = 0.0;
305 for i in 0..n*n*n {
306 if eroded_mask[i] != 0 {
307 max_local = max_local.max(local[i].abs());
308 }
309 }
310
311 assert!(max_local < 0.5, "Harmonic field should be mostly removed, got max {}", max_local);
313 }
314
315 #[test]
316 fn test_lbv_nonuniform_voxels() {
317 let n = 16;
318
319 let mut field = vec![0.0; n * n * n];
321 for z in 0..n {
322 for y in 0..n {
323 for x in 0..n {
324 let idx = x + y * n + z * n * n;
325 field[idx] = (z as f64) * 0.1;
326 }
327 }
328 }
329
330 let mut mask = vec![0u8; n * n * n];
332 let center = n / 2;
333 let radius = n / 3;
334 for z in 0..n {
335 for y in 0..n {
336 for x in 0..n {
337 let dx = (x as i32) - (center as i32);
338 let dy = (y as i32) - (center as i32);
339 let dz = (z as i32) - (center as i32);
340 if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
341 mask[x + y * n + z * n * n] = 1;
342 }
343 }
344 }
345 }
346
347 let grid = Grid::new(n, n, n, 0.5, 1.0, 2.0);
349 let (local, eroded_mask) = lbv_core(
350 &field, &mask, &grid, 1e-5, 200, |_, _| {}
351 );
352
353 for (i, &val) in local.iter().enumerate() {
355 assert!(val.is_finite(), "LBV nonuniform voxels: finite at index {}", i);
356 }
357
358 let eroded_count: usize = eroded_mask.iter().map(|&x| x as usize).sum();
360 assert!(eroded_count > 0, "LBV nonuniform: eroded mask should not be empty");
361 }
362
363 #[test]
364 fn test_lbv_tolerance() {
365 let n = 16;
366
367 let mut field = vec![0.0; n * n * n];
369 for z in 0..n {
370 for y in 0..n {
371 for x in 0..n {
372 let idx = x + y * n + z * n * n;
373 field[idx] = (z as f64) * 0.1;
374 }
375 }
376 }
377
378 let mut mask = vec![0u8; n * n * n];
380 let center = n / 2;
381 let radius = n / 3;
382 for z in 0..n {
383 for y in 0..n {
384 for x in 0..n {
385 let dx = (x as i32) - (center as i32);
386 let dy = (y as i32) - (center as i32);
387 let dz = (z as i32) - (center as i32);
388 if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
389 mask[x + y * n + z * n * n] = 1;
390 }
391 }
392 }
393 }
394
395 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
396
397 let (local_tight, _) = lbv_core(&field, &mask, &grid, 1e-8, 1000, |_, _| {});
399
400 let (local_loose, _) = lbv_core(&field, &mask, &grid, 1e-2, 50, |_, _| {});
402
403 let max_tight: f64 = local_tight.iter()
405 .zip(mask.iter())
406 .filter(|(_, &m)| m != 0)
407 .map(|(&v, _)| v.abs())
408 .fold(0.0f64, f64::max);
409
410 let max_loose: f64 = local_loose.iter()
411 .zip(mask.iter())
412 .filter(|(_, &m)| m != 0)
413 .map(|(&v, _)| v.abs())
414 .fold(0.0f64, f64::max);
415
416 assert!(
418 max_tight <= max_loose + 1e-6,
419 "Tight tolerance max={} should be <= loose tolerance max={}",
420 max_tight, max_loose
421 );
422 }
423
424 #[test]
425 fn test_lbv_with_progress() {
426 let n = 16;
427 let mut field = vec![0.0; n * n * n];
428 for z in 0..n {
429 for y in 0..n {
430 for x in 0..n {
431 field[x + y * n + z * n * n] = (z as f64) * 0.1;
432 }
433 }
434 }
435
436 let mut mask = vec![0u8; n * n * n];
437 let center = n / 2;
438 let radius = n / 3;
439 for z in 0..n {
440 for y in 0..n {
441 for x in 0..n {
442 let dx = (x as i32) - (center as i32);
443 let dy = (y as i32) - (center as i32);
444 let dz = (z as i32) - (center as i32);
445 if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
446 mask[x + y * n + z * n * n] = 1;
447 }
448 }
449 }
450 }
451
452 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
453 let mut progress_calls = Vec::new();
454 let (local, eroded) = lbv_core(
455 &field, &mask, &grid, 1e-5, 200,
456 |iter, max| { progress_calls.push((iter, max)); }
457 );
458
459 assert_eq!(local.len(), n * n * n);
460 assert!(!progress_calls.is_empty(), "Progress callback should be called");
461 for &val in &local {
462 assert!(val.is_finite());
463 }
464 let eroded_count: usize = eroded.iter().map(|&x| x as usize).sum();
465 assert!(eroded_count > 0);
466 }
467
468 #[test]
469 fn test_lbv_non_harmonic_field() {
470 let n = 16;
472 let mut field = vec![0.0; n * n * n];
473 for z in 0..n {
475 for y in 0..n {
476 for x in 0..n {
477 let xf = (x as f64) / (n as f64);
478 field[x + y * n + z * n * n] = xf * xf;
479 }
480 }
481 }
482
483 let mut mask = vec![0u8; n * n * n];
484 let center = n / 2;
485 let radius = n / 3;
486 for z in 0..n {
487 for y in 0..n {
488 for x in 0..n {
489 let dx = (x as i32) - (center as i32);
490 let dy = (y as i32) - (center as i32);
491 let dz = (z as i32) - (center as i32);
492 if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
493 mask[x + y * n + z * n * n] = 1;
494 }
495 }
496 }
497 }
498
499 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
500 let (local, eroded_mask) = lbv_core(&field, &mask, &grid, 1e-6, 500, |_, _| {});
501
502 let mut has_nonzero = false;
504 for i in 0..n * n * n {
505 assert!(local[i].is_finite());
506 if eroded_mask[i] != 0 && local[i].abs() > 1e-10 {
507 has_nonzero = true;
508 }
509 }
510 assert!(has_nonzero, "Non-harmonic field should leave non-zero local field");
511 }
512
513 #[test]
514 fn test_lbv_small_mask() {
515 let n = 8;
517 let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.01).collect();
518
519 let mut mask = vec![0u8; n * n * n];
521 for z in 2..6 {
522 for y in 2..6 {
523 for x in 2..6 {
524 mask[x + y * n + z * n * n] = 1;
525 }
526 }
527 }
528
529 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
530 let (local, eroded) = lbv_core(&field, &mask, &grid, 1e-5, 100, |_, _| {});
531
532 for &val in &local {
533 assert!(val.is_finite());
534 }
535
536 let eroded_count: usize = eroded.iter().map(|&x| x as usize).sum();
538 let mask_count: usize = mask.iter().map(|&x| x as usize).sum();
539 assert!(eroded_count <= mask_count);
540 }
541
542 #[test]
543 fn test_lbv_edge_mask_voxels() {
544 let n = 8;
546 let field = vec![1.0; n * n * n];
547 let mask = vec![1u8; n * n * n]; let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
549
550 let (local, eroded) = lbv_core(&field, &mask, &grid, 1e-5, 100, |_, _| {});
551
552 for &val in &local {
553 assert!(val.is_finite());
554 }
555
556 assert_eq!(eroded[0], 0, "Corner should not be in eroded mask");
559 assert_eq!(eroded[n - 1], 0, "Corner should not be in eroded mask");
560
561 let eroded_count: usize = eroded.iter().map(|&x| x as usize).sum();
562 assert!(eroded_count > 0, "Should have some interior voxels");
563 }
564}