1use runmat_analysis_core::{AnalysisField, AnalysisFieldValues, AnalysisModel};
2use runmat_analysis_fea::diagnostics::FeaDiagnostic;
3use runmat_analysis_fea::{ComputeBackend, FeaProgressEvent, FeaRunResult};
4use runmat_geometry_core::GeometryAsset;
5use runmat_meshing::RegionMeshMapping;
6use runmat_meshing_core::VolumeMeshingOptions;
7use runmat_meshing_evidence::{MeshAuthoringNestedTetrahedronShellSummary, MeshAuthoringSummary};
8use serde::{Deserialize, Serialize};
9use serde_json::Value as JsonValue;
10
11fn default_prep_coordinate_span_m() -> f64 {
12 1.0
13}
14
15fn default_prep_coordinate_secondary_span_m() -> f64 {
16 0.0
17}
18
19fn default_prep_coordinate_active_dimension_count() -> usize {
20 1
21}
22
23fn default_prep_coordinate_characteristic_length_m() -> f64 {
24 1.0
25}
26
27fn default_zero_usize() -> usize {
28 0
29}
30
31fn default_zero_f64() -> f64 {
32 0.0
33}
34
35fn default_reference_element_coordinates_m() -> [[f64; 3]; 3] {
36 [[0.0; 3]; 3]
37}
38
39fn default_element_topology_sample_edge_nodes() -> [[u32; 2]; 8] {
40 [[0; 2]; 8]
41}
42
43fn default_element_topology_sample_node_coordinates_m() -> [[f64; 3]; 8] {
44 [[0.0; 3]; 8]
45}
46
47fn default_element_topology_sample_element_edges() -> [[u32; 3]; 4] {
48 [[0; 3]; 4]
49}
50
51fn default_element_topology_sample_element_orientations() -> [[i8; 3]; 4] {
52 [[0; 3]; 4]
53}
54
55fn default_element_topology_sample_element_areas_m2() -> [f64; 4] {
56 [0.0; 4]
57}
58
59fn default_element_topology_node_coordinates_m() -> Vec<[f64; 3]> {
60 Vec::new()
61}
62
63fn default_element_topology_edge_nodes() -> Vec<[u32; 2]> {
64 Vec::new()
65}
66
67fn default_element_topology_element_edges() -> Vec<[u32; 3]> {
68 Vec::new()
69}
70
71fn default_element_topology_element_orientations() -> Vec<[i8; 3]> {
72 Vec::new()
73}
74
75fn default_element_topology_element_areas_m2() -> Vec<f64> {
76 Vec::new()
77}
78
79#[derive(Debug, Clone, PartialEq)]
80pub struct AnalysisValidateResult {
81 pub valid: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct AnalysisCreateModelIntentSpec {
86 pub model_id: String,
87 pub profile: AnalysisCreateModelProfile,
88 #[serde(default)]
89 pub prep_context: Option<AnalysisCreateModelPrepContext>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct AnalysisCreateModelPrepContext {
94 pub source_geometry_id: String,
95 pub source_geometry_revision: u32,
96 pub region_mappings: Vec<RegionMeshMapping>,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct AnalysisRunPrepContext {
101 pub prepared_mesh_count: usize,
102 pub prepared_node_count: usize,
103 pub prepared_element_count: usize,
104 pub mapped_region_count: usize,
105 pub min_scaled_jacobian: f64,
106 pub mean_aspect_ratio: f64,
107 pub inverted_element_count: usize,
108 pub mapped_load_count: usize,
109 pub mapped_bc_count: usize,
110 pub layout_seed: u64,
111 pub topology_dof_multiplier: f64,
112 pub topology_bandwidth_estimate: u32,
113 pub mapped_region_participation_ratio: f64,
114 pub topology_surface_patch_ratio: f64,
115 pub topology_volume_core_ratio: f64,
116 pub topology_mixed_family_ratio: f64,
117 pub topology_region_span_mean: f64,
118 pub topology_region_block_count: usize,
119 pub topology_region_mesh_mean: f64,
120 pub topology_region_mesh_variance: f64,
121 pub topology_triangle_family_ratio: f64,
122 pub topology_quad_family_ratio: f64,
123 pub topology_tetrahedron_family_ratio: f64,
124 pub topology_hex_family_ratio: f64,
125 #[serde(default = "default_prep_coordinate_span_m")]
126 pub coordinate_span_x_m: f64,
127 #[serde(default = "default_prep_coordinate_secondary_span_m")]
128 pub coordinate_span_y_m: f64,
129 #[serde(default = "default_prep_coordinate_secondary_span_m")]
130 pub coordinate_span_z_m: f64,
131 #[serde(default = "default_prep_coordinate_active_dimension_count")]
132 pub coordinate_active_dimension_count: usize,
133 #[serde(default = "default_prep_coordinate_characteristic_length_m")]
134 pub coordinate_characteristic_length_m: f64,
135 #[serde(default = "default_zero_usize")]
136 pub element_geometry_node_count: usize,
137 #[serde(default = "default_zero_usize")]
138 pub element_geometry_edge_count: usize,
139 #[serde(default = "default_zero_f64")]
140 pub mean_element_edge_length_m: f64,
141 #[serde(default = "default_zero_f64")]
142 pub mean_element_area_m2: f64,
143 #[serde(default = "default_zero_f64")]
144 pub element_geometry_coverage_ratio: f64,
145 #[serde(default = "default_reference_element_coordinates_m")]
146 pub reference_element_coordinates_m: [[f64; 3]; 3],
147 #[serde(default = "default_zero_f64")]
148 pub reference_element_area_m2: f64,
149 #[serde(default = "default_zero_usize")]
150 pub control_volume_cell_count: usize,
151 #[serde(default = "default_zero_usize")]
152 pub control_volume_face_count: usize,
153 #[serde(default = "default_zero_usize")]
154 pub control_volume_internal_face_count: usize,
155 #[serde(default = "default_zero_usize")]
156 pub control_volume_boundary_face_count: usize,
157 #[serde(default = "default_zero_f64")]
158 pub control_volume_connectivity_coverage_ratio: f64,
159 #[serde(default = "default_zero_usize")]
160 pub element_topology_sample_element_count: usize,
161 #[serde(default = "default_zero_usize")]
162 pub element_topology_sample_edge_count: usize,
163 #[serde(default = "default_element_topology_sample_edge_nodes")]
164 pub element_topology_sample_edge_nodes: [[u32; 2]; 8],
165 #[serde(default = "default_element_topology_sample_node_coordinates_m")]
166 pub element_topology_sample_node_coordinates_m: [[f64; 3]; 8],
167 #[serde(default = "default_element_topology_sample_element_edges")]
168 pub element_topology_sample_element_edges: [[u32; 3]; 4],
169 #[serde(default = "default_element_topology_sample_element_orientations")]
170 pub element_topology_sample_element_orientations: [[i8; 3]; 4],
171 #[serde(default = "default_element_topology_sample_element_areas_m2")]
172 pub element_topology_sample_element_areas_m2: [f64; 4],
173 #[serde(default = "default_element_topology_node_coordinates_m")]
174 pub element_topology_node_coordinates_m: Vec<[f64; 3]>,
175 #[serde(default = "default_element_topology_edge_nodes")]
176 pub element_topology_edge_nodes: Vec<[u32; 2]>,
177 #[serde(default = "default_element_topology_element_edges")]
178 pub element_topology_element_edges: Vec<[u32; 3]>,
179 #[serde(default = "default_element_topology_element_orientations")]
180 pub element_topology_element_orientations: Vec<[i8; 3]>,
181 #[serde(default = "default_element_topology_element_areas_m2")]
182 pub element_topology_element_areas_m2: Vec<f64>,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum AnalysisCreateModelProfile {
188 LinearStaticStructural,
189 ThermoMechanicalCoupled,
190 ElectroThermalCoupled,
191 ThermalStandalone,
192 ModalStructural,
193 AcousticHarmonic,
194 TransientStructural,
195 NonlinearStructural,
196 ElectromagneticStatic,
197 CfdSteadyState,
198 CfdTransient,
199 ChtCoupled,
200 FsiCoupled,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct AnalysisPhysicsProfileCatalogEntry {
205 pub profile: AnalysisCreateModelProfile,
206 pub label: &'static str,
207 pub family: &'static str,
208 pub target: &'static str,
209 pub value: &'static str,
210 pub default_outputs: &'static [AnalysisPhysicsProfileDefaultOutput],
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct AnalysisPhysicsProfileDefaultOutput {
215 pub field: &'static str,
216 pub location: &'static str,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct AnalysisRuntimePhysicsProfileCatalogEntry {
222 pub profile: AnalysisCreateModelProfile,
223 pub label: String,
224 pub family: String,
225 pub target: String,
226 pub value: String,
227 pub default_outputs: Vec<AnalysisRuntimePhysicsProfileDefaultOutput>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct AnalysisRuntimePhysicsProfileDefaultOutput {
233 pub field: String,
234 pub location: String,
235}
236
237pub const ANALYSIS_PHYSICS_PROFILE_CATALOG: &[AnalysisPhysicsProfileCatalogEntry] = &[
238 AnalysisPhysicsProfileCatalogEntry {
239 profile: AnalysisCreateModelProfile::LinearStaticStructural,
240 label: "Linear static structural",
241 family: "structural",
242 target: "solid mechanics",
243 value: "static stress and displacement",
244 default_outputs: &[
245 AnalysisPhysicsProfileDefaultOutput {
246 field: "structural.displacement",
247 location: "nodes",
248 },
249 AnalysisPhysicsProfileDefaultOutput {
250 field: "structural.von_mises",
251 location: "elements",
252 },
253 ],
254 },
255 AnalysisPhysicsProfileCatalogEntry {
256 profile: AnalysisCreateModelProfile::ThermoMechanicalCoupled,
257 label: "Thermo-mechanical",
258 family: "coupled physics",
259 target: "coupled solid mechanics and heat transfer",
260 value: "coupled temperature, stress, and displacement",
261 default_outputs: &[
262 AnalysisPhysicsProfileDefaultOutput {
263 field: "thermal.temperature",
264 location: "nodes",
265 },
266 AnalysisPhysicsProfileDefaultOutput {
267 field: "structural.displacement",
268 location: "nodes",
269 },
270 AnalysisPhysicsProfileDefaultOutput {
271 field: "structural.von_mises",
272 location: "elements",
273 },
274 ],
275 },
276 AnalysisPhysicsProfileCatalogEntry {
277 profile: AnalysisCreateModelProfile::ElectroThermalCoupled,
278 label: "Electro-thermal",
279 family: "coupled physics",
280 target: "coupled electromagnetics and heat transfer",
281 value: "resistive heating, temperature, and electrical fields",
282 default_outputs: &[
283 AnalysisPhysicsProfileDefaultOutput {
284 field: "electro_thermal.temperature",
285 location: "nodes",
286 },
287 AnalysisPhysicsProfileDefaultOutput {
288 field: "electro_thermal.joule_heat",
289 location: "elements",
290 },
291 AnalysisPhysicsProfileDefaultOutput {
292 field: "electro_thermal.electric_potential",
293 location: "nodes",
294 },
295 AnalysisPhysicsProfileDefaultOutput {
296 field: "electro_thermal.current_density",
297 location: "elements",
298 },
299 ],
300 },
301 AnalysisPhysicsProfileCatalogEntry {
302 profile: AnalysisCreateModelProfile::ThermalStandalone,
303 label: "Thermal",
304 family: "thermal",
305 target: "heat transfer",
306 value: "temperature and heat flux",
307 default_outputs: &[
308 AnalysisPhysicsProfileDefaultOutput {
309 field: "thermal.temperature",
310 location: "nodes",
311 },
312 AnalysisPhysicsProfileDefaultOutput {
313 field: "thermal.heat_flux",
314 location: "elements",
315 },
316 ],
317 },
318 AnalysisPhysicsProfileCatalogEntry {
319 profile: AnalysisCreateModelProfile::ModalStructural,
320 label: "Modal structural",
321 family: "modal",
322 target: "solid mechanics",
323 value: "natural frequencies and modes",
324 default_outputs: &[
325 AnalysisPhysicsProfileDefaultOutput {
326 field: "structural.mode_shapes",
327 location: "nodes",
328 },
329 AnalysisPhysicsProfileDefaultOutput {
330 field: "structural.natural_frequencies",
331 location: "modes",
332 },
333 ],
334 },
335 AnalysisPhysicsProfileCatalogEntry {
336 profile: AnalysisCreateModelProfile::AcousticHarmonic,
337 label: "Acoustic harmonic",
338 family: "acoustic",
339 target: "acoustics",
340 value: "frequency-domain pressure",
341 default_outputs: &[
342 AnalysisPhysicsProfileDefaultOutput {
343 field: "acoustic.pressure",
344 location: "nodes",
345 },
346 AnalysisPhysicsProfileDefaultOutput {
347 field: "acoustic.sound_pressure_level",
348 location: "nodes",
349 },
350 ],
351 },
352 AnalysisPhysicsProfileCatalogEntry {
353 profile: AnalysisCreateModelProfile::TransientStructural,
354 label: "Transient structural",
355 family: "structural",
356 target: "solid mechanics",
357 value: "time response",
358 default_outputs: &[
359 AnalysisPhysicsProfileDefaultOutput {
360 field: "structural.displacement",
361 location: "nodes",
362 },
363 AnalysisPhysicsProfileDefaultOutput {
364 field: "structural.von_mises",
365 location: "elements",
366 },
367 ],
368 },
369 AnalysisPhysicsProfileCatalogEntry {
370 profile: AnalysisCreateModelProfile::NonlinearStructural,
371 label: "Nonlinear structural",
372 family: "structural",
373 target: "solid mechanics",
374 value: "large deformation or nonlinear material",
375 default_outputs: &[
376 AnalysisPhysicsProfileDefaultOutput {
377 field: "structural.displacement",
378 location: "nodes",
379 },
380 AnalysisPhysicsProfileDefaultOutput {
381 field: "structural.von_mises",
382 location: "elements",
383 },
384 ],
385 },
386 AnalysisPhysicsProfileCatalogEntry {
387 profile: AnalysisCreateModelProfile::ElectromagneticStatic,
388 label: "Electromagnetic",
389 family: "electromagnetic",
390 target: "electromagnetics",
391 value: "electric and magnetic fields",
392 default_outputs: &[
393 AnalysisPhysicsProfileDefaultOutput {
394 field: "em.electric_field",
395 location: "elements",
396 },
397 AnalysisPhysicsProfileDefaultOutput {
398 field: "em.magnetic_flux_density",
399 location: "elements",
400 },
401 ],
402 },
403 AnalysisPhysicsProfileCatalogEntry {
404 profile: AnalysisCreateModelProfile::CfdSteadyState,
405 label: "CFD steady state",
406 family: "CFD",
407 target: "fluid flow",
408 value: "steady flow fields",
409 default_outputs: &[
410 AnalysisPhysicsProfileDefaultOutput {
411 field: "fluid.velocity",
412 location: "nodes",
413 },
414 AnalysisPhysicsProfileDefaultOutput {
415 field: "fluid.pressure",
416 location: "nodes",
417 },
418 ],
419 },
420 AnalysisPhysicsProfileCatalogEntry {
421 profile: AnalysisCreateModelProfile::CfdTransient,
422 label: "CFD transient",
423 family: "CFD",
424 target: "fluid flow",
425 value: "time-dependent flow fields",
426 default_outputs: &[
427 AnalysisPhysicsProfileDefaultOutput {
428 field: "fluid.velocity",
429 location: "nodes",
430 },
431 AnalysisPhysicsProfileDefaultOutput {
432 field: "fluid.pressure",
433 location: "nodes",
434 },
435 ],
436 },
437 AnalysisPhysicsProfileCatalogEntry {
438 profile: AnalysisCreateModelProfile::ChtCoupled,
439 label: "Conjugate heat transfer",
440 family: "coupled physics",
441 target: "coupled fluid and solid heat transfer",
442 value: "fluid and solid heat transfer",
443 default_outputs: &[
444 AnalysisPhysicsProfileDefaultOutput {
445 field: "thermal.temperature",
446 location: "nodes",
447 },
448 AnalysisPhysicsProfileDefaultOutput {
449 field: "fluid.velocity",
450 location: "nodes",
451 },
452 AnalysisPhysicsProfileDefaultOutput {
453 field: "fluid.pressure",
454 location: "nodes",
455 },
456 ],
457 },
458 AnalysisPhysicsProfileCatalogEntry {
459 profile: AnalysisCreateModelProfile::FsiCoupled,
460 label: "Fluid-structure interaction",
461 family: "coupled physics",
462 target: "coupled fluid and structural response",
463 value: "fluid loads and structural response",
464 default_outputs: &[
465 AnalysisPhysicsProfileDefaultOutput {
466 field: "structural.displacement",
467 location: "nodes",
468 },
469 AnalysisPhysicsProfileDefaultOutput {
470 field: "structural.von_mises",
471 location: "elements",
472 },
473 AnalysisPhysicsProfileDefaultOutput {
474 field: "fluid.pressure",
475 location: "nodes",
476 },
477 ],
478 },
479];
480
481pub fn analysis_runtime_physics_profile_catalog() -> Vec<AnalysisRuntimePhysicsProfileCatalogEntry>
482{
483 ANALYSIS_PHYSICS_PROFILE_CATALOG
484 .iter()
485 .map(|entry| AnalysisRuntimePhysicsProfileCatalogEntry {
486 profile: entry.profile,
487 label: entry.label.to_string(),
488 family: entry.family.to_string(),
489 target: entry.target.to_string(),
490 value: entry.value.to_string(),
491 default_outputs: entry
492 .default_outputs
493 .iter()
494 .map(|output| AnalysisRuntimePhysicsProfileDefaultOutput {
495 field: output.field.to_string(),
496 location: output.location.to_string(),
497 })
498 .collect(),
499 })
500 .collect()
501}
502
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(rename_all = "snake_case")]
505pub enum PrecisionMode {
506 Fp32,
507 Fp64,
508 Mixed,
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(rename_all = "snake_case")]
513pub enum PreconditionerMode {
514 Auto,
515 Jacobi,
516 Amg,
517 Ilu,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(rename_all = "snake_case")]
522pub enum QualityPolicy {
523 Strict,
524 Balanced,
525 Exploratory,
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
529#[serde(rename_all = "snake_case")]
530pub enum PrepCalibrationProfile {
531 Auto,
532 Fast,
533 Balanced,
534 Conservative,
535}
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538pub struct ThermoRegionTemperatureDelta {
539 pub region_id: String,
540 pub temperature_delta_k: f64,
541}
542
543#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544pub struct ThermoTimeProfilePoint {
545 pub normalized_time: f64,
546 pub scale: f64,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(rename_all = "snake_case")]
551pub enum ThermoFieldInterpolationMode {
552 Linear,
553 Step,
554}
555
556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
557pub struct ThermoFieldSource {
558 pub source_id: String,
559 pub revision: u32,
560 #[serde(default)]
561 pub interpolation_mode: Option<ThermoFieldInterpolationMode>,
562 #[serde(default)]
563 pub expected_region_ids: Vec<String>,
564}
565
566#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
567pub struct ThermoMechanicalCouplingOptions {
568 pub enabled: bool,
569 pub reference_temperature_k: f64,
570 pub applied_temperature_delta_k: f64,
571 pub thermal_expansion_coefficient: f64,
572 #[serde(default)]
573 pub field_artifact_id: Option<String>,
574 #[serde(default)]
575 pub field_source: Option<ThermoFieldSource>,
576 #[serde(default)]
577 pub region_temperature_deltas: Vec<ThermoRegionTemperatureDelta>,
578 #[serde(default)]
579 pub time_profile: Vec<ThermoTimeProfilePoint>,
580}
581
582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
583pub struct ElectroRegionConductivityScale {
584 pub region_id: String,
585 pub conductivity_scale: f64,
586}
587
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589pub struct ElectroTimeProfilePoint {
590 pub normalized_time: f64,
591 pub current_scale: f64,
592}
593
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
595pub struct ElectroThermalCouplingOptions {
596 pub enabled: bool,
597 pub reference_temperature_k: f64,
598 pub applied_voltage_v: f64,
599 pub base_electrical_conductivity_s_per_m: f64,
600 pub resistive_heating_coefficient: f64,
601 #[serde(default)]
602 pub region_conductivity_scales: Vec<ElectroRegionConductivityScale>,
603 #[serde(default)]
604 pub time_profile: Vec<ElectroTimeProfilePoint>,
605}
606
607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
608pub struct PlasticityConstitutiveOptions {
609 pub enabled: bool,
610 pub yield_strain: f64,
611 pub hardening_modulus_ratio: f64,
612 pub saturation_exponent: f64,
613}
614
615#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
616pub struct ContactInterfaceOptions {
617 pub enabled: bool,
618 pub penalty_stiffness_scale: f64,
619 pub max_penetration_ratio: f64,
620 pub friction_coefficient: f64,
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
624#[serde(rename_all = "snake_case")]
625pub enum QualityReasonCode {
626 MaterialAssignmentConflict,
627 SolverNotConverged,
628 SolverBackendFallback,
629 FieldPromotionFallback,
630 FieldTopologyMismatch,
631 SolidMeshNoVolumeElements,
632 SolidMeshUnsupportedElementKind,
633 SolidMeshUnmappedLoadRegion,
634 SolidMeshUnmappedBoundaryConditionRegion,
635 SolidMeshMaterialCoverageIncomplete,
636 SolidMeshRenderTopologyIncomplete,
637 SolidMeshValidationEvidenceMissing,
638 SolidMeshValidationEvidenceFailed,
639 SolidMeshQualityMinJacobianFailed,
640 SolidMeshQualityAspectRatioWarn,
641 SolidMeshBoundaryProjectionWarn,
642 MissingSolidAnalysisMesh,
643 ModalResidualExceeded,
644 ModalOrthogonalityExceeded,
645 ModalSeparationLow,
646 TransientResidualExceeded,
647 TransientStabilityExceeded,
648 TransientStepFailure,
649 ThermoMechanicalTransientStress,
650 ThermoMechanicalConstitutiveSpreadHigh,
651 ThermoMechanicalAssignmentHeterogeneityHigh,
652 ThermoMechanicalGradientInstability,
653 ThermoMechanicalFieldCoverageLow,
654 ThermoMechanicalFieldExtrapolationHigh,
655 ElectroThermalTransientStress,
656 ElectroThermalNonlinearStress,
657 ElectromagneticSolveQualityLow,
658 ElectromagneticConductivitySpreadHigh,
659 ElectromagneticMaterialHeterogeneityHigh,
660 ElectromagneticAssignmentCoverageLow,
661 ElectromagneticRegionContrastHigh,
662 ElectromagneticConditioningHigh,
663 ElectromagneticSourceRealizationLow,
664 ElectromagneticSourceRegionCoverageLow,
665 ElectromagneticSourceMaterialAlignmentLow,
666 ElectromagneticSourceOverlapHigh,
667 ElectromagneticSourceInterferenceHigh,
668 ElectromagneticBoundaryLocalizationLow,
669 ElectromagneticGroundAnchorEffectivenessLow,
670 ElectromagneticInsulationLeakageHigh,
671 ElectromagneticBoundaryAnchoringLow,
672 ElectromagneticFluxDivergenceHigh,
673 ElectromagneticEnergyImbalanceHigh,
674 ElectromagneticBoundaryEnergyLow,
675 ElectromagneticBoundaryPenaltyConditioningHigh,
676 ElectromagneticSourceRegionEnergyConsistencyLow,
677 ElectromagneticRealResidualHigh,
678 ElectromagneticImagResidualHigh,
679 ElectromagneticSweepCoverageLow,
680 ElectromagneticResonanceSharpnessLow,
681 PlasticityNonlinearStress,
682 ContactNonlinearStress,
683 NonlinearResidualExceeded,
684 NonlinearIncrementFailure,
685 ThermoMechanicalNonlinearStress,
686 ThermalResidualExceeded,
687 ThermalConstitutiveSpreadHigh,
688}
689
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
691pub struct QualityReason {
692 pub code: QualityReasonCode,
693 pub detail: String,
694}
695
696#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
697pub struct AnalysisRunOptions {
698 pub deterministic_mode: bool,
699 pub precision_mode: PrecisionMode,
700 pub preconditioner_mode: PreconditionerMode,
701 pub quality_policy: QualityPolicy,
702 #[serde(default)]
703 pub prep_context: Option<AnalysisRunPrepContext>,
704 #[serde(default)]
705 pub prep_artifact_id: Option<String>,
706 #[serde(default)]
707 pub analysis_mesh_artifact_path: Option<String>,
708 #[serde(default)]
709 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
710}
711
712#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
713pub struct AnalysisElectromagneticRunOptions {
714 pub deterministic_mode: bool,
715 pub precision_mode: PrecisionMode,
716 pub quality_policy: QualityPolicy,
717 pub residual_target: f64,
718 pub harmonic_tolerance: f64,
719 pub harmonic_max_iterations: usize,
720 #[serde(default)]
721 pub prep_context: Option<AnalysisRunPrepContext>,
722 #[serde(default)]
723 pub prep_artifact_id: Option<String>,
724 #[serde(default)]
725 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
726 #[serde(default)]
727 pub sweep_enabled: bool,
728 #[serde(default)]
729 pub sweep_frequency_hz: Vec<f64>,
730}
731
732impl Default for AnalysisElectromagneticRunOptions {
733 fn default() -> Self {
734 Self {
735 deterministic_mode: false,
736 precision_mode: PrecisionMode::Fp64,
737 quality_policy: QualityPolicy::Balanced,
738 residual_target: 1.0e-6,
739 harmonic_tolerance: 1.0e-7,
740 harmonic_max_iterations: 96,
741 prep_context: None,
742 prep_artifact_id: None,
743 prep_calibration_profile: None,
744 sweep_enabled: false,
745 sweep_frequency_hz: Vec::new(),
746 }
747 }
748}
749
750impl Default for AnalysisRunOptions {
751 fn default() -> Self {
752 Self {
753 deterministic_mode: false,
754 precision_mode: PrecisionMode::Fp64,
755 preconditioner_mode: PreconditionerMode::Auto,
756 quality_policy: QualityPolicy::Balanced,
757 prep_context: None,
758 prep_artifact_id: None,
759 analysis_mesh_artifact_path: None,
760 prep_calibration_profile: None,
761 }
762 }
763}
764
765#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
766pub struct AnalysisTransientRunOptions {
767 pub deterministic_mode: bool,
768 pub precision_mode: PrecisionMode,
769 pub quality_policy: QualityPolicy,
770 pub time_step_s: f64,
771 pub min_time_step_s: f64,
772 pub max_time_step_s: f64,
773 pub step_count: usize,
774 pub max_linear_iters: usize,
775 pub tolerance: f64,
776 pub residual_target: f64,
777 pub adaptive_time_step: bool,
778 pub max_step_retries: usize,
779 pub adapt_min_scale: f64,
780 pub adapt_max_scale: f64,
781 pub adapt_growth_exponent: f64,
782 pub adapt_retry_growth_cap: f64,
783 pub adapt_nonconverged_shrink: f64,
784 pub dt_bucket_rel_tolerance: f64,
785 #[serde(default)]
786 pub prep_context: Option<AnalysisRunPrepContext>,
787 #[serde(default)]
788 pub prep_artifact_id: Option<String>,
789 #[serde(default)]
790 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
791}
792
793impl Default for AnalysisTransientRunOptions {
794 fn default() -> Self {
795 Self {
796 deterministic_mode: false,
797 precision_mode: PrecisionMode::Fp64,
798 quality_policy: QualityPolicy::Balanced,
799 time_step_s: 1.0e-3,
800 min_time_step_s: 1.0e-6,
801 max_time_step_s: 2.0e-2,
802 step_count: 10,
803 max_linear_iters: 128,
804 tolerance: 1.0e-8,
805 residual_target: 1.0e-6,
806 adaptive_time_step: true,
807 max_step_retries: 4,
808 adapt_min_scale: 0.8,
809 adapt_max_scale: 1.25,
810 adapt_growth_exponent: 0.35,
811 adapt_retry_growth_cap: 1.05,
812 adapt_nonconverged_shrink: 0.75,
813 dt_bucket_rel_tolerance: 0.0,
814 prep_context: None,
815 prep_artifact_id: None,
816 prep_calibration_profile: None,
817 }
818 }
819}
820
821#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
822pub struct AnalysisAcousticRunOptions {
823 pub deterministic_mode: bool,
824 pub precision_mode: PrecisionMode,
825 pub quality_policy: QualityPolicy,
826 pub mode_count: usize,
827 pub residual_warn_threshold: f64,
828 #[serde(default)]
829 pub prep_context: Option<AnalysisRunPrepContext>,
830 #[serde(default)]
831 pub prep_artifact_id: Option<String>,
832 #[serde(default)]
833 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
834}
835
836impl Default for AnalysisAcousticRunOptions {
837 fn default() -> Self {
838 Self {
839 deterministic_mode: false,
840 precision_mode: PrecisionMode::Fp64,
841 quality_policy: QualityPolicy::Balanced,
842 mode_count: 3,
843 residual_warn_threshold: 1.0e-3,
844 prep_context: None,
845 prep_artifact_id: None,
846 prep_calibration_profile: None,
847 }
848 }
849}
850
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
852pub struct AnalysisCfdRunOptions {
853 pub deterministic_mode: bool,
854 pub precision_mode: PrecisionMode,
855 pub quality_policy: QualityPolicy,
856 pub time_step_s: f64,
857 pub step_count: usize,
858 pub max_linear_iters: usize,
859 pub tolerance: f64,
860 pub residual_warn_threshold: f64,
861 #[serde(default)]
862 pub prep_context: Option<AnalysisRunPrepContext>,
863 #[serde(default)]
864 pub prep_artifact_id: Option<String>,
865 #[serde(default)]
866 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
867}
868
869impl Default for AnalysisCfdRunOptions {
870 fn default() -> Self {
871 Self {
872 deterministic_mode: false,
873 precision_mode: PrecisionMode::Fp64,
874 quality_policy: QualityPolicy::Balanced,
875 time_step_s: 1.0e-3,
876 step_count: 12,
877 max_linear_iters: 128,
878 tolerance: 1.0e-8,
879 residual_warn_threshold: 1.0e-5,
880 prep_context: None,
881 prep_artifact_id: None,
882 prep_calibration_profile: None,
883 }
884 }
885}
886
887#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
888pub struct AnalysisChtRunOptions {
889 pub deterministic_mode: bool,
890 pub precision_mode: PrecisionMode,
891 pub quality_policy: QualityPolicy,
892 pub time_step_s: f64,
893 pub step_count: usize,
894 pub max_linear_iters: usize,
895 pub tolerance: f64,
896 pub residual_warn_threshold: f64,
897 #[serde(default)]
898 pub prep_context: Option<AnalysisRunPrepContext>,
899 #[serde(default)]
900 pub prep_artifact_id: Option<String>,
901 #[serde(default)]
902 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
903}
904
905impl Default for AnalysisChtRunOptions {
906 fn default() -> Self {
907 Self {
908 deterministic_mode: false,
909 precision_mode: PrecisionMode::Fp64,
910 quality_policy: QualityPolicy::Balanced,
911 time_step_s: 1.0e-3,
912 step_count: 12,
913 max_linear_iters: 128,
914 tolerance: 1.0e-8,
915 residual_warn_threshold: 1.0e-4,
916 prep_context: None,
917 prep_artifact_id: None,
918 prep_calibration_profile: None,
919 }
920 }
921}
922
923#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
924pub struct AnalysisFsiRunOptions {
925 pub deterministic_mode: bool,
926 pub precision_mode: PrecisionMode,
927 pub quality_policy: QualityPolicy,
928 pub time_step_s: f64,
929 pub step_count: usize,
930 pub max_linear_iters: usize,
931 pub tolerance: f64,
932 pub residual_warn_threshold: f64,
933 #[serde(default)]
934 pub prep_context: Option<AnalysisRunPrepContext>,
935 #[serde(default)]
936 pub prep_artifact_id: Option<String>,
937 #[serde(default)]
938 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
939}
940
941impl Default for AnalysisFsiRunOptions {
942 fn default() -> Self {
943 Self {
944 deterministic_mode: false,
945 precision_mode: PrecisionMode::Fp64,
946 quality_policy: QualityPolicy::Balanced,
947 time_step_s: 1.0e-3,
948 step_count: 12,
949 max_linear_iters: 128,
950 tolerance: 1.0e-8,
951 residual_warn_threshold: 1.0e-4,
952 prep_context: None,
953 prep_artifact_id: None,
954 prep_calibration_profile: None,
955 }
956 }
957}
958
959#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
960pub struct AnalysisThermalRunOptions {
961 pub deterministic_mode: bool,
962 pub precision_mode: PrecisionMode,
963 pub quality_policy: QualityPolicy,
964 pub step_count: usize,
965 pub time_step_s: f64,
966 pub residual_warn_threshold: f64,
967 #[serde(default)]
968 pub prep_context: Option<AnalysisRunPrepContext>,
969 #[serde(default)]
970 pub prep_artifact_id: Option<String>,
971 #[serde(default)]
972 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
973}
974
975impl Default for AnalysisThermalRunOptions {
976 fn default() -> Self {
977 Self {
978 deterministic_mode: false,
979 precision_mode: PrecisionMode::Fp64,
980 quality_policy: QualityPolicy::Balanced,
981 step_count: 10,
982 time_step_s: 1.0e-2,
983 residual_warn_threshold: 1.0e-4,
984 prep_context: None,
985 prep_artifact_id: None,
986 prep_calibration_profile: None,
987 }
988 }
989}
990
991impl AnalysisTransientRunOptions {
992 pub fn coarse() -> Self {
993 Self {
994 deterministic_mode: false,
995 precision_mode: PrecisionMode::Fp32,
996 quality_policy: QualityPolicy::Exploratory,
997 time_step_s: 5.0e-3,
998 min_time_step_s: 5.0e-4,
999 max_time_step_s: 2.0e-2,
1000 step_count: 6,
1001 max_linear_iters: 64,
1002 tolerance: 1.0e-6,
1003 residual_target: 1.0e-4,
1004 adaptive_time_step: true,
1005 max_step_retries: 2,
1006 adapt_min_scale: 0.75,
1007 adapt_max_scale: 1.3,
1008 adapt_growth_exponent: 0.3,
1009 adapt_retry_growth_cap: 1.02,
1010 adapt_nonconverged_shrink: 0.7,
1011 dt_bucket_rel_tolerance: 0.02,
1012 prep_context: None,
1013 prep_artifact_id: None,
1014 prep_calibration_profile: None,
1015 }
1016 }
1017
1018 pub fn balanced() -> Self {
1019 Self::default()
1020 }
1021
1022 pub fn solid_recommended() -> Self {
1023 Self {
1024 quality_policy: QualityPolicy::Balanced,
1025 deterministic_mode: true,
1026 precision_mode: PrecisionMode::Fp64,
1027 dt_bucket_rel_tolerance: 0.01,
1028 ..Self::balanced()
1029 }
1030 }
1031
1032 pub fn high_accuracy() -> Self {
1033 Self {
1034 deterministic_mode: true,
1035 precision_mode: PrecisionMode::Fp64,
1036 quality_policy: QualityPolicy::Strict,
1037 time_step_s: 5.0e-4,
1038 min_time_step_s: 5.0e-6,
1039 max_time_step_s: 2.0e-3,
1040 step_count: 24,
1041 max_linear_iters: 256,
1042 tolerance: 1.0e-10,
1043 residual_target: 1.0e-7,
1044 adaptive_time_step: true,
1045 max_step_retries: 8,
1046 adapt_min_scale: 0.85,
1047 adapt_max_scale: 1.2,
1048 adapt_growth_exponent: 0.45,
1049 adapt_retry_growth_cap: 1.03,
1050 adapt_nonconverged_shrink: 0.8,
1051 dt_bucket_rel_tolerance: 0.005,
1052 prep_context: None,
1053 prep_artifact_id: None,
1054 prep_calibration_profile: None,
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1060pub struct AnalysisModalRunOptions {
1061 pub deterministic_mode: bool,
1062 pub precision_mode: PrecisionMode,
1063 pub quality_policy: QualityPolicy,
1064 pub mode_count: usize,
1065 pub residual_warn_threshold: f64,
1066 #[serde(default)]
1067 pub prep_context: Option<AnalysisRunPrepContext>,
1068 #[serde(default)]
1069 pub prep_artifact_id: Option<String>,
1070 #[serde(default)]
1071 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
1072}
1073
1074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1075pub struct AnalysisNonlinearRunOptions {
1076 pub deterministic_mode: bool,
1077 pub precision_mode: PrecisionMode,
1078 pub quality_policy: QualityPolicy,
1079 pub increment_count: usize,
1080 pub max_newton_iters: usize,
1081 pub tolerance: f64,
1082 pub residual_convergence_factor: f64,
1083 pub increment_norm_tolerance: f64,
1084 pub line_search: bool,
1085 pub max_line_search_backtracks: usize,
1086 pub line_search_reduction: f64,
1087 pub tangent_refresh_interval: usize,
1088 #[serde(default)]
1089 pub prep_context: Option<AnalysisRunPrepContext>,
1090 #[serde(default)]
1091 pub prep_artifact_id: Option<String>,
1092 #[serde(default)]
1093 pub prep_calibration_profile: Option<PrepCalibrationProfile>,
1094}
1095
1096impl Default for AnalysisNonlinearRunOptions {
1097 fn default() -> Self {
1098 Self {
1099 deterministic_mode: false,
1100 precision_mode: PrecisionMode::Fp64,
1101 quality_policy: QualityPolicy::Balanced,
1102 increment_count: 12,
1103 max_newton_iters: 24,
1104 tolerance: 1.0e-6,
1105 residual_convergence_factor: 5.0,
1106 increment_norm_tolerance: 1.0e-7,
1107 line_search: true,
1108 max_line_search_backtracks: 6,
1109 line_search_reduction: 0.5,
1110 tangent_refresh_interval: 2,
1111 prep_context: None,
1112 prep_artifact_id: None,
1113 prep_calibration_profile: None,
1114 }
1115 }
1116}
1117
1118impl AnalysisNonlinearRunOptions {
1119 pub fn coarse() -> Self {
1120 Self {
1121 deterministic_mode: false,
1122 precision_mode: PrecisionMode::Fp32,
1123 quality_policy: QualityPolicy::Exploratory,
1124 increment_count: 8,
1125 max_newton_iters: 16,
1126 tolerance: 5.0e-6,
1127 residual_convergence_factor: 8.0,
1128 increment_norm_tolerance: 5.0e-7,
1129 line_search: false,
1130 max_line_search_backtracks: 0,
1131 line_search_reduction: 0.6,
1132 tangent_refresh_interval: 4,
1133 prep_context: None,
1134 prep_artifact_id: None,
1135 prep_calibration_profile: None,
1136 }
1137 }
1138
1139 pub fn balanced() -> Self {
1140 Self::default()
1141 }
1142
1143 pub fn high_accuracy() -> Self {
1144 Self {
1145 deterministic_mode: true,
1146 precision_mode: PrecisionMode::Fp64,
1147 quality_policy: QualityPolicy::Strict,
1148 increment_count: 24,
1149 max_newton_iters: 40,
1150 tolerance: 1.0e-7,
1151 residual_convergence_factor: 3.0,
1152 increment_norm_tolerance: 5.0e-8,
1153 line_search: true,
1154 max_line_search_backtracks: 10,
1155 line_search_reduction: 0.5,
1156 tangent_refresh_interval: 1,
1157 prep_context: None,
1158 prep_artifact_id: None,
1159 prep_calibration_profile: None,
1160 }
1161 }
1162
1163 pub fn solid_recommended() -> Self {
1164 Self {
1165 deterministic_mode: true,
1166 precision_mode: PrecisionMode::Fp64,
1167 quality_policy: QualityPolicy::Balanced,
1168 increment_count: 24,
1169 max_newton_iters: 28,
1170 tolerance: 1.0e-6,
1171 residual_convergence_factor: 4.0,
1172 increment_norm_tolerance: 8.0e-8,
1173 line_search: true,
1174 max_line_search_backtracks: 8,
1175 line_search_reduction: 0.5,
1176 tangent_refresh_interval: 2,
1177 prep_context: None,
1178 prep_artifact_id: None,
1179 prep_calibration_profile: None,
1180 }
1181 }
1182}
1183
1184impl Default for AnalysisModalRunOptions {
1185 fn default() -> Self {
1186 Self {
1187 deterministic_mode: false,
1188 precision_mode: PrecisionMode::Fp64,
1189 quality_policy: QualityPolicy::Balanced,
1190 mode_count: 3,
1191 residual_warn_threshold: 1.0e-3,
1192 prep_context: None,
1193 prep_artifact_id: None,
1194 prep_calibration_profile: None,
1195 }
1196 }
1197}
1198
1199impl AnalysisModalRunOptions {
1200 pub fn coarse() -> Self {
1201 Self {
1202 deterministic_mode: false,
1203 precision_mode: PrecisionMode::Fp32,
1204 quality_policy: QualityPolicy::Exploratory,
1205 mode_count: 2,
1206 residual_warn_threshold: 5.0e-3,
1207 prep_context: None,
1208 prep_artifact_id: None,
1209 prep_calibration_profile: None,
1210 }
1211 }
1212
1213 pub fn balanced() -> Self {
1214 Self::default()
1215 }
1216
1217 pub fn high_accuracy() -> Self {
1218 Self {
1219 deterministic_mode: true,
1220 precision_mode: PrecisionMode::Fp64,
1221 quality_policy: QualityPolicy::Strict,
1222 mode_count: 8,
1223 residual_warn_threshold: 5.0e-4,
1224 prep_context: None,
1225 prep_artifact_id: None,
1226 prep_calibration_profile: None,
1227 }
1228 }
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1232#[serde(rename_all = "snake_case")]
1233pub enum QualityGate {
1234 Pass,
1235 Warn,
1236 Fail,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1240#[serde(rename_all = "snake_case")]
1241pub enum RunStatus {
1242 Publishable,
1243 Degraded,
1244 Rejected,
1245}
1246
1247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1248pub struct RunProvenance {
1249 pub backend: ComputeBackend,
1250 pub solver_backend: String,
1251 pub solver_device_apply_k_ratio: f64,
1252 pub solver_host_sync_count: u32,
1253 pub precision_mode: String,
1254 pub deterministic_mode: bool,
1255 pub solver_method: String,
1256 pub preconditioner: String,
1257 pub quality_policy: String,
1258 pub fallback_events: Vec<String>,
1259}
1260
1261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1262pub struct AnalysisRenderTopology {
1263 pub schema_version: String,
1264 pub source: AnalysisRenderTopologySource,
1265 pub meshes: Vec<AnalysisRenderMesh>,
1266}
1267
1268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1269#[serde(rename_all = "snake_case")]
1270pub enum AnalysisRenderTopologySource {
1271 SolverPrep,
1272 AnalysisMesh,
1273}
1274
1275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1276pub struct AnalysisRenderMesh {
1277 pub mesh_id: String,
1278 pub vertices: Vec<[f64; 3]>,
1279 pub triangles: Vec<[u32; 3]>,
1280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1281 pub regions: Vec<AnalysisRenderRegion>,
1282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1283 pub vertex_volume_node_indices: Vec<Option<usize>>,
1284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1285 pub triangle_volume_element_indices: Vec<Option<usize>>,
1286}
1287
1288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1289pub struct AnalysisRenderRegion {
1290 pub region_id: String,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub label: Option<String>,
1293 #[serde(default, skip_serializing_if = "Option::is_none")]
1294 pub tag: Option<String>,
1295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296 pub triangle_ranges: Vec<AnalysisRenderTriangleRange>,
1297}
1298
1299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1300pub struct AnalysisRenderTriangleRange {
1301 pub start: u32,
1302 pub count: u32,
1303}
1304
1305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1306pub struct AnalysisRunResult {
1307 pub run_id: String,
1308 pub run: FeaRunResult,
1309 #[serde(default, skip_serializing_if = "Option::is_none")]
1310 pub render_topology: Option<AnalysisRenderTopology>,
1311 pub modal_results: Option<ModalResultsData>,
1312 #[serde(default)]
1313 pub thermal_results: Option<ThermalResultsData>,
1314 pub transient_results: Option<TransientResultsData>,
1315 pub nonlinear_results: Option<NonlinearResultsData>,
1316 #[serde(default)]
1317 pub electromagnetic_results: Option<ElectromagneticResultsData>,
1318 pub model_validity: QualityGate,
1319 pub solver_convergence: QualityGate,
1320 pub result_quality: QualityGate,
1321 pub run_status: RunStatus,
1322 pub publishable: bool,
1323 pub quality_reasons: Vec<QualityReason>,
1324 pub provenance: RunProvenance,
1325}
1326
1327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1328pub struct AnalysisArtifactRecord {
1329 pub run_id: String,
1330 pub created_at: String,
1331 pub op_version: String,
1332 pub field_ids: Vec<String>,
1333}
1334
1335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1336pub struct AnalysisResultsQuery {
1337 pub include_fields: Vec<String>,
1338 pub include_field_values: bool,
1339 pub include_diagnostics: bool,
1340 pub diagnostic_codes: Vec<String>,
1341 pub include_modal_results: bool,
1342 pub mode_indices: Vec<usize>,
1343 pub include_transient_results: bool,
1344 pub transient_snapshot_indices: Vec<usize>,
1345 pub include_nonlinear_results: bool,
1346 pub include_electromagnetic_results: bool,
1347}
1348
1349impl Default for AnalysisResultsQuery {
1350 fn default() -> Self {
1351 Self {
1352 include_fields: Vec::new(),
1353 include_field_values: true,
1354 include_diagnostics: true,
1355 diagnostic_codes: Vec::new(),
1356 include_modal_results: true,
1357 mode_indices: Vec::new(),
1358 include_transient_results: true,
1359 transient_snapshot_indices: Vec::new(),
1360 include_nonlinear_results: true,
1361 include_electromagnetic_results: true,
1362 }
1363 }
1364}
1365
1366impl AnalysisResultsQuery {
1367 pub fn metadata_only() -> Self {
1368 Self {
1369 include_field_values: false,
1370 include_modal_results: false,
1371 include_transient_results: false,
1372 include_nonlinear_results: false,
1373 include_electromagnetic_results: false,
1374 ..Self::default()
1375 }
1376 }
1377
1378 pub fn field_values(field_id: impl Into<String>) -> Self {
1379 Self {
1380 include_fields: vec![field_id.into()],
1381 include_field_values: true,
1382 include_diagnostics: false,
1383 include_modal_results: false,
1384 include_transient_results: false,
1385 include_nonlinear_results: false,
1386 include_electromagnetic_results: false,
1387 ..Self::default()
1388 }
1389 }
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1393#[serde(rename_all = "snake_case")]
1394pub enum AnalysisFieldKind {
1395 Scalar,
1396 Vector,
1397 Tensor,
1398 Unknown,
1399}
1400
1401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1402#[serde(rename_all = "snake_case")]
1403pub enum AnalysisFieldStorage {
1404 HostF64,
1405 DeviceRef,
1406}
1407
1408#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1409#[serde(rename_all = "snake_case")]
1410pub enum AnalysisFieldLocation {
1411 Node,
1412 Element,
1413 Edge,
1414 BoundaryFace,
1415 InterfaceFace,
1416 Mode,
1417 Global,
1418 #[default]
1419 Unknown,
1420}
1421
1422pub const ANALYSIS_FIELD_DEFAULT_PAGE_SIZE: usize = 4096;
1423pub const ANALYSIS_FIELD_DEFAULT_MATERIALIZE_LIMIT: usize = 256;
1424pub const ANALYSIS_RUN_DATASET_SCHEMA_VERSION: u32 = 1;
1425pub const ANALYSIS_FIELD_DESCRIPTORS_SCHEMA_VERSION: u32 = 1;
1426pub const ANALYSIS_DIAGNOSTICS_SCHEMA_VERSION: u32 = 1;
1427pub const ANALYSIS_OBJECT_ARTIFACT_METADATA_SCHEMA_VERSION: u32 = 1;
1428pub const ANALYSIS_RUN_DATASET_KIND: &str = "finite_element_run_dataset";
1429pub const ANALYSIS_DATASET_ARTIFACT_KIND: &str = "finite_element_dataset";
1430pub const ANALYSIS_FIELD_DESCRIPTORS_ARTIFACT_KIND: &str = "finite_element_field_descriptors";
1431pub const ANALYSIS_DIAGNOSTICS_ARTIFACT_KIND: &str = "finite_element_diagnostics";
1432pub const ANALYSIS_ARTIFACT_MANIFEST_KIND: &str = "finite_element_artifact_manifest";
1433
1434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1435#[serde(rename_all = "snake_case")]
1436pub enum AnalysisDocumentKind {
1437 Study,
1438 Sweep,
1439}
1440
1441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1442#[serde(rename_all = "camelCase")]
1443pub struct AnalysisRuntimeCapabilities {
1444 pub supported_document_extensions: Vec<String>,
1445 pub supported_geometry_extensions: Vec<String>,
1446 #[serde(default)]
1447 pub physics_profiles: Vec<AnalysisRuntimePhysicsProfileCatalogEntry>,
1448 pub supports_check: bool,
1449 pub supports_run: bool,
1450 pub supports_results: bool,
1451 pub supports_live_progress: bool,
1452 pub visualization_backend: String,
1453}
1454
1455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1456#[serde(rename_all = "camelCase")]
1457pub struct AnalysisDocumentCheckResult {
1458 pub path: String,
1459 pub document_kind: AnalysisDocumentKind,
1460 pub valid: bool,
1461 pub validation: JsonValue,
1462 #[serde(skip_serializing_if = "Option::is_none")]
1463 pub plan: Option<JsonValue>,
1464 pub diagnostics: Vec<JsonValue>,
1465 pub evidence_artifact_paths: Vec<String>,
1466}
1467
1468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1469#[serde(rename_all = "camelCase")]
1470pub struct AnalysisDocumentRunResult {
1471 pub path: String,
1472 pub document_kind: AnalysisDocumentKind,
1473 pub run: JsonValue,
1474 #[serde(skip_serializing_if = "Option::is_none")]
1475 pub results: Option<JsonValue>,
1476 pub field_descriptors: Vec<AnalysisFieldDescriptor>,
1477 #[serde(skip_serializing_if = "Option::is_none")]
1478 pub result_summary: Option<JsonValue>,
1479 pub figure_handles: Vec<u32>,
1480 #[serde(skip_serializing_if = "Option::is_none")]
1481 pub artifact_manifest: Option<JsonValue>,
1482 pub diagnostics: Vec<JsonValue>,
1483 pub progress_events: Vec<FeaProgressEvent>,
1484}
1485
1486#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1487#[serde(rename_all = "camelCase")]
1488pub struct AnalysisFieldRequestOptions {
1489 #[serde(default)]
1490 #[serde(skip_serializing_if = "Option::is_none")]
1491 pub offset: Option<usize>,
1492 #[serde(default)]
1493 #[serde(skip_serializing_if = "Option::is_none")]
1494 pub limit: Option<usize>,
1495}
1496
1497#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1498#[serde(rename_all = "camelCase")]
1499pub struct AnalysisFieldPageResult {
1500 pub run_id: String,
1501 pub field_id: String,
1502 pub descriptor: AnalysisFieldDescriptor,
1503 pub values: Vec<f64>,
1504 pub offset: usize,
1505 pub count: usize,
1506 pub total_count: usize,
1507 pub truncated: bool,
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1511#[serde(rename_all = "camelCase")]
1512pub struct AnalysisRunDatasetStudyRef {
1513 pub path: String,
1514 pub document_kind: Option<AnalysisDocumentKind>,
1515 pub valid: Option<bool>,
1516 pub evidence_artifact_paths: Vec<String>,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520#[serde(rename_all = "camelCase")]
1521pub struct AnalysisRunDatasetFieldPagingPolicy {
1522 pub mode: String,
1523 pub policy: String,
1524}
1525
1526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1527#[serde(rename_all = "camelCase")]
1528pub struct AnalysisRunDatasetPayload {
1529 pub schema_version: u32,
1530 pub kind: String,
1531 pub run_id: String,
1532 pub doc_path: String,
1533 pub study: AnalysisRunDatasetStudyRef,
1534 #[serde(default)]
1535 #[serde(skip_serializing_if = "Option::is_none")]
1536 pub analysis_profile: Option<AnalysisCreateModelProfile>,
1537 #[serde(default)]
1538 #[serde(skip_serializing_if = "Option::is_none")]
1539 pub physics_family: Option<String>,
1540 #[serde(default)]
1541 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub analysis_run_kind: Option<AnalysisRunKind>,
1543 #[serde(default)]
1544 #[serde(skip_serializing_if = "Option::is_none")]
1545 pub available_output_summary: Option<String>,
1546 pub run: JsonValue,
1547 pub result_summary: JsonValue,
1548 pub fields: Vec<AnalysisFieldDescriptor>,
1549 pub field_paging: AnalysisRunDatasetFieldPagingPolicy,
1550 pub diagnostics: Vec<JsonValue>,
1551 pub artifact_manifest: JsonValue,
1552}
1553
1554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1555#[serde(rename_all = "camelCase")]
1556pub struct AnalysisFieldDescriptorsArtifactPayload {
1557 pub schema_version: u32,
1558 pub run_id: String,
1559 pub fields: Vec<AnalysisFieldDescriptor>,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1563#[serde(rename_all = "camelCase")]
1564pub struct AnalysisDiagnosticsArtifactPayload {
1565 pub schema_version: u32,
1566 pub run_id: String,
1567 pub diagnostics: Vec<JsonValue>,
1568}
1569
1570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1571#[serde(rename_all = "camelCase")]
1572pub struct AnalysisObjectArtifactMetadata {
1573 pub schema_version: u32,
1574 pub artifact_id: String,
1575 pub src: String,
1576 pub bytes: u64,
1577 pub mime: String,
1578 pub kind: String,
1579 #[serde(skip_serializing_if = "Option::is_none")]
1580 pub handle: Option<u32>,
1581}
1582
1583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1584#[serde(rename_all = "camelCase")]
1585pub struct AnalysisFieldPagingDescriptor {
1586 pub mode: String,
1587 pub value_unit: String,
1588 pub total_count: Option<usize>,
1589 pub page_size: usize,
1590 pub page_count: Option<usize>,
1591 pub default_materialize_limit: usize,
1592}
1593
1594impl AnalysisFieldPagingDescriptor {
1595 pub fn for_value_count(value_count: usize) -> Self {
1596 Self {
1597 mode: "paged".to_string(),
1598 value_unit: "scalar_component".to_string(),
1599 total_count: Some(value_count),
1600 page_size: ANALYSIS_FIELD_DEFAULT_PAGE_SIZE,
1601 page_count: Some(value_count.div_ceil(ANALYSIS_FIELD_DEFAULT_PAGE_SIZE)),
1602 default_materialize_limit: ANALYSIS_FIELD_DEFAULT_MATERIALIZE_LIMIT,
1603 }
1604 }
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1608#[serde(rename_all = "camelCase")]
1609pub struct AnalysisFieldStorageRef {
1610 pub kind: String,
1611 pub field_id: String,
1612 pub storage: AnalysisFieldStorage,
1613 pub size_bytes: Option<u64>,
1614}
1615
1616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1617pub struct AnalysisFieldDescriptor {
1618 pub field_id: String,
1619 #[serde(default)]
1620 pub family: String,
1621 #[serde(default)]
1622 pub quantity: String,
1623 #[serde(default)]
1624 pub topology_id: Option<String>,
1625 #[serde(default)]
1626 pub element_kind: Option<String>,
1627 pub class_name: String,
1628 pub kind: AnalysisFieldKind,
1629 pub dtype: String,
1630 #[serde(default)]
1631 pub unit: Option<String>,
1632 #[serde(default)]
1633 pub location: AnalysisFieldLocation,
1634 pub shape: Vec<usize>,
1635 pub element_count: usize,
1636 #[serde(default)]
1637 pub entity_count: usize,
1638 #[serde(default)]
1639 pub value_count: usize,
1640 pub component_count: Option<usize>,
1641 pub residency: String,
1642 pub storage: AnalysisFieldStorage,
1643 pub size_bytes: Option<u64>,
1644 pub paging: AnalysisFieldPagingDescriptor,
1645 #[serde(rename = "storageRef")]
1646 pub storage_ref: AnalysisFieldStorageRef,
1647}
1648
1649impl AnalysisFieldDescriptor {
1650 pub fn from_field(field: &AnalysisField) -> Self {
1651 let storage = match &field.values {
1652 AnalysisFieldValues::HostF64(_) => AnalysisFieldStorage::HostF64,
1653 AnalysisFieldValues::DeviceRef(_) => AnalysisFieldStorage::DeviceRef,
1654 };
1655 let kind = infer_field_kind(&field.field_id, &field.shape);
1656 let class_name = match kind {
1657 AnalysisFieldKind::Scalar => "fea.ScalarField",
1658 AnalysisFieldKind::Vector => "fea.VectorField",
1659 AnalysisFieldKind::Tensor => "fea.TensorField",
1660 AnalysisFieldKind::Unknown => "fea.Field",
1661 }
1662 .to_string();
1663 let residency = match storage {
1664 AnalysisFieldStorage::HostF64 => "cpu",
1665 AnalysisFieldStorage::DeviceRef => "gpu",
1666 }
1667 .to_string();
1668 let location = infer_field_location(&field.field_id);
1669 let component_count = infer_component_count(&field.field_id, &field.shape);
1670 let value_count = field.element_count();
1671 let entity_count = infer_entity_count(&field.shape, location, component_count);
1672 let size_bytes = value_count
1673 .checked_mul(std::mem::size_of::<f64>())
1674 .map(|bytes| bytes as u64);
1675 Self {
1676 field_id: field.field_id.clone(),
1677 family: infer_field_family(&field.field_id).to_string(),
1678 quantity: infer_field_quantity(&field.field_id).to_string(),
1679 topology_id: infer_field_topology_id(&field.field_id, location).map(str::to_string),
1680 element_kind: infer_field_element_kind(&field.field_id, location).map(str::to_string),
1681 class_name,
1682 kind,
1683 dtype: "double".to_string(),
1684 unit: infer_field_unit(&field.field_id).map(str::to_string),
1685 location,
1686 shape: field.shape.clone(),
1687 element_count: value_count,
1688 entity_count,
1689 value_count,
1690 component_count,
1691 residency,
1692 storage,
1693 size_bytes,
1694 paging: AnalysisFieldPagingDescriptor::for_value_count(value_count),
1695 storage_ref: AnalysisFieldStorageRef {
1696 kind: "runtime_field".to_string(),
1697 field_id: field.field_id.clone(),
1698 storage,
1699 size_bytes,
1700 },
1701 }
1702 }
1703}
1704
1705fn infer_entity_count(
1706 shape: &[usize],
1707 location: AnalysisFieldLocation,
1708 component_count: Option<usize>,
1709) -> usize {
1710 if matches!(
1711 location,
1712 AnalysisFieldLocation::Global | AnalysisFieldLocation::Mode
1713 ) {
1714 return shape.iter().product();
1715 }
1716 if let Some(first_dim) = shape.first().copied() {
1717 if shape.len() > 1 || component_count.is_some() {
1718 return first_dim;
1719 }
1720 }
1721 shape.iter().product()
1722}
1723
1724fn infer_field_family(field_id: &str) -> &str {
1725 field_id
1726 .split_once('.')
1727 .map_or("unknown", |(family, _)| family)
1728}
1729
1730fn infer_field_quantity(field_id: &str) -> &str {
1731 let Some((_, rest)) = field_id.split_once('.') else {
1732 return field_id;
1733 };
1734 let Some((quantity, suffix)) = rest.rsplit_once('.') else {
1735 return rest;
1736 };
1737 if suffix.chars().all(|ch| ch.is_ascii_digit()) {
1738 quantity
1739 } else {
1740 rest
1741 }
1742}
1743
1744fn infer_field_unit(field_id: &str) -> Option<&'static str> {
1745 let normalized = field_id.to_ascii_lowercase();
1746 if normalized.contains("frequency_hz") || normalized.contains("frequency_response") {
1747 return Some("Hz");
1748 }
1749 if normalized.contains("eigenvalue") {
1750 return Some("rad^2/s^2");
1751 }
1752 if normalized.contains("sound_pressure_level_db") {
1753 return Some("dB");
1754 }
1755 if normalized.contains("temperature_gradient") {
1756 return Some("K/m");
1757 }
1758 if normalized.contains("temperature") || normalized.contains("temperature_jump") {
1759 return Some("K");
1760 }
1761 if normalized.contains("electric_potential") {
1762 return Some("V");
1763 }
1764 if normalized.contains("electric_field") {
1765 return Some("V/m");
1766 }
1767 if normalized.contains("electric_flux_density") {
1768 return Some("C/m^2");
1769 }
1770 if normalized.contains("current_density") {
1771 return Some("A/m^2");
1772 }
1773 if normalized.contains("vector_potential") {
1774 return Some("Wb/m");
1775 }
1776 if normalized.contains("magnetic_flux_density") {
1777 return Some("T");
1778 }
1779 if normalized.contains("magnetic_field") {
1780 return Some("A/m");
1781 }
1782 if normalized.contains("poynting_vector")
1783 || normalized.contains("boundary_heat_flux")
1784 || normalized.contains("interface_heat_flux")
1785 || normalized.ends_with(".heat_flux")
1786 || normalized.contains(".heat_flux.")
1787 {
1788 return Some("W/m^2");
1789 }
1790 if normalized.contains("power_loss_density")
1791 || normalized.contains("joule_heat")
1792 || normalized.contains("heat_source")
1793 {
1794 return Some("W/m^3");
1795 }
1796 if normalized.contains("energy_density") {
1797 return Some("J/m^3");
1798 }
1799 if normalized.contains("kinetic_energy")
1800 || normalized.contains("strain_energy")
1801 || normalized.contains("total_strain_energy")
1802 {
1803 return Some("J");
1804 }
1805 if normalized.contains("modal_mass") {
1806 return Some("kg");
1807 }
1808 if normalized.contains("modal_stiffness") {
1809 return Some("N/m");
1810 }
1811 if normalized.contains("wall_shear_stress")
1812 || normalized.contains("contact_pressure")
1813 || normalized.contains("interface_pressure")
1814 || normalized.contains("fluid_pressure")
1815 || normalized.contains(".pressure")
1816 || normalized.contains("stress")
1817 || normalized.contains("traction")
1818 || normalized.contains("von_mises")
1819 {
1820 return Some("Pa");
1821 }
1822 if normalized.contains("reaction_force")
1823 || normalized.contains("beam_axial_force")
1824 || normalized.contains("beam_shear_force")
1825 || normalized.contains("shell_membrane_force")
1826 || normalized.contains("shell_transverse_shear")
1827 {
1828 return Some("N");
1829 }
1830 if normalized.contains("reaction_moment")
1831 || normalized.contains("beam_torsion_moment")
1832 || normalized.contains("beam_bending_moment")
1833 || normalized.contains("shell_bending_moment")
1834 {
1835 return Some("N*m");
1836 }
1837 if normalized.contains("rotation") {
1838 return Some("rad");
1839 }
1840 if normalized.contains("contact_gap")
1841 || normalized.contains("displacement")
1842 || normalized.contains("mode_shape")
1843 {
1844 return Some("m");
1845 }
1846 if normalized.contains("velocity") {
1847 return Some("m/s");
1848 }
1849 if normalized.contains("acceleration") {
1850 return Some("m/s^2");
1851 }
1852 if normalized.contains("strain")
1853 || normalized.contains("residual")
1854 || normalized.contains("equation_scale")
1855 || normalized.contains("load_factor")
1856 || normalized.contains("coupling_iteration_count")
1857 || normalized.contains("reynolds_number")
1858 || normalized.contains("phase")
1859 || normalized.contains("orthogonality")
1860 || normalized.contains("participation_factor")
1861 || normalized.contains("relative_frequency_separation")
1862 {
1863 return Some("1");
1864 }
1865 None
1866}
1867
1868fn infer_field_location(field_id: &str) -> AnalysisFieldLocation {
1869 let normalized = field_id.to_ascii_lowercase();
1870 if normalized.contains("nodal_von_mises") {
1871 return AnalysisFieldLocation::Node;
1872 }
1873 if normalized.contains("vector_potential") {
1874 return AnalysisFieldLocation::Edge;
1875 }
1876 if normalized.contains("wall_shear_stress") || normalized.contains("boundary_heat_flux") {
1877 return AnalysisFieldLocation::BoundaryFace;
1878 }
1879 if normalized.contains("interface_")
1880 || normalized.contains("contact_pressure")
1881 || normalized.contains("contact_gap")
1882 {
1883 return AnalysisFieldLocation::InterfaceFace;
1884 }
1885 if normalized.starts_with("modal.")
1886 && !normalized.starts_with("modal.mode_shape.")
1887 && !normalized.contains("orthogonality")
1888 {
1889 return AnalysisFieldLocation::Mode;
1890 }
1891 if normalized.contains("energy_density") {
1892 return AnalysisFieldLocation::Element;
1893 }
1894 if normalized.contains("residual")
1895 || normalized.contains("equation_scale")
1896 || normalized.contains("energy")
1897 || normalized.contains("load_factor")
1898 || normalized.contains("coupling_iteration_count")
1899 || normalized.contains("orthogonality")
1900 {
1901 return AnalysisFieldLocation::Global;
1902 }
1903 if normalized.starts_with("acoustic.") {
1904 return AnalysisFieldLocation::Node;
1905 }
1906 if normalized.contains("beam_") || normalized.contains("shell_") {
1907 return AnalysisFieldLocation::Element;
1908 }
1909 if normalized.contains("temperature_gradient")
1910 || normalized.contains("heat_flux")
1911 || normalized.contains("heat_source")
1912 || normalized.contains("joule_heat")
1913 || normalized.contains("magnetic_flux_density")
1914 || normalized.contains("magnetic_field")
1915 || normalized.contains("electric_field")
1916 || normalized.contains("electric_flux_density")
1917 || normalized.contains("current_density")
1918 || normalized.contains("poynting_vector")
1919 || normalized.contains("vorticity")
1920 || normalized.contains("pressure")
1921 || normalized.contains("stress")
1922 || normalized.contains("strain")
1923 || normalized.contains("von_mises")
1924 {
1925 return AnalysisFieldLocation::Element;
1926 }
1927 if normalized.contains("displacement")
1928 || normalized.contains("mode_shape")
1929 || normalized.contains("temperature")
1930 || normalized.starts_with("acoustic.")
1931 || normalized.contains("electric_potential")
1932 {
1933 return AnalysisFieldLocation::Node;
1934 }
1935 AnalysisFieldLocation::Element
1936}
1937
1938fn infer_field_topology_id(
1939 field_id: &str,
1940 location: AnalysisFieldLocation,
1941) -> Option<&'static str> {
1942 let normalized = field_id.to_ascii_lowercase();
1943 if location == AnalysisFieldLocation::Global || location == AnalysisFieldLocation::Mode {
1944 return None;
1945 }
1946 if normalized.starts_with("structural.")
1947 || normalized.starts_with("thermo_mechanical.")
1948 || normalized.starts_with("transient.")
1949 || normalized.starts_with("nonlinear.")
1950 {
1951 return Some("analysis_mesh");
1952 }
1953 if normalized.starts_with("thermal.")
1954 || normalized.starts_with("electro_thermal.")
1955 || normalized.starts_with("em.")
1956 || normalized.starts_with("acoustic.")
1957 || normalized.starts_with("cfd.")
1958 || normalized.starts_with("cht.")
1959 || normalized.starts_with("fsi.")
1960 {
1961 return Some("analysis_topology");
1962 }
1963 None
1964}
1965
1966fn infer_field_element_kind(
1967 field_id: &str,
1968 location: AnalysisFieldLocation,
1969) -> Option<&'static str> {
1970 let normalized = field_id.to_ascii_lowercase();
1971 if normalized.contains("beam_") {
1972 return Some("beam2");
1973 }
1974 if normalized.contains("shell_") {
1975 return Some("tri3");
1976 }
1977 if normalized.starts_with("structural.")
1978 && matches!(
1979 location,
1980 AnalysisFieldLocation::Element | AnalysisFieldLocation::BoundaryFace
1981 )
1982 {
1983 return Some("tetrahedron4");
1984 }
1985 None
1986}
1987
1988fn infer_field_kind(field_id: &str, shape: &[usize]) -> AnalysisFieldKind {
1989 let normalized = field_id.to_ascii_lowercase();
1990 if normalized.contains("magnitude") {
1991 return AnalysisFieldKind::Scalar;
1992 }
1993 if normalized.contains("heat_flux") {
1994 return match shape {
1995 [_, components] if (2..=3).contains(components) => AnalysisFieldKind::Vector,
1996 _ => AnalysisFieldKind::Scalar,
1997 };
1998 }
1999 if normalized.contains("beam_shear_force")
2000 || normalized.contains("beam_bending_moment")
2001 || normalized.contains("beam_bending_stress")
2002 || normalized.contains("shell_membrane_force")
2003 || normalized.contains("shell_bending_moment")
2004 || normalized.contains("shell_transverse_shear")
2005 {
2006 return AnalysisFieldKind::Vector;
2007 }
2008 if normalized.contains("shell_von_mises") {
2009 return AnalysisFieldKind::Scalar;
2010 }
2011 if normalized.contains("beam_axial_force")
2012 || normalized.contains("beam_torsion_moment")
2013 || normalized.contains("beam_torsion_stress")
2014 {
2015 return AnalysisFieldKind::Scalar;
2016 }
2017 if normalized.contains("temperature_gradient")
2018 || normalized.contains("wall_shear_stress")
2019 || normalized.contains("vorticity")
2020 || normalized.contains("velocity")
2021 || normalized.contains("traction")
2022 || normalized.contains("magnetic_field")
2023 || normalized.contains("electric_field")
2024 || normalized.contains("current_density")
2025 {
2026 return AnalysisFieldKind::Vector;
2027 }
2028 if normalized.contains("von_mises")
2029 || normalized.contains("equivalent_plastic_strain")
2030 || normalized.contains("pressure")
2031 || normalized.contains("phase")
2032 || normalized.contains("reynolds_number")
2033 || normalized.contains("temperature")
2034 || normalized.contains("energy")
2035 || normalized.contains("residual")
2036 {
2037 return AnalysisFieldKind::Scalar;
2038 }
2039 if normalized.contains("stress") || normalized.contains("strain") {
2040 return AnalysisFieldKind::Tensor;
2041 }
2042 if normalized.contains("orthogonality") {
2043 return AnalysisFieldKind::Tensor;
2044 }
2045 if normalized.contains("displacement")
2046 || normalized.contains("rotation")
2047 || normalized.contains("mode_shape")
2048 || normalized.contains("reaction_force")
2049 || normalized.contains("reaction_moment")
2050 || normalized.contains("vector")
2051 || normalized.contains("flux")
2052 {
2053 return AnalysisFieldKind::Vector;
2054 }
2055 match shape {
2056 [] | [_] => AnalysisFieldKind::Scalar,
2057 [_, 1] => AnalysisFieldKind::Scalar,
2058 [_, 2] | [_, 3] => AnalysisFieldKind::Vector,
2059 [_, _, ..] => AnalysisFieldKind::Tensor,
2060 }
2061}
2062
2063fn infer_component_count(field_id: &str, shape: &[usize]) -> Option<usize> {
2064 let normalized = field_id.to_ascii_lowercase();
2065 if normalized.contains("magnitude") {
2066 return None;
2067 }
2068 if normalized.contains("equivalent_plastic_strain") {
2069 return None;
2070 }
2071 if normalized.contains("heat_flux") {
2072 return match shape {
2073 [_, components] if (2..=3).contains(components) => Some(*components),
2074 _ => None,
2075 };
2076 }
2077 if normalized.contains("beam_shear_force")
2078 || normalized.contains("beam_bending_moment")
2079 || normalized.contains("beam_bending_stress")
2080 {
2081 return Some(2);
2082 }
2083 if normalized.contains("shell_membrane_force") || normalized.contains("shell_bending_moment") {
2084 return Some(3);
2085 }
2086 if normalized.contains("shell_transverse_shear") {
2087 return Some(2);
2088 }
2089 if normalized.contains("shell_von_mises") {
2090 return None;
2091 }
2092 if normalized.contains("beam_axial_force")
2093 || normalized.contains("beam_torsion_moment")
2094 || normalized.contains("beam_torsion_stress")
2095 {
2096 return None;
2097 }
2098 if normalized.contains("temperature_gradient")
2099 || normalized.contains("wall_shear_stress")
2100 || normalized.contains("vorticity")
2101 || normalized.contains("velocity")
2102 || normalized.contains("traction")
2103 || normalized.contains("magnetic_field")
2104 || normalized.contains("electric_field")
2105 || normalized.contains("current_density")
2106 {
2107 return Some(
2108 shape
2109 .last()
2110 .copied()
2111 .filter(|value| (2..=3).contains(value))
2112 .unwrap_or(3),
2113 );
2114 }
2115 if normalized.contains("von_mises")
2116 || normalized.contains("equivalent_plastic_strain")
2117 || normalized.contains("pressure")
2118 || normalized.contains("phase")
2119 || normalized.contains("reynolds_number")
2120 || normalized.contains("temperature")
2121 || normalized.contains("energy")
2122 || normalized.contains("residual")
2123 {
2124 return None;
2125 }
2126 if normalized.contains("displacement")
2127 || normalized.contains("rotation")
2128 || normalized.contains("mode_shape")
2129 || normalized.contains("reaction_force")
2130 || normalized.contains("reaction_moment")
2131 || normalized.contains("particle_velocity")
2132 || normalized.contains("vector")
2133 || normalized.contains("flux")
2134 {
2135 return Some(
2136 shape
2137 .last()
2138 .copied()
2139 .filter(|value| (2..=6).contains(value))
2140 .unwrap_or(3),
2141 );
2142 }
2143 if normalized.contains("stress") || normalized.contains("strain") {
2144 return Some(
2145 shape
2146 .last()
2147 .copied()
2148 .filter(|value| *value == 6)
2149 .unwrap_or(6),
2150 );
2151 }
2152 if normalized.contains("orthogonality") {
2153 return None;
2154 }
2155 match shape {
2156 [_, count] if (1..=6).contains(count) => Some(*count),
2157 _ => None,
2158 }
2159}
2160
2161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2162pub struct AnalysisResultsSummary {
2163 pub field_count: usize,
2164 pub total_elements: usize,
2165 pub mode_count: usize,
2166 pub available_mode_indices: Vec<usize>,
2167 pub min_frequency_hz: Option<f64>,
2168 pub max_frequency_hz: Option<f64>,
2169 pub max_modal_residual_norm: Option<f64>,
2170 pub first_mode_converged: Option<bool>,
2171 pub snapshot_count: usize,
2172 pub time_start_s: Option<f64>,
2173 pub time_end_s: Option<f64>,
2174 pub max_transient_residual_norm: Option<f64>,
2175 pub final_step_converged: Option<bool>,
2176 pub increment_count: usize,
2177 pub failed_increment_count: Option<usize>,
2178 pub max_nonlinear_residual_norm: Option<f64>,
2179 pub max_nonlinear_increment_norm: Option<f64>,
2180 pub max_nonlinear_iteration_count: Option<usize>,
2181 pub final_increment_converged: Option<bool>,
2182 pub nonlinear_line_search_backtracks: Option<usize>,
2183 pub nonlinear_max_backtracks_per_increment: Option<usize>,
2184 pub nonlinear_tangent_rebuild_count: Option<usize>,
2185 pub nonlinear_iteration_spike_count: Option<usize>,
2186 pub nonlinear_convergence_stall_count: Option<usize>,
2187 pub nonlinear_backtrack_burst_count: Option<usize>,
2188 pub prep_calibration_profile: Option<String>,
2189 pub prep_calibration_fingerprint: Option<u64>,
2190 pub prep_acceptance_score: Option<f64>,
2191 pub prep_acceptance_passed: Option<bool>,
2192 pub prep_acceptance_fingerprint: Option<u64>,
2193 pub thermo_coupling_enabled: Option<bool>,
2194 pub thermo_coupling_fingerprint: Option<u64>,
2195 pub thermo_constitutive_temperature_factor: Option<f64>,
2196 pub thermo_effective_modulus_scale: Option<f64>,
2197 pub thermo_constitutive_material_spread_ratio: Option<f64>,
2198 pub thermo_assignment_heterogeneity_index: Option<f64>,
2199 pub thermo_region_delta_count: Option<f64>,
2200 pub thermo_spatial_coverage_ratio: Option<f64>,
2201 pub thermo_field_extrapolation_ratio: Option<f64>,
2202 pub thermo_field_clamp_ratio: Option<f64>,
2203 pub thermo_transient_severity: Option<f64>,
2204 pub thermo_nonlinear_severity: Option<f64>,
2205 pub electro_thermal_coupling_enabled: Option<bool>,
2206 pub electro_thermal_coupling_fingerprint: Option<u64>,
2207 pub electro_joule_heating_scale: Option<f64>,
2208 pub electro_conductivity_spread_ratio: Option<f64>,
2209 pub electro_transient_severity: Option<f64>,
2210 pub electro_transient_time_scale_mean: Option<f64>,
2211 pub electro_nonlinear_severity: Option<f64>,
2212 pub electro_nonlinear_time_scale_mean: Option<f64>,
2213 pub plastic_nonlinear_severity: Option<f64>,
2214 pub plastic_nonlinear_severity_mean: Option<f64>,
2215 pub plastic_load_realization_ratio: Option<f64>,
2216 pub plastic_load_amplification_ratio: Option<f64>,
2217 pub contact_nonlinear_severity: Option<f64>,
2218 pub contact_nonlinear_severity_mean: Option<f64>,
2219 pub contact_load_realization_ratio: Option<f64>,
2220 pub contact_load_amplification_ratio: Option<f64>,
2221 pub thermal_max_residual_norm: Option<f64>,
2222 pub thermal_min_temperature_k: Option<f64>,
2223 pub thermal_max_temperature_k: Option<f64>,
2224 pub thermal_conductivity_spread_ratio: Option<f64>,
2225 pub thermal_heat_capacity_spread_ratio: Option<f64>,
2226 pub thermal_spatial_gradient_index: Option<f64>,
2227 pub thermal_monotonic_response_fraction: Option<f64>,
2228 pub thermal_response_realization_ratio: Option<f64>,
2229 pub electromagnetic_enabled: Option<bool>,
2230 pub electromagnetic_formulation_coverage_ratio: Option<f64>,
2231 pub electromagnetic_magnetostatic_curl_curl_coverage_ratio: Option<f64>,
2232 pub electromagnetic_magnetoquasistatic_eddy_current_coverage_ratio: Option<f64>,
2233 pub electromagnetic_full_wave_displacement_current_coverage_ratio: Option<f64>,
2234 pub electromagnetic_displacement_to_conduction_ratio: Option<f64>,
2235 pub electromagnetic_material_frequency_response_coverage_ratio: Option<f64>,
2236 pub electromagnetic_reference_frequency_hz: Option<f64>,
2237 pub electromagnetic_applied_current_a: Option<f64>,
2238 pub electromagnetic_solve_quality: Option<f64>,
2239 pub electromagnetic_conductivity_spread_ratio: Option<f64>,
2240 pub electromagnetic_relative_permittivity_spread_ratio: Option<f64>,
2241 pub electromagnetic_relative_permeability_spread_ratio: Option<f64>,
2242 pub electromagnetic_material_heterogeneity_index: Option<f64>,
2243 pub electromagnetic_assignment_coverage_ratio: Option<f64>,
2244 pub electromagnetic_assigned_coefficient_coverage_ratio: Option<f64>,
2245 pub electromagnetic_region_coefficient_contrast_index: Option<f64>,
2246 pub electromagnetic_condition_number_estimate: Option<f64>,
2247 pub electromagnetic_source_realization_ratio: Option<f64>,
2248 pub electromagnetic_source_region_coverage_ratio: Option<f64>,
2249 pub electromagnetic_source_material_alignment_ratio: Option<f64>,
2250 pub electromagnetic_source_localization_ratio: Option<f64>,
2251 pub electromagnetic_source_overlap_ratio: Option<f64>,
2252 pub electromagnetic_source_interference_index: Option<f64>,
2253 pub electromagnetic_boundary_anchor_ratio: Option<f64>,
2254 pub electromagnetic_boundary_condition_localization_ratio: Option<f64>,
2255 pub electromagnetic_ground_anchor_effectiveness_ratio: Option<f64>,
2256 pub electromagnetic_insulation_leakage_ratio: Option<f64>,
2257 pub electromagnetic_flux_divergence_ratio: Option<f64>,
2258 pub electromagnetic_energy_imbalance_ratio: Option<f64>,
2259 pub electromagnetic_boundary_energy_ratio: Option<f64>,
2260 pub electromagnetic_boundary_penalty_conditioning_contribution: Option<f64>,
2261 pub electromagnetic_source_region_energy_consistency_ratio: Option<f64>,
2262 pub electromagnetic_real_residual_norm: Option<f64>,
2263 pub electromagnetic_imag_residual_norm: Option<f64>,
2264 pub electromagnetic_sweep_count: Option<f64>,
2265 pub electromagnetic_resonance_peak_frequency_hz: Option<f64>,
2266 pub electromagnetic_resonance_peak_flux_density: Option<f64>,
2267 pub electromagnetic_resonance_bandwidth_hz: Option<f64>,
2268 pub electromagnetic_resonance_quality_factor: Option<f64>,
2269 pub electromagnetic_resonance_flux_gain: Option<f64>,
2270}
2271
2272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2273pub struct AnalysisResultsData {
2274 #[serde(default)]
2275 pub field_descriptors: Vec<AnalysisFieldDescriptor>,
2276 pub fields: Vec<AnalysisField>,
2277 pub modal_results: Option<ModalResultsData>,
2278 #[serde(default)]
2279 pub thermal_results: Option<ThermalResultsData>,
2280 pub transient_results: Option<TransientResultsData>,
2281 pub nonlinear_results: Option<NonlinearResultsData>,
2282 #[serde(default)]
2283 pub electromagnetic_results: Option<ElectromagneticResultsData>,
2284 pub diagnostics: Option<Vec<FeaDiagnostic>>,
2285 pub run_status: RunStatus,
2286 pub publishable: bool,
2287 pub quality_reasons: Vec<QualityReason>,
2288 pub provenance: RunProvenance,
2289 pub summary: AnalysisResultsSummary,
2290}
2291
2292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2293pub struct AnalysisResultsCompareQuery {
2294 pub baseline_run_id: String,
2295 pub candidate_run_id: String,
2296}
2297
2298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2299pub struct AnalysisResultsCompareData {
2300 pub baseline_run_id: String,
2301 pub candidate_run_id: String,
2302 pub publishable_changed: bool,
2303 pub run_status_changed: bool,
2304 pub quality_reason_count_delta: i64,
2305 pub failed_increment_delta: Option<i64>,
2306 pub max_iteration_delta: Option<i64>,
2307 pub nonlinear_spike_count_delta: Option<i64>,
2308 pub nonlinear_stall_count_delta: Option<i64>,
2309 pub solve_ms_delta: Option<f64>,
2310}
2311
2312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2313pub struct AnalysisTrendsQuery {
2314 pub window_size: usize,
2315}
2316
2317impl Default for AnalysisTrendsQuery {
2318 fn default() -> Self {
2319 Self { window_size: 16 }
2320 }
2321}
2322
2323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2324pub struct AnalysisOutputRequest {
2325 pub id: String,
2326 pub field_id: String,
2327 #[serde(default, skip_serializing_if = "Option::is_none")]
2328 pub location: Option<String>,
2329 #[serde(default, skip_serializing_if = "Option::is_none")]
2330 pub kind: Option<String>,
2331}
2332
2333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2334pub struct AnalysisStudySpec {
2335 pub study_id: String,
2336 pub geometry: GeometryAsset,
2337 pub create_model_intent: AnalysisCreateModelIntentSpec,
2338 #[serde(default)]
2339 pub model: Option<AnalysisModel>,
2340 pub run_kind: AnalysisRunKind,
2341 pub backend: ComputeBackend,
2342 #[serde(default)]
2343 pub mesh_options: Option<VolumeMeshingOptions>,
2344 #[serde(default)]
2345 pub outputs: Vec<AnalysisOutputRequest>,
2346 #[serde(default, skip_serializing_if = "Option::is_none")]
2347 pub analysis_mesh_artifact_path: Option<String>,
2348 #[serde(default, skip_serializing_if = "Option::is_none")]
2349 pub analysis_mesh_evidence_artifact_path: Option<String>,
2350 #[serde(default)]
2351 pub linear_static_run_options: Option<AnalysisRunOptions>,
2352 #[serde(default)]
2353 pub modal_run_options: Option<AnalysisModalRunOptions>,
2354 #[serde(default)]
2355 pub acoustic_run_options: Option<AnalysisAcousticRunOptions>,
2356 #[serde(default)]
2357 pub thermal_run_options: Option<AnalysisThermalRunOptions>,
2358 #[serde(default)]
2359 pub transient_run_options: Option<AnalysisTransientRunOptions>,
2360 #[serde(default)]
2361 pub cfd_run_options: Option<AnalysisCfdRunOptions>,
2362 #[serde(default)]
2363 pub cht_run_options: Option<AnalysisChtRunOptions>,
2364 #[serde(default)]
2365 pub fsi_run_options: Option<AnalysisFsiRunOptions>,
2366 #[serde(default)]
2367 pub nonlinear_run_options: Option<AnalysisNonlinearRunOptions>,
2368 #[serde(default)]
2369 pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
2370}
2371
2372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2373pub struct AnalysisStudyDiagramObservation {
2374 #[serde(default, skip_serializing_if = "Option::is_none")]
2375 pub artifact_path: Option<String>,
2376 #[serde(default, skip_serializing_if = "Option::is_none")]
2377 pub source_mime_type: Option<String>,
2378 #[serde(default, skip_serializing_if = "Option::is_none")]
2379 pub summary: Option<String>,
2380 #[serde(default, skip_serializing_if = "Option::is_none")]
2381 pub material_region_id: Option<String>,
2382 #[serde(default, skip_serializing_if = "Option::is_none")]
2383 pub boundary_condition_region_id: Option<String>,
2384 #[serde(default, skip_serializing_if = "Option::is_none")]
2385 pub driving_condition_region_id: Option<String>,
2386 #[serde(default, skip_serializing_if = "Option::is_none")]
2387 pub structural_force_n: Option<[f64; 3]>,
2388 #[serde(default, skip_serializing_if = "Option::is_none")]
2389 pub confidence: Option<f64>,
2390}
2391
2392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2393pub struct AnalysisStudyAuthoringIntent {
2394 pub study_id: String,
2395 #[serde(default)]
2396 pub model_id: Option<String>,
2397 pub geometry: GeometryAsset,
2398 pub mesh_authoring_summary: MeshAuthoringSummary,
2399 pub profile: AnalysisCreateModelProfile,
2400 pub run_kind: AnalysisRunKind,
2401 pub backend: ComputeBackend,
2402 #[serde(default)]
2403 pub analysis_mesh_artifact_path: Option<String>,
2404 #[serde(default)]
2405 pub analysis_mesh_evidence_artifact_path: Option<String>,
2406 #[serde(default)]
2407 pub material_region_id: Option<String>,
2408 #[serde(default)]
2409 pub boundary_condition_region_id: Option<String>,
2410 #[serde(default)]
2411 pub driving_condition_region_id: Option<String>,
2412 #[serde(default)]
2413 pub structural_force_n: Option<[f64; 3]>,
2414 #[serde(default, skip_serializing_if = "Option::is_none")]
2415 pub diagram_observation: Option<AnalysisStudyDiagramObservation>,
2416}
2417
2418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2419pub struct AnalysisStudyAuthoringData {
2420 pub study: AnalysisStudySpec,
2421 pub evidence: AnalysisStudyAuthoringEvidence,
2422 pub evidence_artifact_path: String,
2423}
2424
2425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2426pub struct AnalysisStudyAuthoringEvidence {
2427 pub schema_version: String,
2428 pub mesh_id: String,
2429 pub mesh_authoring_summary_schema_version: String,
2430 #[serde(default)]
2431 pub tetrahedron_generation_family: String,
2432 #[serde(default)]
2433 pub tetrahedron_generation_attempted_family_count: usize,
2434 #[serde(default)]
2435 pub tetrahedron_generation_rejected_family_count: usize,
2436 #[serde(default)]
2437 pub tetrahedron_generation_selected_family_index: usize,
2438 #[serde(default)]
2439 pub tetrahedron_generation_interior_support_candidate_count: usize,
2440 #[serde(default)]
2441 pub tetrahedron_generation_interior_support_accepted_count: usize,
2442 #[serde(
2443 default,
2444 skip_serializing_if = "MeshAuthoringNestedTetrahedronShellSummary::is_empty"
2445 )]
2446 pub nested_tetrahedron_shell: MeshAuthoringNestedTetrahedronShellSummary,
2447 pub selected_material_region_id: String,
2448 #[serde(default, skip_serializing_if = "Option::is_none")]
2449 pub selected_boundary_condition_region_id: Option<String>,
2450 #[serde(default, skip_serializing_if = "Option::is_none")]
2451 pub selected_driving_condition_region_id: Option<String>,
2452 #[serde(default, skip_serializing_if = "Option::is_none")]
2453 pub selected_driving_condition_kind: Option<String>,
2454 #[serde(default, skip_serializing_if = "Option::is_none")]
2455 pub selected_structural_force_n: Option<[f64; 3]>,
2456 #[serde(default, skip_serializing_if = "Option::is_none")]
2457 pub diagram_artifact_path: Option<String>,
2458 #[serde(default, skip_serializing_if = "Option::is_none")]
2459 pub diagram_source_mime_type: Option<String>,
2460 #[serde(default, skip_serializing_if = "Option::is_none")]
2461 pub diagram_summary: Option<String>,
2462 #[serde(default, skip_serializing_if = "Option::is_none")]
2463 pub diagram_confidence: Option<f64>,
2464 #[serde(default, skip_serializing_if = "Option::is_none")]
2465 pub analysis_mesh_artifact_path: Option<String>,
2466 #[serde(default, skip_serializing_if = "Option::is_none")]
2467 pub analysis_mesh_evidence_artifact_path: Option<String>,
2468 pub material_region_source: String,
2469 #[serde(default, skip_serializing_if = "Option::is_none")]
2470 pub boundary_condition_region_source: Option<String>,
2471 #[serde(default, skip_serializing_if = "Option::is_none")]
2472 pub driving_condition_region_source: Option<String>,
2473}
2474
2475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2476pub struct AnalysisStudyValidateResult {
2477 pub valid: bool,
2478 pub issue_codes: Vec<String>,
2479 #[serde(default)]
2480 pub issues: Vec<AnalysisStudyIssue>,
2481 pub evidence_artifact_path: String,
2482}
2483
2484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2485pub struct AnalysisStudyIssue {
2486 pub code: String,
2487 pub message: String,
2488}
2489
2490#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2491pub struct AnalysisStudyPlanData {
2492 pub study_id: String,
2493 pub model_id: String,
2494 pub run_kind: AnalysisRunKind,
2495 pub backend: ComputeBackend,
2496 #[serde(default)]
2497 pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
2498 #[serde(default)]
2499 pub run_options: serde_json::Value,
2500 pub operation_sequence: Vec<String>,
2501 pub run_operation: String,
2502 pub run_op_version: String,
2503 pub study_fingerprint: String,
2504 pub evidence_artifact_path: String,
2505}
2506
2507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2508pub struct AnalysisStudyRunData {
2509 pub study_id: String,
2510 pub model_id: String,
2511 pub model_profile: AnalysisCreateModelProfile,
2512 pub run_kind: AnalysisRunKind,
2513 pub backend: ComputeBackend,
2514 #[serde(default)]
2515 pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
2516 #[serde(default)]
2517 pub run_options: serde_json::Value,
2518 #[serde(default)]
2519 pub prep_artifact_id: Option<String>,
2520 #[serde(default)]
2521 pub analysis_mesh_artifact_path: Option<String>,
2522 #[serde(default)]
2523 pub analysis_mesh_evidence_artifact_path: Option<String>,
2524 #[serde(default)]
2525 pub refined_analysis_mesh_artifact_path: Option<String>,
2526 #[serde(default)]
2527 pub refined_analysis_mesh_evidence_artifact_path: Option<String>,
2528 pub study_fingerprint: String,
2529 pub operation_sequence: Vec<String>,
2530 pub run_operation: String,
2531 pub run_op_version: String,
2532 pub run_id: String,
2533 pub run_status: RunStatus,
2534 pub publishable: bool,
2535 pub solver_convergence: QualityGate,
2536 pub result_quality: QualityGate,
2537 #[serde(default)]
2538 pub quality_reasons: Vec<QualityReason>,
2539 pub provenance: RunProvenance,
2540 pub evidence_artifact_path: String,
2541}
2542
2543#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2544pub struct AnalysisStudySweepSpec {
2545 pub sweep_id: String,
2546 pub studies: Vec<AnalysisStudySpec>,
2547 #[serde(default = "default_study_sweep_fail_fast")]
2548 pub fail_fast: bool,
2549}
2550
2551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2552pub struct AnalysisStudySweepValidateEntry {
2553 pub study_id: String,
2554 pub valid: bool,
2555 pub issue_codes: Vec<String>,
2556 #[serde(default)]
2557 pub issues: Vec<AnalysisStudyIssue>,
2558}
2559
2560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2561pub struct AnalysisStudySweepValidateData {
2562 pub sweep_id: String,
2563 pub valid: bool,
2564 pub issue_codes: Vec<String>,
2565 pub study_entries: Vec<AnalysisStudySweepValidateEntry>,
2566 pub evidence_artifact_path: String,
2567}
2568
2569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2570pub struct AnalysisStudySweepPlanEntry {
2571 pub study_id: String,
2572 pub model_id: String,
2573 pub run_kind: AnalysisRunKind,
2574 pub backend: ComputeBackend,
2575 #[serde(default)]
2576 pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
2577 #[serde(default)]
2578 pub run_options: serde_json::Value,
2579 pub operation_sequence: Vec<String>,
2580 pub run_operation: String,
2581 pub run_op_version: String,
2582 pub study_fingerprint: String,
2583}
2584
2585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2586pub struct AnalysisStudySweepPlanData {
2587 pub sweep_id: String,
2588 pub study_count: usize,
2589 pub planned_count: usize,
2590 pub failed_count: usize,
2591 #[serde(default)]
2592 pub failure_entries: Vec<AnalysisStudySweepFailureEntry>,
2593 pub plan_entries: Vec<AnalysisStudySweepPlanEntry>,
2594 pub evidence_artifact_path: String,
2595}
2596
2597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2598pub struct AnalysisStudySweepRunEntry {
2599 pub study_id: String,
2600 pub run_kind: AnalysisRunKind,
2601 pub run_id: String,
2602 pub run_status: RunStatus,
2603 pub publishable: bool,
2604 pub run_operation: String,
2605 pub run_op_version: String,
2606}
2607
2608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2609pub struct AnalysisStudySweepData {
2610 pub sweep_id: String,
2611 pub study_count: usize,
2612 pub success_count: usize,
2613 pub failed_count: usize,
2614 #[serde(default)]
2615 pub failure_entries: Vec<AnalysisStudySweepFailureEntry>,
2616 pub run_entries: Vec<AnalysisStudySweepRunEntry>,
2617 pub evidence_artifact_path: String,
2618}
2619
2620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2621pub struct AnalysisStudySweepFailureEntry {
2622 pub study_id: String,
2623 pub study_index: usize,
2624 pub error_code: String,
2625 pub message: String,
2626}
2627
2628fn default_study_sweep_fail_fast() -> bool {
2629 true
2630}
2631
2632#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2633#[serde(rename_all = "snake_case")]
2634pub enum AnalysisRunKind {
2635 LinearStatic,
2636 Modal,
2637 Acoustic,
2638 Thermal,
2639 Transient,
2640 Cfd,
2641 Cht,
2642 Fsi,
2643 Nonlinear,
2644 Electromagnetic,
2645}
2646
2647impl AnalysisCreateModelProfile {
2648 pub fn from_snake_case(value: &str) -> Option<Self> {
2649 serde_json::from_value(JsonValue::String(value.trim().to_ascii_lowercase())).ok()
2650 }
2651
2652 pub fn as_snake_case(self) -> &'static str {
2653 match self {
2654 AnalysisCreateModelProfile::LinearStaticStructural => "linear_static_structural",
2655 AnalysisCreateModelProfile::ThermoMechanicalCoupled => "thermo_mechanical_coupled",
2656 AnalysisCreateModelProfile::ElectroThermalCoupled => "electro_thermal_coupled",
2657 AnalysisCreateModelProfile::ThermalStandalone => "thermal_standalone",
2658 AnalysisCreateModelProfile::ModalStructural => "modal_structural",
2659 AnalysisCreateModelProfile::AcousticHarmonic => "acoustic_harmonic",
2660 AnalysisCreateModelProfile::TransientStructural => "transient_structural",
2661 AnalysisCreateModelProfile::NonlinearStructural => "nonlinear_structural",
2662 AnalysisCreateModelProfile::ElectromagneticStatic => "electromagnetic_static",
2663 AnalysisCreateModelProfile::CfdSteadyState => "cfd_steady_state",
2664 AnalysisCreateModelProfile::CfdTransient => "cfd_transient",
2665 AnalysisCreateModelProfile::ChtCoupled => "cht_coupled",
2666 AnalysisCreateModelProfile::FsiCoupled => "fsi_coupled",
2667 }
2668 }
2669
2670 pub fn catalog_entry(self) -> &'static AnalysisPhysicsProfileCatalogEntry {
2671 ANALYSIS_PHYSICS_PROFILE_CATALOG
2672 .iter()
2673 .find(|entry| entry.profile == self)
2674 .expect("analysis profile catalog must contain every profile variant")
2675 }
2676
2677 pub fn derived_run_kind(self) -> AnalysisRunKind {
2678 debug_assert!(
2679 ANALYSIS_PHYSICS_PROFILE_CATALOG
2680 .iter()
2681 .any(|entry| entry.profile == self),
2682 "analysis profile catalog is missing {:?}",
2683 self
2684 );
2685 match self {
2686 AnalysisCreateModelProfile::LinearStaticStructural => AnalysisRunKind::LinearStatic,
2687 AnalysisCreateModelProfile::ThermoMechanicalCoupled => AnalysisRunKind::Transient,
2688 AnalysisCreateModelProfile::ElectroThermalCoupled => AnalysisRunKind::Transient,
2689 AnalysisCreateModelProfile::ThermalStandalone => AnalysisRunKind::Thermal,
2690 AnalysisCreateModelProfile::ModalStructural => AnalysisRunKind::Modal,
2691 AnalysisCreateModelProfile::AcousticHarmonic => AnalysisRunKind::Acoustic,
2692 AnalysisCreateModelProfile::TransientStructural => AnalysisRunKind::Transient,
2693 AnalysisCreateModelProfile::NonlinearStructural => AnalysisRunKind::Nonlinear,
2694 AnalysisCreateModelProfile::ElectromagneticStatic => AnalysisRunKind::Electromagnetic,
2695 AnalysisCreateModelProfile::CfdSteadyState
2696 | AnalysisCreateModelProfile::CfdTransient => AnalysisRunKind::Cfd,
2697 AnalysisCreateModelProfile::ChtCoupled => AnalysisRunKind::Cht,
2698 AnalysisCreateModelProfile::FsiCoupled => AnalysisRunKind::Fsi,
2699 }
2700 }
2701}
2702
2703impl AnalysisRunKind {
2704 pub const fn as_snake_case(self) -> &'static str {
2705 match self {
2706 AnalysisRunKind::LinearStatic => "linear_static",
2707 AnalysisRunKind::Modal => "modal",
2708 AnalysisRunKind::Acoustic => "acoustic",
2709 AnalysisRunKind::Thermal => "thermal",
2710 AnalysisRunKind::Transient => "transient",
2711 AnalysisRunKind::Cfd => "cfd",
2712 AnalysisRunKind::Cht => "cht",
2713 AnalysisRunKind::Fsi => "fsi",
2714 AnalysisRunKind::Nonlinear => "nonlinear",
2715 AnalysisRunKind::Electromagnetic => "electromagnetic",
2716 }
2717 }
2718}
2719
2720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2721pub struct AnalysisTrendKindSummary {
2722 pub run_kind: AnalysisRunKind,
2723 pub sample_count: usize,
2724 pub median_solve_ms: Option<f64>,
2725 pub p95_solve_ms: Option<f64>,
2726 pub publishable_rate: f64,
2727 pub failed_increment_rate: Option<f64>,
2728 pub mean_spike_count: Option<f64>,
2729 pub mean_stall_count: Option<f64>,
2730 pub prep_acceptance_rate: Option<f64>,
2731 pub prep_calibration_fast_rate: Option<f64>,
2732 pub prep_calibration_balanced_rate: Option<f64>,
2733 pub prep_calibration_conservative_rate: Option<f64>,
2734 pub thermo_coupling_enabled_rate: Option<f64>,
2735 pub thermo_transient_warn_rate: Option<f64>,
2736 pub thermo_nonlinear_warn_rate: Option<f64>,
2737 pub thermo_spread_breach_rate: Option<f64>,
2738 pub thermo_heterogeneity_breach_rate: Option<f64>,
2739 pub electro_thermal_coupling_enabled_rate: Option<f64>,
2740 pub electro_transient_warn_rate: Option<f64>,
2741 pub electro_nonlinear_warn_rate: Option<f64>,
2742 pub plastic_nonlinear_warn_rate: Option<f64>,
2743 pub contact_nonlinear_warn_rate: Option<f64>,
2744 pub thermal_stability_warn_rate: Option<f64>,
2745 pub thermal_constitutive_warn_rate: Option<f64>,
2746 pub thermal_spread_breach_rate: Option<f64>,
2747 pub electromagnetic_solve_warn_rate: Option<f64>,
2748 pub electromagnetic_spread_breach_rate: Option<f64>,
2749 pub electromagnetic_heterogeneity_breach_rate: Option<f64>,
2750 pub electromagnetic_coverage_breach_rate: Option<f64>,
2751 pub electromagnetic_contrast_breach_rate: Option<f64>,
2752 pub electromagnetic_conditioning_breach_rate: Option<f64>,
2753 pub electromagnetic_source_realization_breach_rate: Option<f64>,
2754 pub electromagnetic_source_region_coverage_breach_rate: Option<f64>,
2755 pub electromagnetic_source_material_alignment_breach_rate: Option<f64>,
2756 pub electromagnetic_source_overlap_breach_rate: Option<f64>,
2757 pub electromagnetic_source_interference_breach_rate: Option<f64>,
2758 pub electromagnetic_boundary_anchor_breach_rate: Option<f64>,
2759 pub electromagnetic_boundary_localization_breach_rate: Option<f64>,
2760 pub electromagnetic_ground_effectiveness_breach_rate: Option<f64>,
2761 pub electromagnetic_insulation_leakage_breach_rate: Option<f64>,
2762 pub electromagnetic_divergence_breach_rate: Option<f64>,
2763 pub electromagnetic_energy_imbalance_breach_rate: Option<f64>,
2764 pub electromagnetic_boundary_energy_breach_rate: Option<f64>,
2765 pub electromagnetic_boundary_penalty_contribution_breach_rate: Option<f64>,
2766 pub electromagnetic_source_region_energy_consistency_breach_rate: Option<f64>,
2767 pub electromagnetic_real_residual_breach_rate: Option<f64>,
2768 pub electromagnetic_imag_residual_breach_rate: Option<f64>,
2769 pub electromagnetic_sweep_coverage_breach_rate: Option<f64>,
2770 pub electromagnetic_resonance_sharpness_breach_rate: Option<f64>,
2771}
2772
2773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2774pub struct AnalysisTrendsData {
2775 pub window_size: usize,
2776 pub summaries: Vec<AnalysisTrendKindSummary>,
2777}
2778
2779#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2780pub struct ModalResultsData {
2781 pub modal_payload_version: String,
2782 pub eigenvalues_hz: Vec<f64>,
2783 pub mode_shapes: Vec<AnalysisField>,
2784 pub residual_norms: Vec<f64>,
2785 pub mode_units: ModalFrequencyUnits,
2786 pub frequency_basis: ModalFrequencyBasis,
2787}
2788
2789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2790pub struct ThermalResultsData {
2791 pub thermal_payload_version: String,
2792 pub time_points_s: Vec<f64>,
2793 pub temperature_snapshots: Vec<AnalysisField>,
2794 #[serde(default)]
2795 pub temperature_gradient_snapshots: Vec<AnalysisField>,
2796 #[serde(default)]
2797 pub heat_flux_snapshots: Vec<AnalysisField>,
2798 #[serde(default)]
2799 pub heat_source_snapshots: Vec<AnalysisField>,
2800 #[serde(default)]
2801 pub boundary_heat_flux_snapshots: Vec<AnalysisField>,
2802 pub residual_norms: Vec<f64>,
2803 pub reference_temperature_k: f64,
2804}
2805
2806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2807pub struct TransientResultsData {
2808 pub transient_payload_version: String,
2809 pub time_points_s: Vec<f64>,
2810 pub displacement_snapshots: Vec<AnalysisField>,
2811 #[serde(default)]
2812 pub rotation_snapshots: Vec<AnalysisField>,
2813 #[serde(default)]
2814 pub velocity_snapshots: Vec<AnalysisField>,
2815 #[serde(default)]
2816 pub angular_velocity_snapshots: Vec<AnalysisField>,
2817 #[serde(default)]
2818 pub acceleration_snapshots: Vec<AnalysisField>,
2819 #[serde(default)]
2820 pub angular_acceleration_snapshots: Vec<AnalysisField>,
2821 #[serde(default)]
2822 pub von_mises_snapshots: Vec<AnalysisField>,
2823 #[serde(default)]
2824 pub kinetic_energy_snapshots: Vec<AnalysisField>,
2825 #[serde(default)]
2826 pub strain_energy_snapshots: Vec<AnalysisField>,
2827 #[serde(default)]
2828 pub residual_norm_snapshots: Vec<AnalysisField>,
2829 #[serde(default)]
2830 pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
2831 #[serde(default)]
2832 pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
2833 #[serde(default)]
2834 pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
2835 #[serde(default)]
2836 pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
2837 #[serde(default)]
2838 pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
2839 #[serde(default)]
2840 pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
2841 #[serde(default)]
2842 pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
2843 #[serde(default)]
2844 pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
2845 pub residual_norms: Vec<f64>,
2846 pub integration_method: TransientIntegrationMethod,
2847}
2848
2849#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2850pub struct NonlinearResultsData {
2851 pub nonlinear_payload_version: String,
2852 pub load_factors: Vec<f64>,
2853 pub displacement_snapshots: Vec<AnalysisField>,
2854 #[serde(default)]
2855 pub rotation_snapshots: Vec<AnalysisField>,
2856 #[serde(default)]
2857 pub von_mises_snapshots: Vec<AnalysisField>,
2858 #[serde(default)]
2859 pub plastic_strain_snapshots: Vec<AnalysisField>,
2860 #[serde(default)]
2861 pub equivalent_plastic_strain_snapshots: Vec<AnalysisField>,
2862 #[serde(default)]
2863 pub contact_pressure_snapshots: Vec<AnalysisField>,
2864 #[serde(default)]
2865 pub contact_gap_snapshots: Vec<AnalysisField>,
2866 #[serde(default)]
2867 pub load_factor_snapshots: Vec<AnalysisField>,
2868 #[serde(default)]
2869 pub residual_norm_snapshots: Vec<AnalysisField>,
2870 #[serde(default)]
2871 pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
2872 #[serde(default)]
2873 pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
2874 #[serde(default)]
2875 pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
2876 #[serde(default)]
2877 pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
2878 #[serde(default)]
2879 pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
2880 #[serde(default)]
2881 pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
2882 #[serde(default)]
2883 pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
2884 #[serde(default)]
2885 pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
2886 pub residual_norms: Vec<f64>,
2887 #[serde(default)]
2888 pub increment_norms: Vec<f64>,
2889 #[serde(default)]
2890 pub iteration_counts: Vec<usize>,
2891 #[serde(default)]
2892 pub failed_increments: usize,
2893 #[serde(default)]
2894 pub line_search_backtracks: usize,
2895 #[serde(default)]
2896 pub max_line_search_backtracks_per_increment: usize,
2897 #[serde(default)]
2898 pub tangent_rebuild_count: usize,
2899 #[serde(default)]
2900 pub iteration_spike_count: usize,
2901 #[serde(default)]
2902 pub convergence_stall_count: usize,
2903 #[serde(default)]
2904 pub backtrack_burst_count: usize,
2905 pub method: NonlinearMethod,
2906}
2907
2908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2909pub struct ElectromagneticResultsData {
2910 pub electromagnetic_payload_version: String,
2911 pub reference_frequency_hz: f64,
2912 pub applied_current_a: f64,
2913 pub vector_potential_real: AnalysisField,
2914 pub vector_potential_imag: AnalysisField,
2915 pub magnetic_flux_density_real: AnalysisField,
2916 pub magnetic_flux_density_imag: AnalysisField,
2917 pub magnetic_flux_density_magnitude: AnalysisField,
2918 pub magnetic_field_real: AnalysisField,
2919 pub magnetic_field_imag: AnalysisField,
2920 pub current_density_real: AnalysisField,
2921 pub current_density_imag: AnalysisField,
2922 pub electric_field_real: AnalysisField,
2923 pub electric_field_imag: AnalysisField,
2924 pub power_loss_density: AnalysisField,
2925 pub energy_density: AnalysisField,
2926 pub residual_real: AnalysisField,
2927 pub residual_imag: AnalysisField,
2928 pub electric_flux_density_real: AnalysisField,
2929 pub electric_flux_density_imag: AnalysisField,
2930 pub poynting_vector_real: AnalysisField,
2931 pub poynting_vector_imag: AnalysisField,
2932 #[serde(default)]
2933 pub sweep_frequency_hz: Vec<f64>,
2934 #[serde(default)]
2935 pub sweep_peak_flux_density: Vec<f64>,
2936 #[serde(default)]
2937 pub sweep_solve_quality: Vec<f64>,
2938 #[serde(default)]
2939 pub resonance_peak_frequency_hz: Option<f64>,
2940 #[serde(default)]
2941 pub resonance_peak_flux_density: Option<f64>,
2942 #[serde(default)]
2943 pub resonance_bandwidth_hz: Option<f64>,
2944 #[serde(default)]
2945 pub resonance_quality_factor: Option<f64>,
2946 #[serde(default)]
2947 pub resonance_flux_gain: Option<f64>,
2948}
2949
2950#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2951#[serde(rename_all = "snake_case")]
2952pub enum TransientIntegrationMethod {
2953 ImplicitEuler,
2954}
2955
2956#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2957#[serde(rename_all = "snake_case")]
2958pub enum NonlinearMethod {
2959 IncrementalNewtonRaphson,
2960}
2961
2962#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2963#[serde(rename_all = "snake_case")]
2964pub enum ModalFrequencyUnits {
2965 Hz,
2966}
2967
2968#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2969#[serde(rename_all = "snake_case")]
2970pub enum ModalFrequencyBasis {
2971 NativeEigenSolve,
2972}
2973
2974pub(crate) fn format_precision_mode(mode: PrecisionMode) -> String {
2975 match mode {
2976 PrecisionMode::Fp32 => "fp32".to_string(),
2977 PrecisionMode::Fp64 => "fp64".to_string(),
2978 PrecisionMode::Mixed => "mixed".to_string(),
2979 }
2980}
2981
2982pub(crate) fn format_quality_policy(mode: QualityPolicy) -> String {
2983 match mode {
2984 QualityPolicy::Strict => "strict".to_string(),
2985 QualityPolicy::Balanced => "balanced".to_string(),
2986 QualityPolicy::Exploratory => "exploratory".to_string(),
2987 }
2988}
2989
2990#[cfg(test)]
2991mod tests {
2992 use super::*;
2993 use std::collections::HashSet;
2994
2995 #[test]
2996 fn physics_profile_catalog_covers_every_supported_profile_once() {
2997 let expected_profiles = [
2998 AnalysisCreateModelProfile::LinearStaticStructural,
2999 AnalysisCreateModelProfile::ThermoMechanicalCoupled,
3000 AnalysisCreateModelProfile::ElectroThermalCoupled,
3001 AnalysisCreateModelProfile::ThermalStandalone,
3002 AnalysisCreateModelProfile::ModalStructural,
3003 AnalysisCreateModelProfile::AcousticHarmonic,
3004 AnalysisCreateModelProfile::TransientStructural,
3005 AnalysisCreateModelProfile::NonlinearStructural,
3006 AnalysisCreateModelProfile::ElectromagneticStatic,
3007 AnalysisCreateModelProfile::CfdSteadyState,
3008 AnalysisCreateModelProfile::CfdTransient,
3009 AnalysisCreateModelProfile::ChtCoupled,
3010 AnalysisCreateModelProfile::FsiCoupled,
3011 ];
3012
3013 assert_eq!(
3014 ANALYSIS_PHYSICS_PROFILE_CATALOG.len(),
3015 expected_profiles.len()
3016 );
3017
3018 let mut seen = HashSet::new();
3019 for entry in ANALYSIS_PHYSICS_PROFILE_CATALOG {
3020 let serialized_profile = serde_json::to_value(entry.profile)
3021 .expect("profile should serialize")
3022 .as_str()
3023 .expect("profile should serialize as string")
3024 .to_string();
3025 assert!(
3026 expected_profiles.contains(&entry.profile),
3027 "catalog contains unsupported profile {:?}",
3028 entry.profile
3029 );
3030 assert_eq!(entry.profile.as_snake_case(), serialized_profile);
3031 assert_eq!(
3032 AnalysisCreateModelProfile::from_snake_case(entry.profile.as_snake_case()),
3033 Some(entry.profile)
3034 );
3035 assert!(
3036 seen.insert(entry.profile as u8),
3037 "catalog contains duplicate profile {:?}",
3038 entry.profile
3039 );
3040 assert!(!entry.label.trim().is_empty());
3041 assert!(!entry.family.trim().is_empty());
3042 assert!(!entry.target.trim().is_empty());
3043 assert!(!entry.value.trim().is_empty());
3044 assert!(
3045 !entry.default_outputs.is_empty(),
3046 "catalog profile {:?} needs at least one default output",
3047 entry.profile
3048 );
3049 for output in entry.default_outputs {
3050 assert!(!output.field.trim().is_empty());
3051 assert!(!output.location.trim().is_empty());
3052 }
3053 }
3054
3055 for run_kind in [
3056 AnalysisRunKind::LinearStatic,
3057 AnalysisRunKind::Modal,
3058 AnalysisRunKind::Acoustic,
3059 AnalysisRunKind::Thermal,
3060 AnalysisRunKind::Transient,
3061 AnalysisRunKind::Cfd,
3062 AnalysisRunKind::Cht,
3063 AnalysisRunKind::Fsi,
3064 AnalysisRunKind::Nonlinear,
3065 AnalysisRunKind::Electromagnetic,
3066 ] {
3067 let serialized_run_kind = serde_json::to_value(run_kind)
3068 .expect("run kind should serialize")
3069 .as_str()
3070 .expect("run kind should serialize as string")
3071 .to_string();
3072 assert_eq!(run_kind.as_snake_case(), serialized_run_kind);
3073 }
3074 }
3075
3076 #[test]
3077 fn runtime_physics_profile_catalog_exposes_full_family_set() {
3078 let runtime_catalog = analysis_runtime_physics_profile_catalog();
3079
3080 assert_eq!(
3081 runtime_catalog.len(),
3082 ANALYSIS_PHYSICS_PROFILE_CATALOG.len(),
3083 "runtime profile catalog must serialize every supported physics profile"
3084 );
3085
3086 let families: HashSet<&str> = runtime_catalog
3087 .iter()
3088 .map(|entry| entry.family.as_str())
3089 .collect();
3090 for family in [
3091 "structural",
3092 "modal",
3093 "thermal",
3094 "coupled physics",
3095 "electromagnetic",
3096 "acoustic",
3097 "CFD",
3098 ] {
3099 assert!(
3100 families.contains(family),
3101 "runtime profile catalog is missing {family}"
3102 );
3103 }
3104
3105 for entry in runtime_catalog {
3106 assert!(!entry.label.trim().is_empty());
3107 assert!(!entry.target.trim().is_empty());
3108 assert!(!entry.value.trim().is_empty());
3109 assert!(!entry.default_outputs.is_empty());
3110 }
3111 }
3112}