1use ras_commander_fast_map::sloping::{
6 Cell, Corner, Face, Mesh, PolygonStencil, WeightScratch, paint_corner_values,
7};
8use serde::Deserialize;
9use std::{error::Error, fs::File, io::BufReader, time::Instant};
10
11#[derive(Deserialize)]
12struct Receipt {
13 cells: Vec<CellRecord>,
14 faces: Vec<FaceRecord>,
15 vertices: Vec<VertexRecord>,
16 min_ws_plot_tolerance: f32,
17 #[serde(default)]
18 pixel_samples: Vec<PixelSample>,
19}
20#[derive(Deserialize)]
21struct CellRecord {
22 minimum_elevation: f32,
23 wse: f32,
24 #[serde(rename = "virtual")]
25 virtual_cell: bool,
26 faces: Vec<usize>,
27 corner_values: Vec<f32>,
28}
29#[derive(Deserialize)]
30struct FaceRecord {
31 cells: [usize; 2],
32 vertices: [usize; 2],
33 minimum_elevation: f32,
34 perimeter: bool,
35 application: [f64; 2],
36 values: [f32; 2],
37 connection: String,
38 prev_a: usize,
39 next_a: usize,
40}
41#[derive(Deserialize)]
42struct VertexRecord {
43 point: [f64; 2],
44 faces: Vec<usize>,
45 ccw: Vec<usize>,
46 cw: Vec<usize>,
47 values: Vec<f32>,
48}
49#[derive(Deserialize)]
50struct PixelSample {
51 cell: usize,
52 point: [f64; 2],
53 weights: Vec<f32>,
54 wse: f32,
55}
56
57fn main() -> Result<(), Box<dyn Error>> {
58 let args: Vec<_> = std::env::args().collect();
59 let path = args
60 .get(1)
61 .ok_or("Expected a native component JSON receipt")?;
62 let threads: usize = args.get(2).map_or(Ok(1), |v| v.parse())?;
63 if !(1..=64).contains(&threads) {
64 return Err("threads must be 1..64".into());
65 }
66 let input: Receipt = serde_json::from_reader(BufReader::new(File::open(path)?))?;
67 let mesh = Mesh {
68 cells: input
69 .cells
70 .iter()
71 .map(|c| Cell {
72 minimum: c.minimum_elevation,
73 virtual_cell: c.virtual_cell,
74 })
75 .collect(),
76 faces: input
77 .faces
78 .iter()
79 .map(|f| Face {
80 cells: f.cells,
81 vertices: f.vertices,
82 minimum: f.minimum_elevation,
83 perimeter: f.perimeter,
84 application: f.application,
85 })
86 .collect(),
87 corners: input
88 .vertices
89 .iter()
90 .map(|v| Corner {
91 point: v.point,
92 faces: v.faces.clone(),
93 ccw: v.ccw.clone(),
94 cw: v.cw.clone(),
95 })
96 .collect(),
97 };
98 let pool = rayon::ThreadPoolBuilder::new()
99 .num_threads(threads)
100 .build()?;
101 let values: Vec<_> = input.cells.iter().map(|c| c.wse).collect();
102 let started = Instant::now();
103 let output = mesh.reconstruct(&values, input.min_ws_plot_tolerance, &pool)?;
104 let seconds = started.elapsed().as_secs_f64();
105 let mut face_mismatches = 0;
106 let mut connection_mismatches = 0;
107 let mut corner_mismatches = 0;
108 let mut local_mismatches = 0;
109 let mut weight_mismatches = 0;
110 let mut paint_mismatches = 0;
111 let mut first = Vec::new();
112 for (i, (actual, expected)) in output.faces.iter().zip(&input.faces).enumerate() {
113 for side in 0..2 {
114 if actual.values[side].to_bits() != expected.values[side].to_bits() {
115 face_mismatches += 1;
116 if first.len() < 10 {
117 first.push(format!(
118 "face {i} side {side}: {} != {}",
119 actual.values[side], expected.values[side]
120 ));
121 }
122 }
123 }
124 if format!("{:?}", actual.connection) != expected.connection {
125 connection_mismatches += 1;
126 }
127 }
128 for (i, (actual, expected)) in output.corners.iter().zip(&input.vertices).enumerate() {
129 if actual.len() != expected.values.len() {
130 return Err("native corner dimension mismatch".into());
131 }
132 for (j, (&a, &b)) in actual.iter().zip(&expected.values).enumerate() {
133 if a.to_bits() != b.to_bits() {
134 corner_mismatches += 1;
135 if first.len() < 10 {
136 first.push(format!("corner {i} slot {j}: {a} != {b}"));
137 }
138 }
139 }
140 }
141 for (i, cell) in input.cells.iter().enumerate() {
142 for (slot, &face_id) in cell.faces.iter().enumerate() {
143 let face = &input.faces[face_id];
144 let vertex = if face.cells[0] == i {
145 face.prev_a
146 } else {
147 face.next_a
148 };
149 let incident = input.vertices[vertex]
150 .faces
151 .iter()
152 .position(|&f| f == face_id)
153 .ok_or("local face not incident to vertex")?;
154 if output.corners[vertex][incident].to_bits() != cell.corner_values[slot].to_bits() {
155 local_mismatches += 1;
156 }
157 }
158 }
159 for sample in &input.pixel_samples {
160 let cell = &input.cells[sample.cell];
161 let vertices: Vec<_> = cell
162 .faces
163 .iter()
164 .map(|&f| {
165 let face = &input.faces[f];
166 if face.cells[0] == sample.cell {
167 face.prev_a
168 } else {
169 face.next_a
170 }
171 })
172 .collect();
173 let points = vertices.iter().map(|&v| input.vertices[v].point).collect();
174 let stencil = PolygonStencil::new(points)?;
175 let mut scratch = WeightScratch::new(vertices.len());
176 let weights = stencil.weights(sample.point, &mut scratch)?;
177 for (&a, &b) in weights.iter().zip(&sample.weights) {
178 if a.to_bits() != b.to_bits() {
179 weight_mismatches += 1;
180 }
181 }
182 let local: Vec<_> = vertices
183 .iter()
184 .zip(&cell.faces)
185 .map(|(&vertex, &face)| {
186 let incident = input.vertices[vertex]
187 .faces
188 .iter()
189 .position(|&f| f == face)
190 .unwrap();
191 output.corners[vertex][incident]
192 })
193 .collect();
194 let value = paint_corner_values(weights, &local)?;
195 if value.to_bits() != sample.wse.to_bits() {
196 paint_mismatches += 1;
197 if first.len() < 10 {
198 first.push(format!(
199 "pixel cell {}: {value} != {}",
200 sample.cell, sample.wse
201 ));
202 }
203 }
204 }
205 println!(
206 "{}",
207 serde_json::json!({
208 "threads": threads, "cells": input.cells.len(), "faces": input.faces.len(),
209 "corners": input.vertices.len(), "seconds": seconds,
210 "face_value_mismatches": face_mismatches, "connection_mismatches": connection_mismatches,
211 "corner_value_mismatches": corner_mismatches, "cell_corner_mismatches": local_mismatches,
212 "pixel_samples": input.pixel_samples.len(), "weight_mismatches": weight_mismatches,
213 "paint_mismatches": paint_mismatches,
214 "first_mismatches": first,
215 "scope": "native Max-profile face and corner reconstruction, not raster qualification",
216 })
217 );
218 if face_mismatches
219 + connection_mismatches
220 + corner_mismatches
221 + local_mismatches
222 + weight_mismatches
223 + paint_mismatches
224 != 0
225 {
226 return Err("sloping component parity failed".into());
227 }
228 Ok(())
229}