1use std::fmt;
22
23use crate::affine::Affine;
24use crate::domain::Domain;
25use crate::frame::Frame;
26use crate::mapping::{CellConvention, Mapping};
27use crate::positions::{CoordArray, Positions, Rung};
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum Tolerance {
32 Float32,
35 Float64,
37 Absolute(f64),
39 RelativeToStep(f64),
41}
42
43impl Tolerance {
44 fn value(&self, max_abs: f64, step: f64) -> f64 {
45 match *self {
46 Tolerance::Float32 => ulp_f32(max_abs),
47 Tolerance::Float64 => 16.0 * max_abs.max(f64::MIN_POSITIVE) * f64::EPSILON,
48 Tolerance::Absolute(t) => t,
49 Tolerance::RelativeToStep(f) => f * step.abs(),
50 }
51 }
52}
53
54fn ulp_f32(m: f64) -> f64 {
56 let m = m.abs() as f32;
57 if m == 0.0 || !m.is_finite() {
58 return f32::MIN_POSITIVE as f64;
59 }
60 let next = f32::from_bits(m.to_bits() + 1);
61 (next - m) as f64
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub struct Report {
67 pub dims: Vec<usize>,
69 pub from: Rung,
70 pub to: Option<Rung>,
72 pub frame: Frame,
74 pub through: Option<String>,
76 pub max_deviation: f64,
79 pub max_deviation_steps: f64,
81 pub tolerance: f64,
83 pub accepted: bool,
84}
85
86impl fmt::Display for Report {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 let verdict = if self.accepted {
89 "regular"
90 } else {
91 "not regular"
92 };
93 write!(
94 f,
95 "dims {:?}: {:?} -> {}{}; max deviation {:.3e} ({:.2e} steps), tolerance {:.3e}",
96 self.dims,
97 self.from,
98 verdict,
99 self.through
100 .as_ref()
101 .map(|t| format!(" in {t}"))
102 .unwrap_or_default(),
103 self.max_deviation,
104 self.max_deviation_steps,
105 self.tolerance
106 )
107 }
108}
109
110fn fit_line(c: &[f64], cell: CellConvention) -> Option<(f64, f64, f64)> {
113 let n = c.len();
114 if n < 2 || c.iter().any(|v| !v.is_finite()) {
115 return None;
116 }
117 let step = (c[n - 1] - c[0]) / (n - 1) as f64;
118 if step == 0.0 {
119 return None;
120 }
121 let max_dev = c
122 .iter()
123 .enumerate()
124 .map(|(k, v)| (v - (c[0] + k as f64 * step)).abs())
125 .fold(0.0, f64::max);
126 let origin = match cell {
127 CellConvention::Centre => c[0] - 0.5 * step,
128 CellConvention::Edge => c[0],
129 };
130 Some((origin, step, max_dev))
131}
132
133pub fn regular_axis(
135 coords: &[f64],
136 cell: CellConvention,
137 tolerance: Tolerance,
138) -> (Option<Positions>, Report) {
139 regular_axis_inner(coords, cell, tolerance, None)
140}
141
142pub fn regular_axis_through(
146 coords: &[f64],
147 f: impl Fn(f64) -> f64,
148 through: &str,
149 cell: CellConvention,
150 tolerance: Tolerance,
151) -> (Option<Positions>, Report) {
152 let mapped: Vec<f64> = coords.iter().map(|&v| f(v)).collect();
153 regular_axis_inner(&mapped, cell, tolerance, Some(through.to_string()))
154}
155
156fn regular_axis_inner(
157 coords: &[f64],
158 cell: CellConvention,
159 tolerance: Tolerance,
160 through: Option<String>,
161) -> (Option<Positions>, Report) {
162 let max_abs = coords.iter().fold(0.0f64, |m, v| m.max(v.abs()));
163 let mut report = Report {
164 dims: vec![],
165 from: Rung::Rectilinear,
166 to: None,
167 frame: Frame::Index,
168 through,
169 max_deviation: f64::NAN,
170 max_deviation_steps: f64::NAN,
171 tolerance: f64::NAN,
172 accepted: false,
173 };
174 let Some((origin, step, max_dev)) = fit_line(coords, cell) else {
175 return (None, report);
176 };
177 let tol = tolerance.value(max_abs, step);
178 report.max_deviation = max_dev;
179 report.max_deviation_steps = max_dev / step.abs();
180 report.tolerance = tol;
181 if max_dev <= tol {
182 report.accepted = true;
183 report.to = Some(Rung::Regular);
184 (Some(Positions::Regular1 { origin, step }), report)
185 } else {
186 (None, report)
187 }
188}
189
190pub fn regular_grid_through(
197 x: &CoordArray,
198 y: &CoordArray,
199 f: impl Fn(f64, f64) -> Option<(f64, f64)>,
200 through: &str,
201 tolerance: Tolerance,
202) -> (Option<Affine>, Report) {
203 let mut report = Report {
204 dims: vec![],
205 from: Rung::Curvilinear,
206 to: None,
207 frame: Frame::Index,
208 through: Some(through.to_string()),
209 max_deviation: f64::NAN,
210 max_deviation_steps: f64::NAN,
211 tolerance: f64::NAN,
212 accepted: false,
213 };
214 if x.shape.len() != 2 || x.shape != y.shape || x.shape[0] < 2 || x.shape[1] < 2 {
215 return (None, report);
216 }
217 let (nr, nc) = (x.shape[0], x.shape[1]);
218 let mut px = Vec::with_capacity(nr * nc);
219 let mut py = Vec::with_capacity(nr * nc);
220 for k in 0..nr * nc {
221 match f(x.values[k], y.values[k]) {
222 Some((a, b)) if a.is_finite() && b.is_finite() => {
223 px.push(a);
224 py.push(b);
225 }
226 _ => return (None, report),
227 }
228 }
229 let at = |r: usize, c: usize| (px[r * nc + c], py[r * nc + c]);
230 let (x00, y00) = at(0, 0);
231 let (x01, y01) = at(0, nc - 1);
232 let (x10, y10) = at(nr - 1, 0);
233 let dxc = (x01 - x00) / (nc - 1) as f64;
235 let dyc = (y01 - y00) / (nc - 1) as f64;
236 let dxr = (x10 - x00) / (nr - 1) as f64;
237 let dyr = (y10 - y00) / (nr - 1) as f64;
238 let mut max_dev = 0.0f64;
239 for r in 0..nr {
240 for c in 0..nc {
241 let (a, b) = at(r, c);
242 let ex = x00 + c as f64 * dxc + r as f64 * dxr;
243 let ey = y00 + c as f64 * dyc + r as f64 * dyr;
244 max_dev = max_dev.max((a - ex).hypot(b - ey));
245 }
246 }
247 let step = dxc.hypot(dyc).min(dxr.hypot(dyr));
248 let max_abs = px.iter().chain(&py).fold(0.0f64, |m, v| m.max(v.abs()));
249 let tol = tolerance.value(max_abs, step);
250 report.max_deviation = max_dev;
251 report.max_deviation_steps = max_dev / step;
252 report.tolerance = tol;
253 if max_dev > tol {
254 return (None, report);
255 }
256 report.accepted = true;
257 report.to = Some(Rung::Regular);
258 let x0 = x00 - 0.5 * dxc - 0.5 * dxr;
260 let y0 = y00 - 0.5 * dyc - 0.5 * dyr;
261 (Some(Affine([x0, dxc, dxr, y0, dyc, dyr])), report)
262}
263
264pub fn normalise_domain(domain: &mut Domain) -> Vec<Report> {
268 normalise_domain_with(domain, Tolerance::Float32)
269}
270
271pub fn normalise_domain_with(domain: &mut Domain, tolerance: Tolerance) -> Vec<Report> {
273 let mut reports = Vec::new();
274 let candidates: Vec<usize> = domain
275 .mappings
276 .iter()
277 .enumerate()
278 .filter(|(_, m)| matches!(&m.positions, Positions::Rectilinear(v) if v.len() == 1))
279 .map(|(i, _)| i)
280 .collect();
281 for i in candidates {
282 let m = &domain.mappings[i];
283 let Positions::Rectilinear(axes) = &m.positions else {
284 unreachable!()
285 };
286 let (found, mut report) = regular_axis(&axes[0], m.cell, tolerance);
287 report.dims = m.dims.clone();
288 report.frame = m.frame.clone();
289 if let Some(p) = found {
290 let derived = Mapping::new(m.dims.clone(), p, m.frame.clone())
291 .with_cell(m.cell)
292 .with_provenance("normalise");
293 let j = domain.add_mapping(derived);
294 domain.set_primary(j);
295 }
296 reports.push(report);
297 }
298 reports
299}
300
301pub fn affine_from_axes(domain: &Domain, row: usize, col: usize) -> Option<(Affine, Frame)> {
304 let r = domain.primary_for(row)?;
305 let c = domain.primary_for(col)?;
306 if r.frame != c.frame {
307 return None;
308 }
309 match (&r.positions, &c.positions) {
310 (
311 Positions::Regular1 {
312 origin: y0,
313 step: dy,
314 },
315 Positions::Regular1 {
316 origin: x0,
317 step: dx,
318 },
319 ) => Some((Affine([*x0, *dx, 0.0, *y0, 0.0, *dy]), r.frame.clone())),
320 _ => None,
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn ulp_f32_at_180() {
330 assert_eq!(ulp_f32(180.0), 2f64.powi(-16));
332 }
333
334 #[test]
335 fn exact_axis_is_regular() {
336 let c: Vec<f64> = (0..10).map(|i| 0.5 + i as f64).collect();
337 let (p, r) = regular_axis(&c, CellConvention::Centre, Tolerance::Float64);
338 assert!(r.accepted);
339 match p.unwrap() {
340 Positions::Regular1 { origin, step } => {
341 assert_eq!(origin, 0.0);
342 assert_eq!(step, 1.0);
343 }
344 _ => panic!(),
345 }
346 }
347
348 #[test]
349 fn stretched_axis_is_not_regular() {
350 let c: Vec<f64> = (0..10).map(|i| (i as f64).powf(1.2)).collect();
351 let (p, r) = regular_axis(&c, CellConvention::Centre, Tolerance::Float32);
352 assert!(p.is_none());
353 assert!(!r.accepted);
354 assert!(r.max_deviation > r.tolerance);
355 }
356}