//! Layer 4: Property-based tests with proptest (Sprint 20). //! //! Checks that for any valid input within reasonable engineering ranges, //! the kernel always produces a valid, non-empty mesh. //! //! Properties verified: //! - vertex_count > 0 //! - triangle_count > 0 //! - all normals ≈ unit length //! - bounding box positive on ≥1 axis //! - span axis covers the requested span (within 1%) use cimery_core::{ BearingType, ColumnShape, CrossBeamSection, ExpansionJointType, MaterialGrade, PierType, SectionType, }; use cimery_ir::{ BearingIR, CapBeamIR, CrossBeamIR, DeckSlabIR, ExpansionJointIR, FeatureId, GirderIR, PierIR, PscISectionParams, SectionParams, }; use cimery_kernel::{GeomKernel, Mesh, PureRustKernel}; use proptest::prelude::*; // ─── Mesh validity helper ───────────────────────────────────────────────────── fn is_valid_mesh(mesh: &Mesh) -> bool { if mesh.vertex_count() == 0 { return false; } if mesh.triangle_count() == 0 { return false; } if mesh.indices.len() % 3 != 0 { return false; } if mesh.normals.len() != mesh.vertices.len() { return false; } let vcount = mesh.vertex_count() as u32; if mesh.indices.iter().any(|&i| i >= vcount) { return false; } for n in &mesh.normals { let len = (n[0]*n[0] + n[1]*n[1] + n[2]*n[2]).sqrt(); if (len - 1.0).abs() > 1e-3 { return false; } } let (mn, mx) = mesh.aabb(); let any_positive = (0..3).any(|i| mx[i] - mn[i] > 0.0); any_positive } fn span_ok(mesh: &Mesh, expected_mm: f32, axis: usize) -> bool { let (mn, mx) = mesh.aabb(); let actual = mx[axis] - mn[axis]; (actual - expected_mm).abs() < expected_mm * 0.02 } // ─── Girder proptest ────────────────────────────────────────────────────────── proptest! { #[test] fn proptest_girder_always_valid( span_m in 20.0_f64..=80.0, total_height in 1200.0_f64..=3000.0, top_flange_width in 400.0_f64..=800.0, bottom_flange_width in 400.0_f64..=900.0, ) { let section = PscISectionParams { total_height, top_flange_width, top_flange_thickness: 150.0, bottom_flange_width, bottom_flange_thickness: 180.0, web_thickness: 200.0, haunch: 50.0, }; let ir = GirderIR { id: FeatureId::new(), station_start: 0.0, station_end: span_m, offset_from_alignment: 0.0, section_type: SectionType::PscI, section: SectionParams::PscI(section), count: 1, spacing: 0.0, material: MaterialGrade::C50, }; let mesh = PureRustKernel.girder_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "girder mesh invalid for span={span_m}m h={total_height}mm"); prop_assert!(span_ok(&mesh, (span_m * 1000.0) as f32, 2), "girder Z span wrong: expected {}mm", span_m * 1000.0); } } // ─── Deck slab proptest ─────────────────────────────────────────────────────── proptest! { #[test] fn proptest_deck_slab_always_valid( span_m in 20.0_f64..=80.0, width_half in 3_000.0_f64..=8_000.0, thickness in 180.0_f64..=300.0, ) { let ir = DeckSlabIR { id: FeatureId::new(), station_start: 0.0, station_end: span_m, width_left: width_half, width_right: width_half, thickness, haunch_depth: 80.0, cross_slope: 2.0, material: MaterialGrade::C40, }; let mesh = PureRustKernel.deck_slab_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "deck_slab invalid for span={span_m}m w={width_half}mm t={thickness}mm"); } } // ─── Cross beam proptest ────────────────────────────────────────────────────── proptest! { #[test] fn proptest_cross_beam_always_valid( web_height in 800.0_f64..=2_000.0, web_thickness in 8.0_f64..=20.0, flange_width in 150.0_f64..=500.0, flange_thickness in 10.0_f64..=30.0, bay_count in 2_u32..=8, girder_spacing in 1_800.0_f64..=3_500.0, ) { let ir = CrossBeamIR { id: FeatureId::new(), station: 10.0, section: CrossBeamSection::HSection, web_height, web_thickness, flange_width, flange_thickness, bay_count, girder_spacing, material: MaterialGrade::Ss400, }; let mesh = PureRustKernel.cross_beam_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "cross_beam invalid: wh={web_height} wt={web_thickness} bays={bay_count} sp={girder_spacing}"); // Total length = bay_count * spacing, swept along X let expected_len = (bay_count as f64 * girder_spacing) as f32; prop_assert!(span_ok(&mesh, expected_len, 0), "cross_beam X extent wrong: expected {expected_len:.0}mm"); } } // ─── Expansion joint proptest ───────────────────────────────────────────────── proptest! { #[test] fn proptest_expansion_joint_always_valid( gap_width in 20.0_f64..=150.0, total_width in 5_000.0_f64..=15_000.0, depth in 50.0_f64..=200.0, movement_range in 10.0_f64..=100.0, ) { let ir = ExpansionJointIR { id: FeatureId::new(), station: 0.0, joint_type: ExpansionJointType::RubberType, gap_width, total_width, depth, movement_range, }; let mesh = PureRustKernel.expansion_joint_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "expansion_joint invalid: gap={gap_width} w={total_width} d={depth}"); } } // ─── Bearing proptest ───────────────────────────────────────────────────────── proptest! { #[test] fn proptest_bearing_always_valid( plan_length in 200.0_f64..=600.0, plan_width in 200.0_f64..=600.0, height in 50.0_f64..=200.0, ) { let ir = BearingIR { id: FeatureId::new(), station: 0.0, bearing_type: BearingType::Elastomeric, plan_length, plan_width, total_height: height, capacity_vertical:2_500.0, }; let mesh = PureRustKernel.bearing_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "bearing invalid: pl={plan_length} pw={plan_width} h={height}"); } } // ─── Pier proptest ──────────────────────────────────────────────────────────── proptest! { #[test] fn proptest_pier_always_valid( col_diameter in 800.0_f64..=2_500.0, col_height in 4_000.0_f64..=20_000.0, cap_length in 8_000.0_f64..=20_000.0, ) { let ir = PierIR { id: FeatureId::new(), station: 20.0, skew_angle: 0.0, pier_type: PierType::SingleColumn, column_shape: ColumnShape::Circular, column_count: 1, column_spacing: 0.0, column_diameter: col_diameter, column_depth: 0.0, column_height: col_height, cap_beam: CapBeamIR { length: cap_length, width: 1_200.0, depth: 1_400.0, cantilever_left: 1_000.0, cantilever_right:1_000.0, }, material: MaterialGrade::C40, }; let mesh = PureRustKernel.pier_mesh(&ir).unwrap(); prop_assert!(is_valid_mesh(&mesh), "pier invalid: d={col_diameter} h={col_height} cap_l={cap_length}"); } } // ─── Negative: zero span must fail ─────────────────────────────────────────── proptest! { #[test] fn proptest_zero_span_fails(dummy in 0.0_f64..1.0) { let _ = dummy; // ensure proptest runs at least once let ir = GirderIR { id: FeatureId::new(), station_start: 40.0, station_end: 40.0, // zero span offset_from_alignment: 0.0, section_type: SectionType::PscI, section: SectionParams::PscI(PscISectionParams::kds_standard()), count: 1, spacing: 0.0, material: MaterialGrade::C50, }; prop_assert!(PureRustKernel.girder_mesh(&ir).is_err(), "zero-span girder must return Err"); } }