Skip to main content

sloping_components/
sloping_components.rs

1//! Compare Rust reconstruction with a private, reflected native component receipt.
2//! Run with: cargo run --release --example sloping_components -- receipt.json 4
3//! This checks component equality, not complete raster qualification.
4
5use 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}