1pub mod problem {
4 pub mod bc;
5 pub mod domains;
6 pub mod interfaces;
7 pub mod loads;
8 pub mod material_assignment;
9 pub mod materials;
10 pub mod model;
11 pub mod steps;
12 pub mod structure;
13}
14pub mod field;
15pub mod validate;
16
17pub use field::{AnalysisField, AnalysisFieldValues, DeviceFieldRef};
18pub use problem::bc::{BoundaryCondition, BoundaryConditionKind};
19pub use problem::domains::{
20 CfdDomain, CfdSolveFamily, CfdTimeProfilePoint, ElectroRegionConductivityScale,
21 ElectroThermalDomain, ElectroTimeProfilePoint, ElectromagneticDomain,
22 ThermoFieldInterpolationMode, ThermoFieldSource, ThermoMechanicalDomain,
23 ThermoRegionTemperatureDelta, ThermoTimeProfilePoint,
24};
25pub use problem::interfaces::{
26 AnalysisInterface, AnalysisInterfaceKind, ConjugateHeatTransferInterfaceModel,
27 ContactInterfaceModel, FluidStructureInterfaceModel,
28};
29pub use problem::loads::{LoadCase, LoadKind};
30pub use problem::material_assignment::{EvidenceConfidence, MaterialAssignment};
31pub use problem::materials::{
32 ConductivityFrequencyPoint, MaterialAcousticModel, MaterialElectricalModel,
33 MaterialMechanicalModel, MaterialModel, MaterialPlasticModel, MaterialThermalModel,
34};
35pub use problem::model::{AnalysisModel, AnalysisModelId, ReferenceFrame};
36pub use problem::steps::{AnalysisStep, AnalysisStepKind};
37pub use problem::structure::{
38 BeamElementModel, BeamSectionModel, ShellElementModel, ShellSectionModel, StructuralElement,
39 StructuralElementKind, StructuralModel, StructuralNode,
40};
41pub use validate::{validate_model, validate_model_against_geometry, AnalysisValidationError};
42
43#[cfg(test)]
44mod tests {
45 use runmat_geometry_core::UnitSystem;
46
47 use super::*;
48
49 fn valid_model() -> AnalysisModel {
50 AnalysisModel {
51 model_id: AnalysisModelId("analysis_model_1".to_string()),
52 geometry_id: "geo:model_1".to_string(),
53 geometry_revision: 1,
54 units: UnitSystem::Meter,
55 frame: ReferenceFrame::Global,
56 materials: vec![MaterialModel {
57 material_id: "mat_steel".to_string(),
58 name: "Steel".to_string(),
59 mechanical: MaterialMechanicalModel {
60 youngs_modulus_pa: 200e9,
61 poisson_ratio: 0.3,
62 density_kg_per_m3: 7850.0,
63 },
64 thermal: MaterialThermalModel::default(),
65 acoustic: None,
66 electrical: None,
67 plastic: None,
68 }],
69 material_assignments: Vec::new(),
70 structural: None,
71 thermo_mechanical: None,
72 electro_thermal: None,
73 electromagnetic: None,
74 cfd: None,
75 interfaces: Vec::new(),
76 boundary_conditions: vec![BoundaryCondition {
77 bc_id: "bc_fixed_root".to_string(),
78 region_id: "root".to_string(),
79 kind: BoundaryConditionKind::Fixed,
80 }],
81 loads: vec![LoadCase {
82 load_id: "load_tip".to_string(),
83 region_id: "tip".to_string(),
84 kind: LoadKind::Force {
85 fx: 0.0,
86 fy: -1000.0,
87 fz: 0.0,
88 },
89 }],
90 steps: vec![AnalysisStep {
91 step_id: "step_static".to_string(),
92 kind: AnalysisStepKind::Static,
93 }],
94 }
95 }
96
97 #[test]
98 fn missing_material_bc_load_validation_failures() {
99 let mut model = valid_model();
100 model.materials.clear();
101 assert_eq!(
102 validate_model(&model).expect_err("expected material validation failure"),
103 AnalysisValidationError::MissingMaterials
104 );
105
106 let mut model = valid_model();
107 model.boundary_conditions.clear();
108 assert_eq!(
109 validate_model(&model).expect_err("expected boundary condition validation failure"),
110 AnalysisValidationError::MissingBoundaryConditions
111 );
112
113 let mut model = valid_model();
114 model.loads.clear();
115 assert_eq!(
116 validate_model(&model).expect_err("expected load validation failure"),
117 AnalysisValidationError::MissingLoads
118 );
119 }
120
121 #[test]
122 fn invalid_moment_vectors_fail_validation() {
123 let mut model = valid_model();
124 model.loads[0].kind = LoadKind::Moment {
125 mx: f64::NAN,
126 my: 0.0,
127 mz: 1.0,
128 };
129 assert_eq!(
130 validate_model(&model).expect_err("expected nonfinite moment validation failure"),
131 AnalysisValidationError::InvalidMomentVector {
132 load_id: "load_tip".to_string()
133 }
134 );
135
136 let mut model = valid_model();
137 model.loads[0].kind = LoadKind::Moment {
138 mx: 0.0,
139 my: 0.0,
140 mz: 0.0,
141 };
142 assert_eq!(
143 validate_model(&model).expect_err("expected zero moment validation failure"),
144 AnalysisValidationError::ZeroMomentVector {
145 load_id: "load_tip".to_string()
146 }
147 );
148 }
149
150 #[test]
151 fn invalid_wrench_vectors_fail_validation() {
152 let mut model = valid_model();
153 model.loads[0].kind = LoadKind::Wrench {
154 fx: 0.0,
155 fy: 1.0,
156 fz: 0.0,
157 mx: f64::INFINITY,
158 my: 0.0,
159 mz: 0.0,
160 px: 0.0,
161 py: 0.0,
162 pz: 0.0,
163 };
164 assert_eq!(
165 validate_model(&model).expect_err("expected nonfinite wrench validation failure"),
166 AnalysisValidationError::InvalidWrench {
167 load_id: "load_tip".to_string()
168 }
169 );
170
171 let mut model = valid_model();
172 model.loads[0].kind = LoadKind::Wrench {
173 fx: 0.0,
174 fy: 0.0,
175 fz: 0.0,
176 mx: 0.0,
177 my: 0.0,
178 mz: 0.0,
179 px: 0.0,
180 py: 0.0,
181 pz: 0.0,
182 };
183 assert_eq!(
184 validate_model(&model).expect_err("expected zero wrench validation failure"),
185 AnalysisValidationError::ZeroWrench {
186 load_id: "load_tip".to_string()
187 }
188 );
189 }
190
191 #[test]
192 fn unit_frame_mismatch_rejection() {
193 let mut model = valid_model();
194 model.units = UnitSystem::Inch;
195 let err =
196 validate_model_against_geometry(&model, UnitSystem::Meter, &ReferenceFrame::Global)
197 .expect_err("expected unit mismatch");
198 assert!(matches!(
199 err,
200 AnalysisValidationError::UnitMismatch {
201 model: UnitSystem::Inch,
202 geometry: UnitSystem::Meter
203 }
204 ));
205
206 let mut model = valid_model();
207 model.frame = ReferenceFrame::Local("fixture_frame".to_string());
208 let err =
209 validate_model_against_geometry(&model, UnitSystem::Meter, &ReferenceFrame::Global)
210 .expect_err("expected frame mismatch");
211 assert!(matches!(
212 err,
213 AnalysisValidationError::FrameMismatch {
214 model: ReferenceFrame::Local(_),
215 geometry: ReferenceFrame::Global
216 }
217 ));
218 }
219
220 #[test]
221 fn valid_model_is_accepted() {
222 let model = valid_model();
223 validate_model(&model).expect("model should be valid");
224 validate_model_against_geometry(&model, UnitSystem::Meter, &ReferenceFrame::Global)
225 .expect("model should match geometry context");
226 }
227
228 #[test]
229 fn moment_load_kind_serializes_as_snake_case() {
230 let load = LoadCase {
231 load_id: "tip_moment".to_string(),
232 region_id: "tip".to_string(),
233 kind: LoadKind::Moment {
234 mx: 1.0,
235 my: 2.0,
236 mz: 3.0,
237 },
238 };
239
240 let json = serde_json::to_value(&load).expect("load should serialize");
241 assert_eq!(json["kind"]["moment"]["mx"], 1.0);
242 assert_eq!(json["kind"]["moment"]["my"], 2.0);
243 assert_eq!(json["kind"]["moment"]["mz"], 3.0);
244
245 let decoded: LoadCase = serde_json::from_value(json).expect("load should deserialize");
246 assert_eq!(decoded, load);
247 }
248
249 #[test]
250 fn wrench_load_kind_serializes_as_snake_case() {
251 let load = LoadCase {
252 load_id: "tip_wrench".to_string(),
253 region_id: "tip".to_string(),
254 kind: LoadKind::Wrench {
255 fx: 1.0,
256 fy: 2.0,
257 fz: 3.0,
258 mx: 4.0,
259 my: 5.0,
260 mz: 6.0,
261 px: 0.1,
262 py: 0.2,
263 pz: 0.3,
264 },
265 };
266
267 let json = serde_json::to_value(&load).expect("load should serialize");
268 assert_eq!(json["kind"]["wrench"]["fx"], 1.0);
269 assert_eq!(json["kind"]["wrench"]["mz"], 6.0);
270 assert_eq!(json["kind"]["wrench"]["pz"], 0.3);
271
272 let decoded: LoadCase = serde_json::from_value(json).expect("load should deserialize");
273 assert_eq!(decoded, load);
274 }
275
276 #[test]
277 fn prescribed_rotation_bc_serializes_as_snake_case() {
278 let bc = BoundaryCondition {
279 bc_id: "root_rotation".to_string(),
280 region_id: "root".to_string(),
281 kind: BoundaryConditionKind::PrescribedRotation {
282 rx: 0.0,
283 ry: 0.0,
284 rz: 0.125,
285 },
286 };
287
288 let json = serde_json::to_value(&bc).expect("bc should serialize");
289 assert_eq!(json["kind"]["prescribed_rotation"]["rx"], 0.0);
290 assert_eq!(json["kind"]["prescribed_rotation"]["ry"], 0.0);
291 assert_eq!(json["kind"]["prescribed_rotation"]["rz"], 0.125);
292
293 let decoded: BoundaryCondition =
294 serde_json::from_value(json).expect("bc should deserialize");
295 assert_eq!(decoded, bc);
296 }
297
298 #[test]
299 fn structural_beam_model_round_trips() {
300 let mut model = valid_model();
301 model.structural = Some(StructuralModel {
302 nodes: vec![
303 StructuralNode {
304 node_id: 1,
305 coordinates_m: [0.0, 0.0, 0.0],
306 },
307 StructuralNode {
308 node_id: 2,
309 coordinates_m: [1.0, 0.0, 0.0],
310 },
311 ],
312 elements: vec![StructuralElement {
313 element_id: "beam_1".to_string(),
314 region_id: "beam_span".to_string(),
315 kind: StructuralElementKind::Beam(BeamElementModel {
316 node_ids: [1, 2],
317 section_id: "section_1".to_string(),
318 reference_axis: [0.0, 0.0, 1.0],
319 }),
320 }],
321 beam_sections: vec![BeamSectionModel {
322 section_id: "section_1".to_string(),
323 area_m2: 2.0e-4,
324 iy_m4: 1.6e-9,
325 iz_m4: 6.4e-9,
326 torsion_j_m4: 2.4e-9,
327 outer_fiber_y_m: 0.01,
328 outer_fiber_z_m: 0.005,
329 torsion_outer_radius_m: 0.011_180_339_887_498_949,
330 }],
331 shell_sections: Vec::new(),
332 });
333
334 let json = serde_json::to_value(&model).expect("model should serialize");
335 assert_eq!(
336 json["structural"]["elements"][0]["kind"]["beam"]["node_ids"][1],
337 2
338 );
339
340 let decoded: AnalysisModel =
341 serde_json::from_value(json).expect("model should deserialize");
342 assert_eq!(decoded, model);
343 }
344
345 #[test]
346 fn structural_shell_model_round_trips() {
347 let mut model = valid_model();
348 model.structural = Some(StructuralModel {
349 nodes: vec![
350 StructuralNode {
351 node_id: 1,
352 coordinates_m: [0.0, 0.0, 0.0],
353 },
354 StructuralNode {
355 node_id: 2,
356 coordinates_m: [1.0, 0.0, 0.0],
357 },
358 StructuralNode {
359 node_id: 3,
360 coordinates_m: [0.0, 1.0, 0.0],
361 },
362 ],
363 elements: vec![StructuralElement {
364 element_id: "shell_1".to_string(),
365 region_id: "shell_panel".to_string(),
366 kind: StructuralElementKind::Shell(ShellElementModel {
367 node_ids: [1, 2, 3],
368 section_id: "panel_2mm".to_string(),
369 reference_axis: [1.0, 0.0, 0.0],
370 }),
371 }],
372 beam_sections: Vec::new(),
373 shell_sections: vec![ShellSectionModel {
374 section_id: "panel_2mm".to_string(),
375 thickness_m: 0.002,
376 shear_correction: 5.0 / 6.0,
377 drilling_stiffness_scale: 1.0e-4,
378 }],
379 });
380
381 let json = serde_json::to_value(&model).expect("model should serialize");
382 assert_eq!(
383 json["structural"]["elements"][0]["kind"]["shell"]["node_ids"][2],
384 3
385 );
386
387 let decoded: AnalysisModel =
388 serde_json::from_value(json).expect("model should deserialize");
389 assert_eq!(decoded, model);
390 }
391
392 #[test]
393 fn electrical_model_defaults_frequency_response() {
394 let electrical = MaterialElectricalModel::default();
395 assert!(electrical.conductivity_frequency_response.is_empty());
396 }
397}