//! Layer 3: Two-kernel cross-check (Sprint 20). //! //! Verifies that StubKernel and PureRustKernel produce meshes that satisfy //! the same geometric contracts, and that OcctKernel (if enabled) matches //! the bounding-box dimensions of PureRustKernel within tolerance. //! //! Cross-check contract: //! 1. Both kernels succeed for the same valid IR. //! 2. Bounding boxes are within 5% of each other on non-trivial axes. //! 3. Triangle count of PureRustKernel ≥ StubKernel (richer geometry). //! //! Note: OcctKernel tests are gated behind `#[cfg(feature = "occt")]` //! because OCCT is not available in standard CI (see cimery/CLAUDE.md). use cimery_core::{ AbutmentType, BearingType, ColumnShape, CrossBeamSection, ExpansionJointType, MaterialGrade, PierType, SectionType, }; use cimery_ir::{ AbutmentIR, BearingIR, CapBeamIR, CrossBeamIR, DeckSlabIR, ExpansionJointIR, FeatureId, GirderIR, PierIR, PscISectionParams, SectionParams, WingWallIR, }; use cimery_kernel::{GeomKernel, Mesh, PureRustKernel, StubKernel}; #[cfg(feature = "occt")] use cimery_kernel::OcctKernel; // ─── Helpers ────────────────────────────────────────────────────────────────── /// Bounding-box extent on each axis. fn extents(mesh: &Mesh) -> [f32; 3] { let (mn, mx) = mesh.aabb(); [mx[0]-mn[0], mx[1]-mn[1], mx[2]-mn[2]] } /// Assert that two extents agree within `pct` percent on the axis with the /// largest extent (primary dimension). fn assert_primary_extent_close(a: &Mesh, b: &Mesh, pct: f32, label: &str) { let ea = extents(a); let eb = extents(b); // Pick the axis with the largest extent in `a` as the primary dimension. let axis = ea.iter().enumerate().max_by(|x, y| x.1.partial_cmp(y.1).unwrap()).map(|(i,_)| i).unwrap_or(0); let tol = ea[axis].max(eb[axis]) * pct / 100.0; assert!( (ea[axis] - eb[axis]).abs() <= tol, "{label}: primary extent mismatch on axis {axis}: {:.1} vs {:.1} (tol {:.1})", ea[axis], eb[axis], tol ); } // ─── IR factories ───────────────────────────────────────────────────────────── fn girder_40m() -> GirderIR { GirderIR { id: FeatureId::new(), station_start: 0.0, station_end: 40.0, offset_from_alignment: 0.0, section_type: SectionType::PscI, section: SectionParams::PscI(PscISectionParams::kds_standard()), count: 5, spacing: 2_500.0, material: MaterialGrade::C50, } } fn deck_slab_40m() -> DeckSlabIR { DeckSlabIR { id: FeatureId::new(), station_start: 0.0, station_end: 40.0, width_left: 5_500.0, width_right: 5_500.0, thickness: 220.0, haunch_depth: 100.0, cross_slope: 2.0, material: MaterialGrade::C40, } } fn bearing_standard() -> BearingIR { BearingIR { id: FeatureId::new(), station: 0.0, bearing_type: BearingType::Elastomeric, plan_length: 350.0, plan_width: 350.0, total_height: 90.0, capacity_vertical:2_500.0, } } fn pier_standard() -> PierIR { 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: 1_500.0, column_depth: 0.0, column_height: 8_000.0, cap_beam: CapBeamIR { length: 13_000.0, width: 1_200.0, depth: 1_400.0, cantilever_left: 1_000.0, cantilever_right:1_000.0, }, material: MaterialGrade::C40, } } fn abutment_standard() -> AbutmentIR { AbutmentIR { id: FeatureId::new(), station: 0.0, skew_angle: 0.0, abutment_type: AbutmentType::ReverseT, breast_wall_height: 5_000.0, breast_wall_thickness: 800.0, breast_wall_width: 12_000.0, footing_length: 4_000.0, footing_width: 13_000.0, footing_thickness: 1_000.0, wing_wall_left: WingWallIR { length: 4_000.0, height: 4_000.0, thickness: 500.0 }, wing_wall_right: WingWallIR { length: 4_000.0, height: 4_000.0, thickness: 500.0 }, material: MaterialGrade::C40, } } fn cross_beam_standard() -> CrossBeamIR { CrossBeamIR { id: FeatureId::new(), station: 10.0, section: CrossBeamSection::HSection, web_height: 1_260.0, web_thickness: 12.0, flange_width: 300.0, flange_thickness: 16.0, bay_count: 4, girder_spacing: 2_500.0, material: MaterialGrade::Ss400, } } fn expansion_joint_standard() -> ExpansionJointIR { ExpansionJointIR { id: FeatureId::new(), station: 0.0, joint_type: ExpansionJointType::RubberType, gap_width: 50.0, total_width: 11_000.0, depth: 100.0, movement_range: 30.0, } } // ─── StubKernel vs PureRustKernel ───────────────────────────────────────────── #[test] fn cross_check_girder_both_succeed() { let ir = girder_40m(); let stub = StubKernel.girder_mesh(&ir).expect("StubKernel::girder failed"); let prk = PureRustKernel.girder_mesh(&ir).expect("PureRustKernel::girder failed"); // PureRustKernel must produce more triangles than the stub box assert!(prk.triangle_count() >= stub.triangle_count(), "PureRustKernel should produce ≥ triangles: prk={} stub={}", prk.triangle_count(), stub.triangle_count()); // Both should span the full 40 m along Z assert_primary_extent_close(&stub, &prk, 5.0, "girder span Z"); } #[test] fn cross_check_deck_slab_both_succeed() { let ir = deck_slab_40m(); let stub = StubKernel.deck_slab_mesh(&ir).expect("StubKernel::deck_slab failed"); let prk = PureRustKernel.deck_slab_mesh(&ir).expect("PureRustKernel::deck_slab failed"); assert!(prk.vertex_count() > 0 && stub.vertex_count() > 0); assert_primary_extent_close(&stub, &prk, 5.0, "deck_slab span"); } #[test] fn cross_check_bearing_both_succeed() { let ir = bearing_standard(); let stub = StubKernel.bearing_mesh(&ir).expect("StubKernel::bearing failed"); let prk = PureRustKernel.bearing_mesh(&ir).expect("PureRustKernel::bearing failed"); assert!(stub.vertex_count() > 0 && prk.vertex_count() > 0); } #[test] fn cross_check_pier_both_succeed() { let ir = pier_standard(); let stub = StubKernel.pier_mesh(&ir).expect("StubKernel::pier failed"); let prk = PureRustKernel.pier_mesh(&ir).expect("PureRustKernel::pier failed"); assert!(stub.vertex_count() > 0 && prk.vertex_count() > 0); // Both must span at least the column height let (_, mx_s) = stub.aabb(); let (_, mx_p) = prk.aabb(); assert!(mx_s[1] > 0.0, "StubKernel pier: Y extent must be positive"); assert!(mx_p[1] > 0.0, "PureRustKernel pier: Y extent must be positive"); } #[test] fn cross_check_abutment_both_succeed() { let ir = abutment_standard(); let stub = StubKernel.abutment_mesh(&ir).expect("StubKernel::abutment failed"); let prk = PureRustKernel.abutment_mesh(&ir).expect("PureRustKernel::abutment failed"); assert!(stub.vertex_count() > 0 && prk.vertex_count() > 0); } #[test] fn cross_check_cross_beam_both_succeed() { let ir = cross_beam_standard(); let stub = StubKernel.cross_beam_mesh(&ir).expect("StubKernel::cross_beam failed"); let prk = PureRustKernel.cross_beam_mesh(&ir).expect("PureRustKernel::cross_beam failed"); assert!(stub.vertex_count() > 0 && prk.vertex_count() > 0); assert_primary_extent_close(&stub, &prk, 10.0, "cross_beam length"); } #[test] fn cross_check_expansion_joint_both_succeed() { let ir = expansion_joint_standard(); let stub = StubKernel.expansion_joint_mesh(&ir).expect("StubKernel::expansion_joint failed"); let prk = PureRustKernel.expansion_joint_mesh(&ir).expect("PureRustKernel::expansion_joint failed"); assert!(stub.vertex_count() > 0 && prk.vertex_count() > 0); } // ─── OcctKernel cross-check (requires --features occt) ──────────────────────── #[cfg(feature = "occt")] mod occt_cross_check { use super::*; #[test] fn occt_girder_matches_prk_span() { let ir = girder_40m(); let prk = PureRustKernel.girder_mesh(&ir).unwrap(); let occt = OcctKernel.girder_mesh(&ir).unwrap(); assert_primary_extent_close(&prk, &occt, 5.0, "OcctKernel girder span"); } #[test] fn occt_pier_column_height_correct() { let ir = pier_standard(); let prk = PureRustKernel.pier_mesh(&ir).unwrap(); let occt = OcctKernel.pier_mesh(&ir).unwrap(); // Y extent must include column height (8000 mm) in both let (_, mx_p) = prk.aabb(); let (_, mx_o) = occt.aabb(); assert!(mx_p[1] >= ir.column_height as f32 * 0.9, "PureRust pier Y must cover column_height, got {:.0}", mx_p[1]); assert!(mx_o[1] >= ir.column_height as f32 * 0.9, "Occt pier Y must cover column_height, got {:.0}", mx_o[1]); } #[test] fn occt_abutment_matches_prk_height() { let ir = abutment_standard(); let prk = PureRustKernel.abutment_mesh(&ir).unwrap(); let occt = OcctKernel.abutment_mesh(&ir).unwrap(); assert_primary_extent_close(&prk, &occt, 10.0, "OcctKernel abutment height"); } }