axiolid_pointcloud_reconstruction_contract/
conformance.rs1use core::fmt;
27
28use axiolid_contracts::{Determinism, ExecutionOptions};
29use axiolid_core::{Point3, Scalar, Tolerance};
30use axiolid_pointcloud::PointCloud;
31
32use crate::{PointcloudReconstruction, Reconstruction, ReconstructionRequest};
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum Outcome {
37 Passed,
39 Failed(String),
41 Skipped(String),
46}
47
48impl Outcome {
49 pub fn is_failure(&self) -> bool {
51 matches!(self, Self::Failed(_))
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct Check {
58 pub name: &'static str,
60 pub outcome: Outcome,
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub struct ConformanceReport {
67 pub checks: Vec<Check>,
69}
70
71impl ConformanceReport {
72 pub fn is_conformant(&self) -> bool {
74 !self.checks.iter().any(|check| check.outcome.is_failure())
75 }
76
77 pub fn exercised(&self) -> usize {
79 self.checks
80 .iter()
81 .filter(|check| matches!(check.outcome, Outcome::Passed))
82 .count()
83 }
84
85 pub fn skipped(&self) -> usize {
87 self.checks
88 .iter()
89 .filter(|check| matches!(check.outcome, Outcome::Skipped(_)))
90 .count()
91 }
92}
93
94impl fmt::Display for ConformanceReport {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 writeln!(
97 f,
98 "conformance: {} exercised, {} skipped, {} failed",
99 self.exercised(),
100 self.skipped(),
101 self.checks
102 .iter()
103 .filter(|check| check.outcome.is_failure())
104 .count()
105 )?;
106 for check in &self.checks {
107 match &check.outcome {
108 Outcome::Passed => writeln!(f, " PASS {}", check.name)?,
109 Outcome::Skipped(reason) => writeln!(f, " SKIP {} ({reason})", check.name)?,
110 Outcome::Failed(detail) => writeln!(f, " FAIL {} — {detail}", check.name)?,
111 }
112 }
113 Ok(())
114 }
115}
116
117fn options() -> ExecutionOptions {
118 ExecutionOptions::new(Tolerance::METRE)
119}
120
121fn sphere_samples(count: usize) -> PointCloud {
123 let mut points = Vec::with_capacity(count);
124 let mut normals = Vec::with_capacity(count);
125 let golden = core::f64::consts::PI * (3.0 - 5.0_f64.sqrt());
128 for i in 0..count {
129 let y = 1.0 - (i as Scalar / (count.max(2) - 1) as Scalar) * 2.0;
130 let radius = (1.0 - y * y).max(0.0).sqrt();
131 let theta = golden * i as Scalar;
132 let direction = Point3::new(theta.cos() * radius, y, theta.sin() * radius);
133 points.push(direction);
134 normals.push(direction);
135 }
136 PointCloud::new(points)
137 .expect("sphere samples are finite")
138 .with_normals(normals)
139 .expect("normals match")
140}
141
142pub fn run(provider: &impl PointcloudReconstruction) -> ConformanceReport {
144 let checks = vec![
145 Check {
146 name: "too few points is refused by name, not by empty mesh",
147 outcome: check_too_few(provider),
148 },
149 Check {
150 name: "degenerate extent is refused by name",
151 outcome: check_degenerate(provider),
152 },
153 Check {
154 name: "evidence counts agree with the mesh returned",
155 outcome: check_evidence_consistent(provider),
156 },
157 Check {
158 name: "input point count is reported faithfully",
159 outcome: check_input_count(provider),
160 },
161 Check {
162 name: "a declared-deterministic provider repeats itself",
163 outcome: check_determinism(provider),
164 },
165 Check {
166 name: "minimum_points agrees with actual refusal behaviour",
167 outcome: check_minimum_agrees(provider),
168 },
169 ];
170
171 ConformanceReport { checks }
172}
173
174fn check_too_few(provider: &impl PointcloudReconstruction) -> Outcome {
175 let cloud = PointCloud::new(vec![Point3::ZERO]).expect("one point");
176 match provider.reconstruct(&cloud, &ReconstructionRequest::default(), &options()) {
177 Ok(Reconstruction::Refused(_)) => Outcome::Passed,
178 Ok(Reconstruction::Surface(outcome)) => Outcome::Failed(format!(
179 "a single point produced a surface with {} triangles",
180 outcome.mesh.indices.len() / 3
181 )),
182 Err(error) => Outcome::Failed(format!("errored instead of refusing: {error}")),
183 }
184}
185
186fn check_degenerate(provider: &impl PointcloudReconstruction) -> Outcome {
187 let points: Vec<Point3> = (0..64)
190 .map(|i| Point3::new(i as Scalar * 0.1, 0.0, 0.0))
191 .collect();
192 let cloud = PointCloud::new(points).expect("collinear points are finite");
193 match provider.reconstruct(&cloud, &ReconstructionRequest::default(), &options()) {
194 Ok(Reconstruction::Refused(_)) => Outcome::Passed,
195 Ok(Reconstruction::Surface(outcome)) => Outcome::Failed(format!(
196 "collinear points produced a surface with {} triangles",
197 outcome.mesh.indices.len() / 3
198 )),
199 Err(error) => Outcome::Failed(format!("errored instead of refusing: {error}")),
200 }
201}
202
203fn check_evidence_consistent(provider: &impl PointcloudReconstruction) -> Outcome {
204 let cloud = sphere_samples(512);
205 match provider.reconstruct(&cloud, &ReconstructionRequest::default(), &options()) {
206 Ok(Reconstruction::Refused(reason)) => Outcome::Skipped(reason.to_string()),
207 Err(error) => Outcome::Failed(format!("errored on a dense sphere: {error}")),
208 Ok(Reconstruction::Surface(outcome)) => {
209 let actual = outcome.mesh.indices.len() / 3;
210 if outcome.evidence.output_triangles != actual {
211 return Outcome::Failed(format!(
212 "evidence claims {} triangles, mesh has {actual}",
213 outcome.evidence.output_triangles
214 ));
215 }
216 if outcome.evidence.used_points > outcome.evidence.input_points {
217 return Outcome::Failed(format!(
218 "used {} of {} points",
219 outcome.evidence.used_points, outcome.evidence.input_points
220 ));
221 }
222 if actual > 0 && !outcome.evidence.achieved_edge_length.is_finite() {
223 return Outcome::Failed(
224 "a surface was produced without reporting its edge length".to_owned(),
225 );
226 }
227 if outcome.evidence.interpolated_triangles > actual {
228 return Outcome::Failed(format!(
229 "claims {} interpolated triangles of {actual} total",
230 outcome.evidence.interpolated_triangles
231 ));
232 }
233 Outcome::Passed
234 }
235 }
236}
237
238fn check_input_count(provider: &impl PointcloudReconstruction) -> Outcome {
239 let cloud = sphere_samples(300);
240 match provider.reconstruct(&cloud, &ReconstructionRequest::default(), &options()) {
241 Ok(Reconstruction::Refused(reason)) => Outcome::Skipped(reason.to_string()),
242 Err(error) => Outcome::Failed(format!("errored: {error}")),
243 Ok(outcome) => {
244 let outcome = outcome.outcome().expect("surface");
245 if outcome.evidence.input_points == cloud.len() {
246 Outcome::Passed
247 } else {
248 Outcome::Failed(format!(
249 "reported {} input points for a cloud of {}",
250 outcome.evidence.input_points,
251 cloud.len()
252 ))
253 }
254 }
255 }
256}
257
258fn check_determinism(provider: &impl PointcloudReconstruction) -> Outcome {
259 if provider.determinism() == Determinism::BestEffort {
260 return Outcome::Skipped("provider only claims best-effort determinism".to_owned());
261 }
262 let cloud = sphere_samples(400);
263 let request = ReconstructionRequest::default();
264 let first = provider.reconstruct(&cloud, &request, &options());
265 let second = provider.reconstruct(&cloud, &request, &options());
266 match (first, second) {
267 (Ok(Reconstruction::Surface(a)), Ok(Reconstruction::Surface(b))) => {
268 if a.mesh.positions == b.mesh.positions && a.mesh.indices == b.mesh.indices {
269 Outcome::Passed
270 } else {
271 Outcome::Failed(
272 "a provider claiming determinism produced two different meshes".to_owned(),
273 )
274 }
275 }
276 (Ok(Reconstruction::Refused(reason)), _) => Outcome::Skipped(reason.to_string()),
277 (Err(error), _) | (_, Err(error)) => Outcome::Failed(format!("errored: {error}")),
278 _ => Outcome::Failed("one call produced a surface and the other refused".to_owned()),
279 }
280}
281
282fn check_minimum_agrees(provider: &impl PointcloudReconstruction) -> Outcome {
283 let minimum = provider.minimum_points();
284 if minimum == 0 {
285 return Outcome::Failed("minimum_points of 0 cannot be honest".to_owned());
286 }
287 let points: Vec<Point3> = (0..minimum.saturating_sub(1))
291 .map(|i| Point3::new(i as Scalar, (i % 3) as Scalar, (i % 5) as Scalar))
292 .collect();
293 if points.is_empty() {
294 return Outcome::Skipped("minimum is 1; nothing below it to test".to_owned());
295 }
296 let cloud = PointCloud::new(points).expect("finite");
297 match provider.reconstruct(&cloud, &ReconstructionRequest::default(), &options()) {
298 Ok(Reconstruction::Refused(_)) => Outcome::Passed,
299 Ok(Reconstruction::Surface(_)) => Outcome::Failed(format!(
300 "declared minimum_points={minimum} but reconstructed from {}",
301 minimum - 1
302 )),
303 Err(error) => Outcome::Failed(format!("errored instead of refusing: {error}")),
304 }
305}