| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use crate::model::{Pt, Vectors}; |
| use std::collections::HashMap; |
|
|
| fn push_unique(v: &mut Vec<f64>, x: f64) { |
| if !v.iter().any(|&y| y == x) { |
| v.push(x); |
| } |
| } |
|
|
| |
| |
| pub fn segment_tau_forces(a: Pt, b: Pt, field: &[f64], width: usize, height: usize) -> (f64, f64) { |
| let dx = b[0] - a[0]; |
| let dy = b[1] - a[1]; |
| let length = dx.hypot(dy); |
| if length < 1e-12 { |
| return (0.0, 0.0); |
| } |
|
|
| let mut cuts: Vec<f64> = Vec::with_capacity(8); |
| push_unique(&mut cuts, 0.0); |
| push_unique(&mut cuts, 1.0); |
|
|
| if dx.abs() > 1e-12 { |
| let k0 = a[0].min(b[0]).ceil() as i64; |
| let k1 = a[0].max(b[0]).floor() as i64; |
| for k in k0..=k1 { |
| let t = (k as f64 - a[0]) / dx; |
| if t > 0.0 && t < 1.0 { |
| push_unique(&mut cuts, t); |
| } |
| } |
| } |
| if dy.abs() > 1e-12 { |
| let k0 = a[1].min(b[1]).ceil() as i64; |
| let k1 = a[1].max(b[1]).floor() as i64; |
| for k in k0..=k1 { |
| let t = (k as f64 - a[1]) / dy; |
| if t > 0.0 && t < 1.0 { |
| push_unique(&mut cuts, t); |
| } |
| } |
| } |
| cuts.sort_by(|p, q| p.partial_cmp(q).expect("no NaN in cut parameters")); |
|
|
| let mut i0 = 0.0_f64; |
| let mut i1 = 0.0_f64; |
| for w in cuts.windows(2) { |
| let (t0, t1) = (w[0], w[1]); |
| let tm = 0.5 * (t0 + t1); |
| let px = (a[0] + tm * dx) as i64; |
| let py = (a[1] + tm * dy) as i64; |
| if px >= 0 && (px as usize) < width && py >= 0 && (py as usize) < height { |
| let seg = field[py as usize * width + px as usize] * (t1 - t0) * length; |
| i0 += seg * (1.0 - tm); |
| i1 += seg * tm; |
| } |
| } |
| (i0, i1) |
| } |
|
|
| |
| pub fn vertex_gradients( |
| v: &Vectors, |
| img: &[f64], |
| target: &[f64], |
| ) -> HashMap<i64, Vec<Vec<[f64; 2]>>> { |
| let (width, height) = (v.width, v.height); |
| let n = width * height; |
| let scale = 2.0 / v.l0; |
| let colors = v.colors01(); |
|
|
| let mut out: HashMap<i64, Vec<Vec<[f64; 2]>>> = HashMap::new(); |
| let mut field = vec![0.0_f64; n]; |
|
|
| for e in &v.edges { |
| if !e.interior { |
| continue; |
| } |
| let cl = colors[&e.left_label]; |
| let cr = colors[&e.right_label]; |
| let cd = [cl[0] - cr[0], cl[1] - cr[1], cl[2] - cr[2]]; |
|
|
| |
| for p in 0..n { |
| let r0 = img[p * 3] - target[p * 3]; |
| let r1 = img[p * 3 + 1] - target[p * 3 + 1]; |
| let r2 = img[p * 3 + 2] - target[p * 3 + 2]; |
| field[p] = r0 * cd[0] + r1 * cd[1] + r2 * cd[2]; |
| } |
|
|
| let mut per_cubic: Vec<Vec<[f64; 2]>> = Vec::with_capacity(e.cubics.len()); |
| for pts in &e.cubics { |
| let m = pts.len(); |
| let mut vgrad = vec![[0.0_f64; 2]; m]; |
| for i in 0..m - 1 { |
| let sx = pts[i + 1][0] - pts[i][0]; |
| let sy = pts[i + 1][1] - pts[i][1]; |
| let length = sx.hypot(sy); |
| if length < 1e-12 { |
| continue; |
| } |
| let n0 = -sy / length; |
| let n1 = sx / length; |
| let (i0, i1) = segment_tau_forces(pts[i], pts[i + 1], &field, width, height); |
| |
| let f0 = scale * i0; |
| let f1 = scale * i1; |
| vgrad[i][0] += f0 * n0; |
| vgrad[i][1] += f0 * n1; |
| vgrad[i + 1][0] += f1 * n0; |
| vgrad[i + 1][1] += f1 * n1; |
| } |
| per_cubic.push(vgrad); |
| } |
| out.insert(e.id, per_cubic); |
| } |
| out |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| |
| |
| #[test] |
| fn tau_forces_split_evenly_for_a_uniform_field() { |
| let field = vec![2.0; 4]; |
| let (i0, i1) = segment_tau_forces([0.2, 0.2], [0.8, 0.2], &field, 2, 2); |
| let expected = 2.0 * 0.6 * 0.5; |
| assert!((i0 - expected).abs() < 1e-12, "i0 {i0} != {expected}"); |
| assert!((i1 - expected).abs() < 1e-12, "i1 {i1} != {expected}"); |
| } |
|
|
| #[test] |
| fn tau_forces_vanish_on_a_degenerate_segment() { |
| let field = vec![5.0; 4]; |
| let (i0, i1) = segment_tau_forces([0.5, 0.5], [0.5, 0.5], &field, 2, 2); |
| assert_eq!((i0, i1), (0.0, 0.0)); |
| } |
|
|
| #[test] |
| fn tau_forces_ignore_geometry_off_the_raster() { |
| let field = vec![7.0; 4]; |
| let (i0, i1) = segment_tau_forces([-5.0, -5.0], [-4.0, -5.0], &field, 2, 2); |
| assert_eq!((i0, i1), (0.0, 0.0)); |
| } |
|
|
| |
| |
| #[test] |
| fn tau_forces_split_at_pixel_boundaries() { |
| |
| let field = vec![1.0, 3.0]; |
| let (i0, i1) = segment_tau_forces([0.0, 0.5], [2.0, 0.5], &field, 2, 1); |
| |
| |
| assert!((i0 - 1.5).abs() < 1e-12, "i0 {i0}"); |
| assert!((i1 - 2.5).abs() < 1e-12, "i1 {i1}"); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| mod split { |
| use super::*; |
|
|
| pub const W: usize = 12; |
| pub const H: usize = 12; |
| pub const BG: f64 = 1.0; |
| pub const CA: [f64; 3] = [0.2, 0.4, 0.6]; |
| pub const CB: [f64; 3] = [0.9, 0.8, 0.7]; |
|
|
| pub fn render(x: f64) -> Vec<f64> { |
| use crate::coverage::polygon_coverage; |
| let a = vec![[0.0, 0.0], [x, 0.0], [x, H as f64], [0.0, H as f64]]; |
| let b = vec![ |
| [x, 0.0], |
| [W as f64, 0.0], |
| [W as f64, H as f64], |
| [x, H as f64], |
| ]; |
| let ca = polygon_coverage(&[a], W, H); |
| let cb = polygon_coverage(&[b], W, H); |
| let mut img = vec![BG; W * H * 3]; |
| for p in 0..W * H { |
| for ch in 0..3 { |
| img[p * 3 + ch] += |
| ca[p].abs() * (CA[ch] - BG) + cb[p].abs() * (CB[ch] - BG); |
| } |
| } |
| img |
| } |
|
|
| pub fn energy(x: f64, target: &[f64], l0: f64) -> f64 { |
| crate::energy::e_data(&render(x), target, l0) |
| } |
|
|
| |
| |
| pub fn analytic_dx(x: f64, target: &[f64], l0: f64) -> f64 { |
| let img = render(x); |
| |
| let cd = [CB[0] - CA[0], CB[1] - CA[1], CB[2] - CA[2]]; |
| let field: Vec<f64> = (0..W * H) |
| .map(|p| { |
| (0..3) |
| .map(|ch| (img[p * 3 + ch] - target[p * 3 + ch]) * cd[ch]) |
| .sum() |
| }) |
| .collect(); |
| let (a, b) = ([x, 0.0], [x, H as f64]); |
| let (sx, sy) = (b[0] - a[0], b[1] - a[1]); |
| let length = sx.hypot(sy); |
| let n0 = -sy / length; |
| let (i0, i1) = segment_tau_forces(a, b, &field, W, H); |
| let scale = 2.0 / l0; |
| scale * i0 * n0 + scale * i1 * n0 |
| } |
| } |
|
|
| |
| |
| |
| #[test] |
| fn gradient_matches_central_difference() { |
| let l0 = 1.0_f64; |
| let target = split::render(7.3); |
| let x0 = 4.37_f64; |
|
|
| let analytic = split::analytic_dx(x0, &target, l0); |
| let h = 1e-6; |
| let fd = (split::energy(x0 + h, &target, l0) - split::energy(x0 - h, &target, l0)) |
| / (2.0 * h); |
|
|
| let rel = (analytic - fd).abs() / fd.abs().max(1e-12); |
| assert!( |
| rel < 1e-5, |
| "analytic {analytic:.9e} vs finite-difference {fd:.9e} (rel {rel:.2e})" |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[test] |
| fn on_grid_line_the_derivative_is_one_sided_by_design() { |
| let l0 = 1.0_f64; |
| let target = split::render(7.3); |
| let h = 1e-6; |
|
|
| |
| let off = 4.37_f64; |
| let a_off = split::analytic_dx(off, &target, l0); |
| let fd_off = (split::energy(off + h, &target, l0) - split::energy(off - h, &target, l0)) |
| / (2.0 * h); |
| assert!( |
| (a_off - fd_off).abs() / fd_off.abs().max(1e-12) < 1e-5, |
| "off-grid should agree: {a_off} vs {fd_off}" |
| ); |
|
|
| |
| let on = 5.0_f64; |
| let a_on = split::analytic_dx(on, &target, l0); |
| let right = (split::energy(on + h, &target, l0) - split::energy(on, &target, l0)) / h; |
| let central = |
| (split::energy(on + h, &target, l0) - split::energy(on - h, &target, l0)) / (2.0 * h); |
| assert!( |
| (a_on - right).abs() / right.abs().max(1e-12) < 1e-4, |
| "on-grid analytic {a_on} should match the right derivative {right}" |
| ); |
| assert!( |
| (a_on - central).abs() / central.abs().max(1e-12) > 1e-3, |
| "the kink should be visible: analytic {a_on} vs central {central}" |
| ); |
| } |
| } |
|
|