| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use crate::coverage::polygon_coverage; |
| use crate::energy::{sum_compensated, Neumaier}; |
| use crate::gradient::segment_tau_forces; |
| use crate::model::Pt; |
| use std::collections::HashMap; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| #[derive(Clone, Debug)] |
| pub enum RegionColor { |
| Flat([f64; 3]), |
| Quad { |
| |
| coeffs: [[f64; 3]; 6], |
| cx: f64, |
| cy: f64, |
| s: f64, |
| }, |
| } |
|
|
| impl RegionColor { |
| |
| #[inline] |
| pub fn eval(&self, x: f64, y: f64) -> [f64; 3] { |
| match self { |
| RegionColor::Flat(c) => *c, |
| RegionColor::Quad { coeffs, cx, cy, s } => { |
| let u = (x - cx) / s; |
| let v = (y - cy) / s; |
| let b = [1.0, u, v, u * u, u * v, v * v]; |
| let mut out = [0.0_f64; 3]; |
| for (ch, o) in out.iter_mut().enumerate() { |
| let mut acc = 0.0; |
| for k in 0..6 { |
| acc += b[k] * coeffs[k][ch]; |
| } |
| |
| |
| *o = acc.clamp(0.0, 1.0); |
| } |
| out |
| } |
| } |
| } |
|
|
| #[inline] |
| pub fn is_quad(&self) -> bool { |
| matches!(self, RegionColor::Quad { .. }) |
| } |
| } |
|
|
| |
| pub struct Face { |
| pub label: usize, |
| pub loops: Vec<Vec<Pt>>, |
| } |
|
|
| |
| pub struct GradCubic { |
| pub left: usize, |
| pub right: usize, |
| |
| pub pts: Vec<Pt>, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| #[allow(clippy::too_many_arguments)] |
| pub fn windowed_data( |
| width: usize, |
| height: usize, |
| x0: f64, |
| y0: f64, |
| target: &[f64], |
| faces: &[Face], |
| colors: &[RegionColor], |
| background: f64, |
| l0: f64, |
| grads: &[GradCubic], |
| weights: Option<&[f64]>, |
| ) -> (f64, Vec<Vec<[f64; 2]>>) { |
| let n = width * height; |
|
|
| |
| |
| let mut quad_fields: HashMap<usize, Vec<[f64; 3]>> = HashMap::new(); |
| let field_for = |label: usize, cache: &mut HashMap<usize, Vec<[f64; 3]>>| { |
| if colors[label].is_quad() && !cache.contains_key(&label) { |
| let mut f = Vec::with_capacity(n); |
| for row in 0..height { |
| let gy = y0 + row as f64 + 0.5; |
| for col in 0..width { |
| f.push(colors[label].eval(x0 + col as f64 + 0.5, gy)); |
| } |
| } |
| cache.insert(label, f); |
| } |
| }; |
|
|
| |
| let mut img = vec![background; n * 3]; |
| for f in faces { |
| if f.loops.is_empty() { |
| continue; |
| } |
| let cov = polygon_coverage(&f.loops, width, height); |
| match &colors[f.label] { |
| RegionColor::Flat(c) => { |
| let (d0, d1, d2) = (c[0] - background, c[1] - background, c[2] - background); |
| for p in 0..n { |
| let a = cov[p].abs(); |
| img[p * 3] += a * d0; |
| img[p * 3 + 1] += a * d1; |
| img[p * 3 + 2] += a * d2; |
| } |
| } |
| RegionColor::Quad { .. } => { |
| field_for(f.label, &mut quad_fields); |
| let cf = &quad_fields[&f.label]; |
| for p in 0..n { |
| let a = cov[p].abs(); |
| img[p * 3] += a * (cf[p][0] - background); |
| img[p * 3 + 1] += a * (cf[p][1] - background); |
| img[p * 3 + 2] += a * (cf[p][2] - background); |
| } |
| } |
| } |
| } |
| for x in img.iter_mut() { |
| *x = x.clamp(0.0, 1.0); |
| } |
|
|
| |
| let mut resid = vec![0.0_f64; n * 3]; |
| for i in 0..n * 3 { |
| resid[i] = img[i] - target[i]; |
| } |
| let mut acc = Neumaier::default(); |
| for p in 0..n { |
| |
| let s = resid[p * 3] * resid[p * 3] |
| + resid[p * 3 + 1] * resid[p * 3 + 1] |
| + resid[p * 3 + 2] * resid[p * 3 + 2]; |
| acc.add(match weights { |
| Some(w) => s * w[p], |
| None => s, |
| }); |
| } |
| let e_data = acc.total() / l0; |
|
|
| |
| let scale = 2.0 / l0; |
| let mut field = vec![0.0_f64; n]; |
| let mut out: Vec<Vec<[f64; 2]>> = Vec::with_capacity(grads.len()); |
|
|
| for g in grads { |
| |
| |
| |
| let mixed = colors[g.left].is_quad() || colors[g.right].is_quad(); |
| if mixed { |
| field_for(g.left, &mut quad_fields); |
| field_for(g.right, &mut quad_fields); |
| let fl = quad_fields.get(&g.left); |
| let fr = quad_fields.get(&g.right); |
| let flat_l = if let RegionColor::Flat(c) = &colors[g.left] { *c } else { [0.0; 3] }; |
| let flat_r = if let RegionColor::Flat(c) = &colors[g.right] { *c } else { [0.0; 3] }; |
| for p in 0..n { |
| let cl = match fl { Some(f) => f[p], None => flat_l }; |
| let cr = match fr { Some(f) => f[p], None => flat_r }; |
| let v = resid[p * 3] * (cl[0] - cr[0]) |
| + resid[p * 3 + 1] * (cl[1] - cr[1]) |
| + resid[p * 3 + 2] * (cl[2] - cr[2]); |
| field[p] = match weights { |
| Some(w) => v * w[p], |
| None => v, |
| }; |
| } |
| } else { |
| let cl = if let RegionColor::Flat(c) = &colors[g.left] { *c } else { [0.0; 3] }; |
| let cr = if let RegionColor::Flat(c) = &colors[g.right] { *c } else { [0.0; 3] }; |
| let cd = [cl[0] - cr[0], cl[1] - cr[1], cl[2] - cr[2]]; |
| for p in 0..n { |
| let v = resid[p * 3] * cd[0] + resid[p * 3 + 1] * cd[1] + resid[p * 3 + 2] * cd[2]; |
| field[p] = match weights { |
| Some(w) => v * w[p], |
| None => v, |
| }; |
| } |
| } |
|
|
| let m = g.pts.len(); |
| let mut vgrad = vec![[0.0_f64; 2]; m]; |
| for i in 0..m.saturating_sub(1) { |
| let sx = g.pts[i + 1][0] - g.pts[i][0]; |
| let sy = g.pts[i + 1][1] - g.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(g.pts[i], g.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; |
| } |
| out.push(vgrad); |
| } |
|
|
| (e_data, out) |
| } |
|
|
| |
| |
| pub fn window_coverage_sums(width: usize, height: usize, faces: &[Face]) -> Vec<f64> { |
| faces |
| .iter() |
| .map(|f| { |
| if f.loops.is_empty() { |
| return 0.0; |
| } |
| let cov = polygon_coverage(&f.loops, width, height); |
| sum_compensated(cov.iter().map(|x| x.abs())) |
| }) |
| .collect() |
| } |
|
|
| #[cfg(feature = "python")] |
| pub use bindings::register; |
|
|
| #[cfg(feature = "python")] |
| mod bindings { |
| use super::*; |
| use numpy::{PyReadonlyArray2, PyReadonlyArray3, ToPyArray}; |
| use pyo3::prelude::*; |
| use pyo3::types::PyList; |
|
|
| |
| fn build_faces( |
| n_faces: usize, |
| face_labels: &[usize], |
| loop_face: &[usize], |
| loops: &Bound<'_, PyList>, |
| ) -> PyResult<Vec<Face>> { |
| let mut faces: Vec<Face> = (0..n_faces) |
| .map(|i| Face { |
| label: face_labels[i], |
| loops: Vec::new(), |
| }) |
| .collect(); |
| for (i, item) in loops.iter().enumerate() { |
| let arr: PyReadonlyArray2<f64> = item.extract()?; |
| let v = arr.as_array(); |
| let poly: Vec<Pt> = v.rows().into_iter().map(|r| [r[0], r[1]]).collect(); |
| faces[loop_face[i]].loops.push(poly); |
| } |
| Ok(faces) |
| } |
|
|
| |
| |
| |
| |
| #[pyfunction] |
| #[pyo3(signature = (width, height, x0, y0, target, face_labels, loop_face, loops, colors01, |
| color_kind, quad_coeffs, quad_transform, background, l0, |
| grad_left, grad_right, grad_pts, weights=None))] |
| #[allow(clippy::too_many_arguments)] |
| fn windowed_data<'py>( |
| py: Python<'py>, |
| width: usize, |
| height: usize, |
| x0: f64, |
| y0: f64, |
| target: PyReadonlyArray3<'py, f64>, |
| face_labels: Vec<usize>, |
| loop_face: Vec<usize>, |
| loops: &Bound<'py, PyList>, |
| colors01: PyReadonlyArray2<'py, f64>, |
| |
| |
| |
| color_kind: Vec<u8>, |
| quad_coeffs: PyReadonlyArray3<'py, f64>, |
| quad_transform: PyReadonlyArray2<'py, f64>, |
| background: f64, |
| l0: f64, |
| grad_left: Vec<usize>, |
| grad_right: Vec<usize>, |
| grad_pts: &Bound<'py, PyList>, |
| weights: Option<PyReadonlyArray2<'py, f64>>, |
| ) -> PyResult<(f64, Py<numpy::PyArray3<f64>>)> { |
| let faces = build_faces(face_labels.len(), &face_labels, &loop_face, loops)?; |
|
|
| let colors_view = colors01.as_array(); |
| let qc = quad_coeffs.as_array(); |
| let qt = quad_transform.as_array(); |
| let colors: Vec<RegionColor> = colors_view |
| .rows() |
| .into_iter() |
| .enumerate() |
| .map(|(i, r)| { |
| if color_kind.get(i).copied().unwrap_or(0) == 1 { |
| let mut coeffs = [[0.0_f64; 3]; 6]; |
| for (k, row) in coeffs.iter_mut().enumerate() { |
| for (ch, c) in row.iter_mut().enumerate() { |
| *c = qc[[i, k, ch]]; |
| } |
| } |
| RegionColor::Quad { coeffs, cx: qt[[i, 0]], cy: qt[[i, 1]], s: qt[[i, 2]] } |
| } else { |
| RegionColor::Flat([r[0], r[1], r[2]]) |
| } |
| }) |
| .collect(); |
|
|
| let mut grads: Vec<GradCubic> = Vec::with_capacity(grad_left.len()); |
| for (i, item) in grad_pts.iter().enumerate() { |
| let arr: PyReadonlyArray2<f64> = item.extract()?; |
| let v = arr.as_array(); |
| grads.push(GradCubic { |
| left: grad_left[i], |
| right: grad_right[i], |
| pts: v.rows().into_iter().map(|r| [r[0], r[1]]).collect(), |
| }); |
| } |
|
|
| let t = target.as_array(); |
| let t_slice: Vec<f64> = t.iter().copied().collect(); |
| let w_slice: Option<Vec<f64>> = weights.map(|w| w.as_array().iter().copied().collect()); |
|
|
| let (e, g) = super::windowed_data( |
| width, |
| height, |
| x0, |
| y0, |
| &t_slice, |
| &faces, |
| &colors, |
| background, |
| l0, |
| &grads, |
| w_slice.as_deref(), |
| ); |
|
|
| let m = g.first().map(|v| v.len()).unwrap_or(0); |
| let mut flat = Vec::with_capacity(g.len() * m * 2); |
| for vg in &g { |
| for p in vg { |
| flat.push(p[0]); |
| flat.push(p[1]); |
| } |
| } |
| let arr = numpy::ndarray::Array3::from_shape_vec((g.len(), m, 2), flat) |
| .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; |
| Ok((e, arr.to_pyarray(py).unbind())) |
| } |
|
|
| |
| |
| #[pyfunction] |
| fn window_coverage_sums<'py>( |
| py: Python<'py>, |
| width: usize, |
| height: usize, |
| face_labels: Vec<usize>, |
| loop_face: Vec<usize>, |
| loops: &Bound<'py, PyList>, |
| ) -> PyResult<Py<numpy::PyArray1<f64>>> { |
| let faces = build_faces(face_labels.len(), &face_labels, &loop_face, loops)?; |
| let sums = super::window_coverage_sums(width, height, &faces); |
| Ok(numpy::ndarray::Array1::from_vec(sums) |
| .to_pyarray(py) |
| .unbind()) |
| } |
|
|
| pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { |
| m.add_function(wrap_pyfunction!(self::windowed_data, m)?)?; |
| m.add_function(wrap_pyfunction!(self::window_coverage_sums, m)?)?; |
| Ok(()) |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| |
| |
| #[test] |
| fn full_coverage_window_has_zero_energy_against_matching_target() { |
| let (w, h) = (4_usize, 4_usize); |
| let face = Face { |
| label: 1, |
| loops: vec![vec![ |
| [0.0, 0.0], |
| [w as f64, 0.0], |
| [w as f64, h as f64], |
| [0.0, h as f64], |
| ]], |
| }; |
| let colors = [ |
| RegionColor::Flat([1.0, 1.0, 1.0]), |
| RegionColor::Flat([0.25, 0.5, 0.75]), |
| ]; |
| let mut target = vec![0.0; w * h * 3]; |
| for p in 0..w * h { |
| target[p * 3] = 0.25; |
| target[p * 3 + 1] = 0.5; |
| target[p * 3 + 2] = 0.75; |
| } |
| let (e, g) = windowed_data(w, h, 0.0, 0.0, &target, &[face], &colors, 1.0, 1.0, &[], None); |
| assert!(e.abs() < 1e-15, "energy {e} should vanish"); |
| assert!(g.is_empty()); |
| } |
|
|
| |
| |
| #[test] |
| fn empty_window_is_background_over_l0() { |
| let (w, h) = (2_usize, 3_usize); |
| let target = vec![0.0; w * h * 3]; |
| let flat = [RegionColor::Flat([0.0, 0.0, 0.0])]; |
| let (e, _) = windowed_data(w, h, 0.0, 0.0, &target, &[], &flat, 1.0, 2.0, &[], None); |
| |
| assert!((e - 9.0).abs() < 1e-12, "energy {e}"); |
| } |
|
|
| |
| #[test] |
| fn weights_scale_the_energy() { |
| let (w, h) = (2_usize, 2_usize); |
| let target = vec![0.0; w * h * 3]; |
| let weights = vec![0.5; w * h]; |
| let flat = [RegionColor::Flat([0.0, 0.0, 0.0])]; |
| let (unweighted, _) = |
| windowed_data(w, h, 0.0, 0.0, &target, &[], &flat, 1.0, 1.0, &[], None); |
| let (weighted, _) = |
| windowed_data(w, h, 0.0, 0.0, &target, &[], &flat, 1.0, 1.0, &[], Some(&weights)); |
| assert!((weighted - 0.5 * unweighted).abs() < 1e-12); |
| } |
|
|
| |
| |
| #[test] |
| fn negative_coordinates_truncate_toward_zero_not_floor() { |
| let field = vec![1.0; 4]; |
| |
| let (a0, a1) = segment_tau_forces([-0.5, 0.5], [-0.5, 1.5], &field, 2, 2); |
| assert!( |
| a0.abs() + a1.abs() > 0.0, |
| "x=-0.5 truncates to column 0 and must contribute" |
| ); |
| |
| let (b0, b1) = segment_tau_forces([-1.5, 0.5], [-1.5, 1.5], &field, 2, 2); |
| assert_eq!((b0, b1), (0.0, 0.0), "x=-1.5 truncates to -1 and is outside"); |
| } |
|
|
| #[test] |
| fn coverage_sums_report_per_face_area() { |
| let (w, h) = (6_usize, 6_usize); |
| let f = Face { |
| label: 0, |
| loops: vec![vec![[1.0, 1.0], [4.0, 1.0], [4.0, 3.0], [1.0, 3.0]]], |
| }; |
| let sums = window_coverage_sums(w, h, &[f]); |
| assert!((sums[0] - 6.0).abs() < 1e-12, "area {}", sums[0]); |
| } |
| } |
|
|
| #[cfg(test)] |
| mod quad_tests { |
| use super::*; |
|
|
| fn quad(coeffs: [[f64; 3]; 6], cx: f64, cy: f64, s: f64) -> RegionColor { |
| RegionColor::Quad { coeffs, cx, cy, s } |
| } |
|
|
| |
| |
| #[test] |
| fn quadratic_eval_matches_a_direct_reference() { |
| let coeffs = [ |
| [0.50, 0.40, 0.30], |
| [0.10, -0.05, 0.02], |
| [-0.03, 0.07, 0.01], |
| [0.01, 0.02, -0.01], |
| [0.02, -0.01, 0.03], |
| [-0.02, 0.01, 0.02], |
| ]; |
| let (cx, cy, s) = (37.5, 21.25, 18.0); |
| let c = quad(coeffs, cx, cy, s); |
| for &(x, y) in &[(30.0, 20.0), (37.5, 21.25), (48.0, 33.0), (12.5, 5.5)] { |
| let u = (x - cx) / s; |
| let v = (y - cy) / s; |
| let b = [1.0, u, v, u * u, u * v, v * v]; |
| let got = c.eval(x, y); |
| for ch in 0..3 { |
| let want: f64 = (0..6).map(|k| b[k] * coeffs[k][ch]).sum::<f64>().clamp(0.0, 1.0); |
| assert!( |
| (got[ch] - want).abs() < 1e-15, |
| "ch{ch} at ({x},{y}): {} vs {want}", |
| got[ch] |
| ); |
| } |
| } |
| } |
|
|
| |
| |
| #[test] |
| fn quadratic_eval_is_clipped() { |
| let mut coeffs = [[0.0_f64; 3]; 6]; |
| coeffs[0] = [5.0, -5.0, 0.5]; |
| let c = quad(coeffs, 0.0, 0.0, 1.0); |
| assert_eq!(c.eval(1.0, 1.0), [1.0, 0.0, 0.5]); |
| } |
|
|
| |
| |
| |
| #[test] |
| fn far_from_origin_is_a_non_event_thanks_to_the_transform() { |
| let coeffs = [ |
| [0.5, 0.5, 0.5], |
| [0.2, 0.1, 0.0], |
| [0.0, 0.1, 0.2], |
| [0.05, 0.0, 0.0], |
| [0.0, 0.05, 0.0], |
| [0.0, 0.0, 0.05], |
| ]; |
| let near = quad(coeffs, 50.0, 50.0, 25.0); |
| let far = quad(coeffs, 10_050.0, 10_050.0, 25.0); |
| for &(dx, dy) in &[(-20.0, -20.0), (0.0, 0.0), (17.0, -8.0), (25.0, 25.0)] { |
| let a = near.eval(50.0 + dx, 50.0 + dy); |
| let b = far.eval(10_050.0 + dx, 10_050.0 + dy); |
| for ch in 0..3 { |
| assert!( |
| (a[ch] - b[ch]).abs() < 1e-15, |
| "translation changed the colour: {a:?} vs {b:?}" |
| ); |
| } |
| } |
| |
| let u: f64 = (10_075.0 - 10_050.0) / 25.0; |
| assert!(u.abs() <= 1.0 + 1e-12, "u should be O(1), got {u}"); |
| } |
|
|
| |
| |
| |
| #[test] |
| fn mixed_flat_quadratic_jump_is_position_dependent() { |
| const W: usize = 10; |
| const H: usize = 10; |
| let coeffs = [ |
| [0.9, 0.2, 0.2], |
| [0.4, 0.0, 0.0], |
| [0.0, 0.0, 0.0], |
| [0.0, 0.0, 0.0], |
| [0.0, 0.0, 0.0], |
| [0.0, 0.0, 0.0], |
| ]; |
| let colors = vec![ |
| RegionColor::Flat([0.1, 0.1, 0.1]), |
| quad(coeffs, 5.0, 5.0, 5.0), |
| ]; |
| let left = vec![[0.0, 0.0], [5.0, 0.0], [5.0, H as f64], [0.0, H as f64]]; |
| let right = vec![ |
| [5.0, 0.0], |
| [W as f64, 0.0], |
| [W as f64, H as f64], |
| [5.0, H as f64], |
| ]; |
| let faces = vec![ |
| Face { label: 0, loops: vec![left] }, |
| Face { label: 1, loops: vec![right] }, |
| ]; |
| let target = vec![0.5; W * H * 3]; |
| let grads = vec![GradCubic { |
| left: 1, |
| right: 0, |
| pts: vec![[5.0, 1.0], [5.0, 5.0], [5.0, 9.0]], |
| }]; |
|
|
| let (_, g_a) = windowed_data( |
| W, H, 0.0, 0.0, &target, &faces, &colors, 1.0, 1.0, &grads, None, |
| ); |
| |
| |
| let colors_b = vec![ |
| RegionColor::Flat([0.1, 0.1, 0.1]), |
| quad(coeffs, 1.0, 5.0, 5.0), |
| ]; |
| let (_, g_b) = windowed_data( |
| W, H, 0.0, 0.0, &target, &faces, &colors_b, 1.0, 1.0, &grads, None, |
| ); |
| let diff: f64 = g_a[0] |
| .iter() |
| .zip(&g_b[0]) |
| .map(|(a, b)| (a[0] - b[0]).abs() + (a[1] - b[1]).abs()) |
| .sum(); |
| assert!( |
| diff > 1e-9, |
| "gradient did not respond to the quadratic transform — the colour jump was collapsed \ |
| to a per-edge constant (Trap 1), diff {diff:e}" |
| ); |
| } |
|
|
| |
| |
| #[test] |
| fn quadratic_gradient_matches_central_difference() { |
| const W: usize = 14; |
| const H: usize = 14; |
| let coeffs = [ |
| [0.30, 0.55, 0.70], |
| [0.15, -0.10, 0.05], |
| [-0.08, 0.12, 0.03], |
| [0.02, 0.01, -0.02], |
| [0.03, -0.02, 0.01], |
| [-0.01, 0.02, 0.02], |
| ]; |
| let colors = vec![ |
| quad(coeffs, 7.0, 7.0, 7.0), |
| RegionColor::Flat([0.85, 0.80, 0.75]), |
| ]; |
|
|
| let scene = |x: f64| -> Vec<Face> { |
| vec![ |
| Face { |
| label: 0, |
| loops: vec![vec![[0.0, 0.0], [x, 0.0], [x, H as f64], [0.0, H as f64]]], |
| }, |
| Face { |
| label: 1, |
| loops: vec![vec![ |
| [x, 0.0], |
| [W as f64, 0.0], |
| [W as f64, H as f64], |
| [x, H as f64], |
| ]], |
| }, |
| ] |
| }; |
| let energy = |x: f64, target: &[f64]| -> f64 { |
| windowed_data(W, H, 0.0, 0.0, target, &scene(x), &colors, 1.0, 1.0, &[], None).0 |
| }; |
|
|
| |
| let target = { |
| let mut img = vec![1.0_f64; W * H * 3]; |
| let faces = scene(9.3); |
| for f in &faces { |
| let cov = crate::coverage::polygon_coverage(&f.loops, W, H); |
| for p in 0..W * H { |
| let row = p / W; |
| let col = p % W; |
| let c = colors[f.label].eval(col as f64 + 0.5, row as f64 + 0.5); |
| for ch in 0..3 { |
| img[p * 3 + ch] += cov[p].abs() * (c[ch] - 1.0); |
| } |
| } |
| } |
| for v in img.iter_mut() { |
| *v = v.clamp(0.0, 1.0); |
| } |
| img |
| }; |
|
|
| let x0 = 5.37_f64; |
| |
| |
| |
| |
| let grads = vec![GradCubic { |
| left: 1, |
| right: 0, |
| pts: vec![[x0, 0.0], [x0, H as f64]], |
| }]; |
| let (_, g) = windowed_data( |
| W, H, 0.0, 0.0, &target, &scene(x0), &colors, 1.0, 1.0, &grads, None, |
| ); |
| let analytic = g[0][0][0] + g[0][1][0]; |
|
|
| let h = 1e-6; |
| let fd = (energy(x0 + h, &target) - energy(x0 - h, &target)) / (2.0 * h); |
| let rel = (analytic - fd).abs() / fd.abs().max(1e-12); |
| assert!( |
| rel < 1e-4, |
| "quadratic analytic {analytic:.9e} vs finite-difference {fd:.9e} (rel {rel:.2e})" |
| ); |
| } |
| } |
|
|