Files
ParaWiki/cimery/crates/kernel/tests/layer4_proptest.rs
minsung 1f9ca3a00f Sprint 14~22 — egui 리본 UI + OcctKernel B-rep + 가로보/신축이음 + 선형 좌표 + USD 익스포트 + WASM + CI/CD + 테스트 4층
Sprint 14: egui TopBottomPanel 리본 + CollapsingHeader SidePanel (상부구조·추가부재·선형·프로젝트)
Sprint 15: IncrementalDb 전 Feature 타입 확장 (girder→7종), dirty-tracking 20 unit tests
Sprint 16: Gitea + GitHub Actions CI/CD (check/test/clippy/fmt + 멀티플랫폼 릴리스)
Sprint 17: AlignmentTransform + AlignmentScene — 선형 국소 프레임 → 세계 좌표 변환
Sprint 18: OcctKernel 교각(16각형 기둥+코핑) + 교대(흉벽+푸팅+날개벽) B-rep
Sprint 19: CrossBeamIR + ExpansionJointIR — IR/DSL/kernel/scene 전 계층, sweep_profile_flat_x
Sprint 20: 테스트 4층 — Layer1 insta 스냅샷(7종), Layer2 기하 불변량(19), Layer3 두-커널(7), Layer4 proptest(7) — 61 tests pass
Sprint 21: cimery-usd PureRustKernel 실제 기하 변환 + BridgeExporter 증분 캐시
Sprint 22: viewer wasm feature + wasm-bindgen/web-sys + GitHub Actions Cloudflare Pages 배포

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 08:18:06 +09:00

249 lines
9.5 KiB
Rust

//! 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");
}
}