Skip to main content

runmat_runtime/analysis/
contracts.rs

1use runmat_analysis_core::{AnalysisField, AnalysisFieldValues, AnalysisModel};
2use runmat_analysis_fea::diagnostics::FeaDiagnostic;
3use runmat_analysis_fea::{ComputeBackend, FeaRunResult};
4use runmat_geometry_core::GeometryAsset;
5use runmat_meshing_core::RegionMeshMapping;
6use serde::{Deserialize, Serialize};
7
8fn default_prep_coordinate_span_m() -> f64 {
9    1.0
10}
11
12fn default_prep_coordinate_secondary_span_m() -> f64 {
13    0.0
14}
15
16fn default_prep_coordinate_active_dimension_count() -> usize {
17    1
18}
19
20fn default_prep_coordinate_characteristic_length_m() -> f64 {
21    1.0
22}
23
24fn default_zero_usize() -> usize {
25    0
26}
27
28fn default_zero_f64() -> f64 {
29    0.0
30}
31
32fn default_reference_element_coordinates_m() -> [[f64; 3]; 3] {
33    [[0.0; 3]; 3]
34}
35
36fn default_element_topology_sample_edge_nodes() -> [[u32; 2]; 8] {
37    [[0; 2]; 8]
38}
39
40fn default_element_topology_sample_node_coordinates_m() -> [[f64; 3]; 8] {
41    [[0.0; 3]; 8]
42}
43
44fn default_element_topology_sample_element_edges() -> [[u32; 3]; 4] {
45    [[0; 3]; 4]
46}
47
48fn default_element_topology_sample_element_orientations() -> [[i8; 3]; 4] {
49    [[0; 3]; 4]
50}
51
52fn default_element_topology_sample_element_areas_m2() -> [f64; 4] {
53    [0.0; 4]
54}
55
56fn default_element_topology_node_coordinates_m() -> Vec<[f64; 3]> {
57    Vec::new()
58}
59
60fn default_element_topology_edge_nodes() -> Vec<[u32; 2]> {
61    Vec::new()
62}
63
64fn default_element_topology_element_edges() -> Vec<[u32; 3]> {
65    Vec::new()
66}
67
68fn default_element_topology_element_orientations() -> Vec<[i8; 3]> {
69    Vec::new()
70}
71
72fn default_element_topology_element_areas_m2() -> Vec<f64> {
73    Vec::new()
74}
75
76#[derive(Debug, Clone, PartialEq)]
77pub struct AnalysisValidateResult {
78    pub valid: bool,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct AnalysisCreateModelIntentSpec {
83    pub model_id: String,
84    pub profile: AnalysisCreateModelProfile,
85    #[serde(default)]
86    pub prep_context: Option<AnalysisCreateModelPrepContext>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct AnalysisCreateModelPrepContext {
91    pub source_geometry_id: String,
92    pub source_geometry_revision: u32,
93    pub region_mappings: Vec<RegionMeshMapping>,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct AnalysisRunPrepContext {
98    pub prepared_mesh_count: usize,
99    pub prepared_node_count: usize,
100    pub prepared_element_count: usize,
101    pub mapped_region_count: usize,
102    pub min_scaled_jacobian: f64,
103    pub mean_aspect_ratio: f64,
104    pub inverted_element_count: usize,
105    pub mapped_load_count: usize,
106    pub mapped_bc_count: usize,
107    pub layout_seed: u64,
108    pub topology_dof_multiplier: f64,
109    pub topology_bandwidth_estimate: u32,
110    pub mapped_region_participation_ratio: f64,
111    pub topology_surface_patch_ratio: f64,
112    pub topology_volume_core_ratio: f64,
113    pub topology_mixed_family_ratio: f64,
114    pub topology_region_span_mean: f64,
115    pub topology_region_block_count: usize,
116    pub topology_region_mesh_mean: f64,
117    pub topology_region_mesh_variance: f64,
118    pub topology_triangle_family_ratio: f64,
119    pub topology_quad_family_ratio: f64,
120    pub topology_tet_family_ratio: f64,
121    pub topology_hex_family_ratio: f64,
122    #[serde(default = "default_prep_coordinate_span_m")]
123    pub coordinate_span_x_m: f64,
124    #[serde(default = "default_prep_coordinate_secondary_span_m")]
125    pub coordinate_span_y_m: f64,
126    #[serde(default = "default_prep_coordinate_secondary_span_m")]
127    pub coordinate_span_z_m: f64,
128    #[serde(default = "default_prep_coordinate_active_dimension_count")]
129    pub coordinate_active_dimension_count: usize,
130    #[serde(default = "default_prep_coordinate_characteristic_length_m")]
131    pub coordinate_characteristic_length_m: f64,
132    #[serde(default = "default_zero_usize")]
133    pub element_geometry_node_count: usize,
134    #[serde(default = "default_zero_usize")]
135    pub element_geometry_edge_count: usize,
136    #[serde(default = "default_zero_f64")]
137    pub mean_element_edge_length_m: f64,
138    #[serde(default = "default_zero_f64")]
139    pub mean_element_area_m2: f64,
140    #[serde(default = "default_zero_f64")]
141    pub element_geometry_coverage_ratio: f64,
142    #[serde(default = "default_reference_element_coordinates_m")]
143    pub reference_element_coordinates_m: [[f64; 3]; 3],
144    #[serde(default = "default_zero_f64")]
145    pub reference_element_area_m2: f64,
146    #[serde(default = "default_zero_usize")]
147    pub control_volume_cell_count: usize,
148    #[serde(default = "default_zero_usize")]
149    pub control_volume_face_count: usize,
150    #[serde(default = "default_zero_usize")]
151    pub control_volume_internal_face_count: usize,
152    #[serde(default = "default_zero_usize")]
153    pub control_volume_boundary_face_count: usize,
154    #[serde(default = "default_zero_f64")]
155    pub control_volume_connectivity_coverage_ratio: f64,
156    #[serde(default = "default_zero_usize")]
157    pub element_topology_sample_element_count: usize,
158    #[serde(default = "default_zero_usize")]
159    pub element_topology_sample_edge_count: usize,
160    #[serde(default = "default_element_topology_sample_edge_nodes")]
161    pub element_topology_sample_edge_nodes: [[u32; 2]; 8],
162    #[serde(default = "default_element_topology_sample_node_coordinates_m")]
163    pub element_topology_sample_node_coordinates_m: [[f64; 3]; 8],
164    #[serde(default = "default_element_topology_sample_element_edges")]
165    pub element_topology_sample_element_edges: [[u32; 3]; 4],
166    #[serde(default = "default_element_topology_sample_element_orientations")]
167    pub element_topology_sample_element_orientations: [[i8; 3]; 4],
168    #[serde(default = "default_element_topology_sample_element_areas_m2")]
169    pub element_topology_sample_element_areas_m2: [f64; 4],
170    #[serde(default = "default_element_topology_node_coordinates_m")]
171    pub element_topology_node_coordinates_m: Vec<[f64; 3]>,
172    #[serde(default = "default_element_topology_edge_nodes")]
173    pub element_topology_edge_nodes: Vec<[u32; 2]>,
174    #[serde(default = "default_element_topology_element_edges")]
175    pub element_topology_element_edges: Vec<[u32; 3]>,
176    #[serde(default = "default_element_topology_element_orientations")]
177    pub element_topology_element_orientations: Vec<[i8; 3]>,
178    #[serde(default = "default_element_topology_element_areas_m2")]
179    pub element_topology_element_areas_m2: Vec<f64>,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum AnalysisCreateModelProfile {
185    LinearStaticStructural,
186    ThermoMechanicalCoupled,
187    ThermalStandalone,
188    ModalStructural,
189    AcousticHarmonic,
190    TransientStructural,
191    NonlinearStructural,
192    ElectromagneticStatic,
193    CfdSteadyState,
194    CfdTransient,
195    ChtCoupled,
196    FsiCoupled,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum PrecisionMode {
202    Fp32,
203    Fp64,
204    Mixed,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum PreconditionerMode {
210    Auto,
211    Jacobi,
212    Amg,
213    Ilu,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "snake_case")]
218pub enum QualityPolicy {
219    Strict,
220    Balanced,
221    Exploratory,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum PrepCalibrationProfile {
227    Auto,
228    Fast,
229    Balanced,
230    Conservative,
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234pub struct ThermoRegionTemperatureDelta {
235    pub region_id: String,
236    pub temperature_delta_k: f64,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct ThermoTimeProfilePoint {
241    pub normalized_time: f64,
242    pub scale: f64,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum ThermoFieldInterpolationMode {
248    Linear,
249    Step,
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub struct ThermoFieldSource {
254    pub source_id: String,
255    pub revision: u32,
256    #[serde(default)]
257    pub interpolation_mode: Option<ThermoFieldInterpolationMode>,
258    #[serde(default)]
259    pub expected_region_ids: Vec<String>,
260}
261
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct ThermoMechanicalCouplingOptions {
264    pub enabled: bool,
265    pub reference_temperature_k: f64,
266    pub applied_temperature_delta_k: f64,
267    pub thermal_expansion_coefficient: f64,
268    #[serde(default)]
269    pub field_artifact_id: Option<String>,
270    #[serde(default)]
271    pub field_source: Option<ThermoFieldSource>,
272    #[serde(default)]
273    pub region_temperature_deltas: Vec<ThermoRegionTemperatureDelta>,
274    #[serde(default)]
275    pub time_profile: Vec<ThermoTimeProfilePoint>,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct ElectroRegionConductivityScale {
280    pub region_id: String,
281    pub conductivity_scale: f64,
282}
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct ElectroTimeProfilePoint {
286    pub normalized_time: f64,
287    pub current_scale: f64,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct ElectroThermalCouplingOptions {
292    pub enabled: bool,
293    pub reference_temperature_k: f64,
294    pub applied_voltage_v: f64,
295    pub base_electrical_conductivity_s_per_m: f64,
296    pub resistive_heating_coefficient: f64,
297    #[serde(default)]
298    pub region_conductivity_scales: Vec<ElectroRegionConductivityScale>,
299    #[serde(default)]
300    pub time_profile: Vec<ElectroTimeProfilePoint>,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304pub struct PlasticityConstitutiveOptions {
305    pub enabled: bool,
306    pub yield_strain: f64,
307    pub hardening_modulus_ratio: f64,
308    pub saturation_exponent: f64,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub struct ContactInterfaceOptions {
313    pub enabled: bool,
314    pub penalty_stiffness_scale: f64,
315    pub max_penetration_ratio: f64,
316    pub friction_coefficient: f64,
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case")]
321pub enum QualityReasonCode {
322    MaterialAssignmentConflict,
323    SolverNotConverged,
324    SolverBackendFallback,
325    FieldPromotionFallback,
326    ModalResidualExceeded,
327    ModalOrthogonalityExceeded,
328    ModalSeparationLow,
329    TransientResidualExceeded,
330    TransientStabilityExceeded,
331    TransientStepFailure,
332    ThermoMechanicalTransientStress,
333    ThermoMechanicalConstitutiveSpreadHigh,
334    ThermoMechanicalAssignmentHeterogeneityHigh,
335    ThermoMechanicalGradientInstability,
336    ThermoMechanicalFieldCoverageLow,
337    ThermoMechanicalFieldExtrapolationHigh,
338    ElectroThermalTransientStress,
339    ElectroThermalNonlinearStress,
340    ElectromagneticSolveQualityLow,
341    ElectromagneticConductivitySpreadHigh,
342    ElectromagneticMaterialHeterogeneityHigh,
343    ElectromagneticAssignmentCoverageLow,
344    ElectromagneticRegionContrastHigh,
345    ElectromagneticConditioningHigh,
346    ElectromagneticSourceRealizationLow,
347    ElectromagneticSourceRegionCoverageLow,
348    ElectromagneticSourceMaterialAlignmentLow,
349    ElectromagneticSourceOverlapHigh,
350    ElectromagneticSourceInterferenceHigh,
351    ElectromagneticBoundaryLocalizationLow,
352    ElectromagneticGroundAnchorEffectivenessLow,
353    ElectromagneticInsulationLeakageHigh,
354    ElectromagneticBoundaryAnchoringLow,
355    ElectromagneticFluxDivergenceHigh,
356    ElectromagneticEnergyImbalanceHigh,
357    ElectromagneticBoundaryEnergyLow,
358    ElectromagneticBoundaryPenaltyConditioningHigh,
359    ElectromagneticSourceRegionEnergyConsistencyLow,
360    ElectromagneticRealResidualHigh,
361    ElectromagneticImagResidualHigh,
362    ElectromagneticSweepCoverageLow,
363    ElectromagneticResonanceSharpnessLow,
364    PlasticityNonlinearStress,
365    ContactNonlinearStress,
366    NonlinearResidualExceeded,
367    NonlinearIncrementFailure,
368    ThermoMechanicalNonlinearStress,
369    ThermalResidualExceeded,
370    ThermalConstitutiveSpreadHigh,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct QualityReason {
375    pub code: QualityReasonCode,
376    pub detail: String,
377}
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380pub struct AnalysisRunOptions {
381    pub deterministic_mode: bool,
382    pub precision_mode: PrecisionMode,
383    pub preconditioner_mode: PreconditionerMode,
384    pub quality_policy: QualityPolicy,
385    #[serde(default)]
386    pub prep_context: Option<AnalysisRunPrepContext>,
387    #[serde(default)]
388    pub prep_artifact_id: Option<String>,
389    #[serde(default)]
390    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct AnalysisElectromagneticRunOptions {
395    pub deterministic_mode: bool,
396    pub precision_mode: PrecisionMode,
397    pub quality_policy: QualityPolicy,
398    pub residual_target: f64,
399    pub harmonic_tolerance: f64,
400    pub harmonic_max_iterations: usize,
401    #[serde(default)]
402    pub prep_context: Option<AnalysisRunPrepContext>,
403    #[serde(default)]
404    pub prep_artifact_id: Option<String>,
405    #[serde(default)]
406    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
407    #[serde(default)]
408    pub sweep_enabled: bool,
409    #[serde(default)]
410    pub sweep_frequency_hz: Vec<f64>,
411}
412
413impl Default for AnalysisElectromagneticRunOptions {
414    fn default() -> Self {
415        Self {
416            deterministic_mode: false,
417            precision_mode: PrecisionMode::Fp64,
418            quality_policy: QualityPolicy::Balanced,
419            residual_target: 1.0e-6,
420            harmonic_tolerance: 1.0e-7,
421            harmonic_max_iterations: 96,
422            prep_context: None,
423            prep_artifact_id: None,
424            prep_calibration_profile: None,
425            sweep_enabled: false,
426            sweep_frequency_hz: Vec::new(),
427        }
428    }
429}
430
431impl Default for AnalysisRunOptions {
432    fn default() -> Self {
433        Self {
434            deterministic_mode: false,
435            precision_mode: PrecisionMode::Fp64,
436            preconditioner_mode: PreconditionerMode::Auto,
437            quality_policy: QualityPolicy::Balanced,
438            prep_context: None,
439            prep_artifact_id: None,
440            prep_calibration_profile: None,
441        }
442    }
443}
444
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446pub struct AnalysisTransientRunOptions {
447    pub deterministic_mode: bool,
448    pub precision_mode: PrecisionMode,
449    pub quality_policy: QualityPolicy,
450    pub time_step_s: f64,
451    pub min_time_step_s: f64,
452    pub max_time_step_s: f64,
453    pub step_count: usize,
454    pub max_linear_iters: usize,
455    pub tolerance: f64,
456    pub residual_target: f64,
457    pub adaptive_time_step: bool,
458    pub max_step_retries: usize,
459    pub adapt_min_scale: f64,
460    pub adapt_max_scale: f64,
461    pub adapt_growth_exponent: f64,
462    pub adapt_retry_growth_cap: f64,
463    pub adapt_nonconverged_shrink: f64,
464    pub dt_bucket_rel_tolerance: f64,
465    #[serde(default)]
466    pub prep_context: Option<AnalysisRunPrepContext>,
467    #[serde(default)]
468    pub prep_artifact_id: Option<String>,
469    #[serde(default)]
470    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
471}
472
473impl Default for AnalysisTransientRunOptions {
474    fn default() -> Self {
475        Self {
476            deterministic_mode: false,
477            precision_mode: PrecisionMode::Fp64,
478            quality_policy: QualityPolicy::Balanced,
479            time_step_s: 1.0e-3,
480            min_time_step_s: 1.0e-6,
481            max_time_step_s: 2.0e-2,
482            step_count: 10,
483            max_linear_iters: 128,
484            tolerance: 1.0e-8,
485            residual_target: 1.0e-6,
486            adaptive_time_step: true,
487            max_step_retries: 4,
488            adapt_min_scale: 0.8,
489            adapt_max_scale: 1.25,
490            adapt_growth_exponent: 0.35,
491            adapt_retry_growth_cap: 1.05,
492            adapt_nonconverged_shrink: 0.75,
493            dt_bucket_rel_tolerance: 0.0,
494            prep_context: None,
495            prep_artifact_id: None,
496            prep_calibration_profile: None,
497        }
498    }
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502pub struct AnalysisAcousticRunOptions {
503    pub deterministic_mode: bool,
504    pub precision_mode: PrecisionMode,
505    pub quality_policy: QualityPolicy,
506    pub mode_count: usize,
507    pub residual_warn_threshold: f64,
508    #[serde(default)]
509    pub prep_context: Option<AnalysisRunPrepContext>,
510    #[serde(default)]
511    pub prep_artifact_id: Option<String>,
512    #[serde(default)]
513    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
514}
515
516impl Default for AnalysisAcousticRunOptions {
517    fn default() -> Self {
518        Self {
519            deterministic_mode: false,
520            precision_mode: PrecisionMode::Fp64,
521            quality_policy: QualityPolicy::Balanced,
522            mode_count: 3,
523            residual_warn_threshold: 1.0e-3,
524            prep_context: None,
525            prep_artifact_id: None,
526            prep_calibration_profile: None,
527        }
528    }
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
532pub struct AnalysisCfdRunOptions {
533    pub deterministic_mode: bool,
534    pub precision_mode: PrecisionMode,
535    pub quality_policy: QualityPolicy,
536    pub time_step_s: f64,
537    pub step_count: usize,
538    pub max_linear_iters: usize,
539    pub tolerance: f64,
540    pub residual_warn_threshold: f64,
541    #[serde(default)]
542    pub prep_context: Option<AnalysisRunPrepContext>,
543    #[serde(default)]
544    pub prep_artifact_id: Option<String>,
545    #[serde(default)]
546    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
547}
548
549impl Default for AnalysisCfdRunOptions {
550    fn default() -> Self {
551        Self {
552            deterministic_mode: false,
553            precision_mode: PrecisionMode::Fp64,
554            quality_policy: QualityPolicy::Balanced,
555            time_step_s: 1.0e-3,
556            step_count: 12,
557            max_linear_iters: 128,
558            tolerance: 1.0e-8,
559            residual_warn_threshold: 1.0e-5,
560            prep_context: None,
561            prep_artifact_id: None,
562            prep_calibration_profile: None,
563        }
564    }
565}
566
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568pub struct AnalysisChtRunOptions {
569    pub deterministic_mode: bool,
570    pub precision_mode: PrecisionMode,
571    pub quality_policy: QualityPolicy,
572    pub time_step_s: f64,
573    pub step_count: usize,
574    pub max_linear_iters: usize,
575    pub tolerance: f64,
576    pub residual_warn_threshold: f64,
577    #[serde(default)]
578    pub prep_context: Option<AnalysisRunPrepContext>,
579    #[serde(default)]
580    pub prep_artifact_id: Option<String>,
581    #[serde(default)]
582    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
583}
584
585impl Default for AnalysisChtRunOptions {
586    fn default() -> Self {
587        Self {
588            deterministic_mode: false,
589            precision_mode: PrecisionMode::Fp64,
590            quality_policy: QualityPolicy::Balanced,
591            time_step_s: 1.0e-3,
592            step_count: 12,
593            max_linear_iters: 128,
594            tolerance: 1.0e-8,
595            residual_warn_threshold: 1.0e-4,
596            prep_context: None,
597            prep_artifact_id: None,
598            prep_calibration_profile: None,
599        }
600    }
601}
602
603#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
604pub struct AnalysisFsiRunOptions {
605    pub deterministic_mode: bool,
606    pub precision_mode: PrecisionMode,
607    pub quality_policy: QualityPolicy,
608    pub time_step_s: f64,
609    pub step_count: usize,
610    pub max_linear_iters: usize,
611    pub tolerance: f64,
612    pub residual_warn_threshold: f64,
613    #[serde(default)]
614    pub prep_context: Option<AnalysisRunPrepContext>,
615    #[serde(default)]
616    pub prep_artifact_id: Option<String>,
617    #[serde(default)]
618    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
619}
620
621impl Default for AnalysisFsiRunOptions {
622    fn default() -> Self {
623        Self {
624            deterministic_mode: false,
625            precision_mode: PrecisionMode::Fp64,
626            quality_policy: QualityPolicy::Balanced,
627            time_step_s: 1.0e-3,
628            step_count: 12,
629            max_linear_iters: 128,
630            tolerance: 1.0e-8,
631            residual_warn_threshold: 1.0e-4,
632            prep_context: None,
633            prep_artifact_id: None,
634            prep_calibration_profile: None,
635        }
636    }
637}
638
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
640pub struct AnalysisThermalRunOptions {
641    pub deterministic_mode: bool,
642    pub precision_mode: PrecisionMode,
643    pub quality_policy: QualityPolicy,
644    pub step_count: usize,
645    pub time_step_s: f64,
646    pub residual_warn_threshold: f64,
647    #[serde(default)]
648    pub prep_context: Option<AnalysisRunPrepContext>,
649    #[serde(default)]
650    pub prep_artifact_id: Option<String>,
651    #[serde(default)]
652    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
653}
654
655impl Default for AnalysisThermalRunOptions {
656    fn default() -> Self {
657        Self {
658            deterministic_mode: false,
659            precision_mode: PrecisionMode::Fp64,
660            quality_policy: QualityPolicy::Balanced,
661            step_count: 10,
662            time_step_s: 1.0e-2,
663            residual_warn_threshold: 1.0e-4,
664            prep_context: None,
665            prep_artifact_id: None,
666            prep_calibration_profile: None,
667        }
668    }
669}
670
671impl AnalysisTransientRunOptions {
672    pub fn coarse() -> Self {
673        Self {
674            deterministic_mode: false,
675            precision_mode: PrecisionMode::Fp32,
676            quality_policy: QualityPolicy::Exploratory,
677            time_step_s: 5.0e-3,
678            min_time_step_s: 5.0e-4,
679            max_time_step_s: 2.0e-2,
680            step_count: 6,
681            max_linear_iters: 64,
682            tolerance: 1.0e-6,
683            residual_target: 1.0e-4,
684            adaptive_time_step: true,
685            max_step_retries: 2,
686            adapt_min_scale: 0.75,
687            adapt_max_scale: 1.3,
688            adapt_growth_exponent: 0.3,
689            adapt_retry_growth_cap: 1.02,
690            adapt_nonconverged_shrink: 0.7,
691            dt_bucket_rel_tolerance: 0.02,
692            prep_context: None,
693            prep_artifact_id: None,
694            prep_calibration_profile: None,
695        }
696    }
697
698    pub fn balanced() -> Self {
699        Self::default()
700    }
701
702    pub fn production_recommended() -> Self {
703        Self {
704            quality_policy: QualityPolicy::Balanced,
705            deterministic_mode: true,
706            precision_mode: PrecisionMode::Fp64,
707            dt_bucket_rel_tolerance: 0.01,
708            ..Self::balanced()
709        }
710    }
711
712    pub fn high_accuracy() -> Self {
713        Self {
714            deterministic_mode: true,
715            precision_mode: PrecisionMode::Fp64,
716            quality_policy: QualityPolicy::Strict,
717            time_step_s: 5.0e-4,
718            min_time_step_s: 5.0e-6,
719            max_time_step_s: 2.0e-3,
720            step_count: 24,
721            max_linear_iters: 256,
722            tolerance: 1.0e-10,
723            residual_target: 1.0e-7,
724            adaptive_time_step: true,
725            max_step_retries: 8,
726            adapt_min_scale: 0.85,
727            adapt_max_scale: 1.2,
728            adapt_growth_exponent: 0.45,
729            adapt_retry_growth_cap: 1.03,
730            adapt_nonconverged_shrink: 0.8,
731            dt_bucket_rel_tolerance: 0.005,
732            prep_context: None,
733            prep_artifact_id: None,
734            prep_calibration_profile: None,
735        }
736    }
737}
738
739#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
740pub struct AnalysisModalRunOptions {
741    pub deterministic_mode: bool,
742    pub precision_mode: PrecisionMode,
743    pub quality_policy: QualityPolicy,
744    pub mode_count: usize,
745    pub residual_warn_threshold: f64,
746    #[serde(default)]
747    pub prep_context: Option<AnalysisRunPrepContext>,
748    #[serde(default)]
749    pub prep_artifact_id: Option<String>,
750    #[serde(default)]
751    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
752}
753
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
755pub struct AnalysisNonlinearRunOptions {
756    pub deterministic_mode: bool,
757    pub precision_mode: PrecisionMode,
758    pub quality_policy: QualityPolicy,
759    pub increment_count: usize,
760    pub max_newton_iters: usize,
761    pub tolerance: f64,
762    pub residual_convergence_factor: f64,
763    pub increment_norm_tolerance: f64,
764    pub line_search: bool,
765    pub max_line_search_backtracks: usize,
766    pub line_search_reduction: f64,
767    pub tangent_refresh_interval: usize,
768    #[serde(default)]
769    pub prep_context: Option<AnalysisRunPrepContext>,
770    #[serde(default)]
771    pub prep_artifact_id: Option<String>,
772    #[serde(default)]
773    pub prep_calibration_profile: Option<PrepCalibrationProfile>,
774}
775
776impl Default for AnalysisNonlinearRunOptions {
777    fn default() -> Self {
778        Self {
779            deterministic_mode: false,
780            precision_mode: PrecisionMode::Fp64,
781            quality_policy: QualityPolicy::Balanced,
782            increment_count: 12,
783            max_newton_iters: 24,
784            tolerance: 1.0e-6,
785            residual_convergence_factor: 5.0,
786            increment_norm_tolerance: 1.0e-7,
787            line_search: true,
788            max_line_search_backtracks: 6,
789            line_search_reduction: 0.5,
790            tangent_refresh_interval: 2,
791            prep_context: None,
792            prep_artifact_id: None,
793            prep_calibration_profile: None,
794        }
795    }
796}
797
798impl AnalysisNonlinearRunOptions {
799    pub fn coarse() -> Self {
800        Self {
801            deterministic_mode: false,
802            precision_mode: PrecisionMode::Fp32,
803            quality_policy: QualityPolicy::Exploratory,
804            increment_count: 8,
805            max_newton_iters: 16,
806            tolerance: 5.0e-6,
807            residual_convergence_factor: 8.0,
808            increment_norm_tolerance: 5.0e-7,
809            line_search: false,
810            max_line_search_backtracks: 0,
811            line_search_reduction: 0.6,
812            tangent_refresh_interval: 4,
813            prep_context: None,
814            prep_artifact_id: None,
815            prep_calibration_profile: None,
816        }
817    }
818
819    pub fn balanced() -> Self {
820        Self::default()
821    }
822
823    pub fn high_accuracy() -> Self {
824        Self {
825            deterministic_mode: true,
826            precision_mode: PrecisionMode::Fp64,
827            quality_policy: QualityPolicy::Strict,
828            increment_count: 24,
829            max_newton_iters: 40,
830            tolerance: 1.0e-7,
831            residual_convergence_factor: 3.0,
832            increment_norm_tolerance: 5.0e-8,
833            line_search: true,
834            max_line_search_backtracks: 10,
835            line_search_reduction: 0.5,
836            tangent_refresh_interval: 1,
837            prep_context: None,
838            prep_artifact_id: None,
839            prep_calibration_profile: None,
840        }
841    }
842
843    pub fn production_recommended() -> Self {
844        Self {
845            deterministic_mode: true,
846            precision_mode: PrecisionMode::Fp64,
847            quality_policy: QualityPolicy::Balanced,
848            increment_count: 24,
849            max_newton_iters: 28,
850            tolerance: 1.0e-6,
851            residual_convergence_factor: 4.0,
852            increment_norm_tolerance: 8.0e-8,
853            line_search: true,
854            max_line_search_backtracks: 8,
855            line_search_reduction: 0.5,
856            tangent_refresh_interval: 2,
857            prep_context: None,
858            prep_artifact_id: None,
859            prep_calibration_profile: None,
860        }
861    }
862}
863
864impl Default for AnalysisModalRunOptions {
865    fn default() -> Self {
866        Self {
867            deterministic_mode: false,
868            precision_mode: PrecisionMode::Fp64,
869            quality_policy: QualityPolicy::Balanced,
870            mode_count: 3,
871            residual_warn_threshold: 1.0e-3,
872            prep_context: None,
873            prep_artifact_id: None,
874            prep_calibration_profile: None,
875        }
876    }
877}
878
879impl AnalysisModalRunOptions {
880    pub fn coarse() -> Self {
881        Self {
882            deterministic_mode: false,
883            precision_mode: PrecisionMode::Fp32,
884            quality_policy: QualityPolicy::Exploratory,
885            mode_count: 2,
886            residual_warn_threshold: 5.0e-3,
887            prep_context: None,
888            prep_artifact_id: None,
889            prep_calibration_profile: None,
890        }
891    }
892
893    pub fn balanced() -> Self {
894        Self::default()
895    }
896
897    pub fn high_accuracy() -> Self {
898        Self {
899            deterministic_mode: true,
900            precision_mode: PrecisionMode::Fp64,
901            quality_policy: QualityPolicy::Strict,
902            mode_count: 8,
903            residual_warn_threshold: 5.0e-4,
904            prep_context: None,
905            prep_artifact_id: None,
906            prep_calibration_profile: None,
907        }
908    }
909}
910
911#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
912#[serde(rename_all = "snake_case")]
913pub enum QualityGate {
914    Pass,
915    Warn,
916    Fail,
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
920#[serde(rename_all = "snake_case")]
921pub enum RunStatus {
922    Publishable,
923    Degraded,
924    Rejected,
925}
926
927#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
928pub struct RunProvenance {
929    pub backend: ComputeBackend,
930    pub solver_backend: String,
931    pub solver_device_apply_k_ratio: f64,
932    pub solver_host_sync_count: u32,
933    pub precision_mode: String,
934    pub deterministic_mode: bool,
935    pub solver_method: String,
936    pub preconditioner: String,
937    pub quality_policy: String,
938    pub fallback_events: Vec<String>,
939}
940
941#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
942pub struct AnalysisRenderTopology {
943    pub schema_version: String,
944    pub source: AnalysisRenderTopologySource,
945    pub meshes: Vec<AnalysisRenderMesh>,
946}
947
948#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
949#[serde(rename_all = "snake_case")]
950pub enum AnalysisRenderTopologySource {
951    SolverPrep,
952}
953
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct AnalysisRenderMesh {
956    pub mesh_id: String,
957    pub vertices: Vec<[f64; 3]>,
958    pub triangles: Vec<[u32; 3]>,
959}
960
961#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
962pub struct AnalysisRunResult {
963    pub run_id: String,
964    pub run: FeaRunResult,
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub render_topology: Option<AnalysisRenderTopology>,
967    pub modal_results: Option<ModalResultsData>,
968    #[serde(default)]
969    pub thermal_results: Option<ThermalResultsData>,
970    pub transient_results: Option<TransientResultsData>,
971    pub nonlinear_results: Option<NonlinearResultsData>,
972    #[serde(default)]
973    pub electromagnetic_results: Option<ElectromagneticResultsData>,
974    pub model_validity: QualityGate,
975    pub solver_convergence: QualityGate,
976    pub result_quality: QualityGate,
977    pub run_status: RunStatus,
978    pub publishable: bool,
979    pub quality_reasons: Vec<QualityReason>,
980    pub provenance: RunProvenance,
981}
982
983#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
984pub struct AnalysisArtifactRecord {
985    pub run_id: String,
986    pub created_at: String,
987    pub op_version: String,
988    pub field_ids: Vec<String>,
989}
990
991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
992pub struct AnalysisResultsQuery {
993    pub include_fields: Vec<String>,
994    pub include_field_values: bool,
995    pub include_diagnostics: bool,
996    pub diagnostic_codes: Vec<String>,
997    pub include_modal_results: bool,
998    pub mode_indices: Vec<usize>,
999    pub include_transient_results: bool,
1000    pub transient_snapshot_indices: Vec<usize>,
1001    pub include_nonlinear_results: bool,
1002    pub include_electromagnetic_results: bool,
1003}
1004
1005impl Default for AnalysisResultsQuery {
1006    fn default() -> Self {
1007        Self {
1008            include_fields: Vec::new(),
1009            include_field_values: true,
1010            include_diagnostics: true,
1011            diagnostic_codes: Vec::new(),
1012            include_modal_results: true,
1013            mode_indices: Vec::new(),
1014            include_transient_results: true,
1015            transient_snapshot_indices: Vec::new(),
1016            include_nonlinear_results: true,
1017            include_electromagnetic_results: true,
1018        }
1019    }
1020}
1021
1022impl AnalysisResultsQuery {
1023    pub fn metadata_only() -> Self {
1024        Self {
1025            include_field_values: false,
1026            include_modal_results: false,
1027            include_transient_results: false,
1028            include_nonlinear_results: false,
1029            include_electromagnetic_results: false,
1030            ..Self::default()
1031        }
1032    }
1033
1034    pub fn field_values(field_id: impl Into<String>) -> Self {
1035        Self {
1036            include_fields: vec![field_id.into()],
1037            include_field_values: true,
1038            include_diagnostics: false,
1039            include_modal_results: false,
1040            include_transient_results: false,
1041            include_nonlinear_results: false,
1042            include_electromagnetic_results: false,
1043            ..Self::default()
1044        }
1045    }
1046}
1047
1048#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1049#[serde(rename_all = "snake_case")]
1050pub enum AnalysisFieldKind {
1051    Scalar,
1052    Vector,
1053    Tensor,
1054    Unknown,
1055}
1056
1057#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1058#[serde(rename_all = "snake_case")]
1059pub enum AnalysisFieldStorage {
1060    HostF64,
1061    DeviceRef,
1062}
1063
1064#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1065#[serde(rename_all = "snake_case")]
1066pub enum AnalysisFieldLocation {
1067    Node,
1068    Element,
1069    Edge,
1070    BoundaryFace,
1071    InterfaceFace,
1072    Mode,
1073    Global,
1074    #[default]
1075    Unknown,
1076}
1077
1078#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1079pub struct AnalysisFieldDescriptor {
1080    pub field_id: String,
1081    #[serde(default)]
1082    pub family: String,
1083    #[serde(default)]
1084    pub quantity: String,
1085    pub class_name: String,
1086    pub kind: AnalysisFieldKind,
1087    pub dtype: String,
1088    #[serde(default)]
1089    pub unit: Option<String>,
1090    #[serde(default)]
1091    pub location: AnalysisFieldLocation,
1092    pub shape: Vec<usize>,
1093    pub element_count: usize,
1094    pub component_count: Option<usize>,
1095    pub residency: String,
1096    pub storage: AnalysisFieldStorage,
1097    pub size_bytes: Option<u64>,
1098}
1099
1100impl AnalysisFieldDescriptor {
1101    pub fn from_field(field: &AnalysisField) -> Self {
1102        let storage = match &field.values {
1103            AnalysisFieldValues::HostF64(_) => AnalysisFieldStorage::HostF64,
1104            AnalysisFieldValues::DeviceRef(_) => AnalysisFieldStorage::DeviceRef,
1105        };
1106        let kind = infer_field_kind(&field.field_id, &field.shape);
1107        let class_name = match kind {
1108            AnalysisFieldKind::Scalar => "fea.ScalarField",
1109            AnalysisFieldKind::Vector => "fea.VectorField",
1110            AnalysisFieldKind::Tensor => "fea.TensorField",
1111            AnalysisFieldKind::Unknown => "fea.Field",
1112        }
1113        .to_string();
1114        let residency = match storage {
1115            AnalysisFieldStorage::HostF64 => "cpu",
1116            AnalysisFieldStorage::DeviceRef => "gpu",
1117        }
1118        .to_string();
1119        let element_count = field.element_count();
1120        Self {
1121            field_id: field.field_id.clone(),
1122            family: infer_field_family(&field.field_id).to_string(),
1123            quantity: infer_field_quantity(&field.field_id).to_string(),
1124            class_name,
1125            kind,
1126            dtype: "double".to_string(),
1127            unit: infer_field_unit(&field.field_id).map(str::to_string),
1128            location: infer_field_location(&field.field_id),
1129            shape: field.shape.clone(),
1130            element_count,
1131            component_count: infer_component_count(&field.field_id, &field.shape),
1132            residency,
1133            storage,
1134            size_bytes: element_count
1135                .checked_mul(std::mem::size_of::<f64>())
1136                .map(|bytes| bytes as u64),
1137        }
1138    }
1139}
1140
1141fn infer_field_family(field_id: &str) -> &str {
1142    field_id
1143        .split_once('.')
1144        .map_or("unknown", |(family, _)| family)
1145}
1146
1147fn infer_field_quantity(field_id: &str) -> &str {
1148    let Some((_, rest)) = field_id.split_once('.') else {
1149        return field_id;
1150    };
1151    let Some((quantity, suffix)) = rest.rsplit_once('.') else {
1152        return rest;
1153    };
1154    if suffix.chars().all(|ch| ch.is_ascii_digit()) {
1155        quantity
1156    } else {
1157        rest
1158    }
1159}
1160
1161fn infer_field_unit(field_id: &str) -> Option<&'static str> {
1162    let normalized = field_id.to_ascii_lowercase();
1163    if normalized.contains("frequency_hz") || normalized.contains("frequency_response") {
1164        return Some("Hz");
1165    }
1166    if normalized.contains("eigenvalue") {
1167        return Some("rad^2/s^2");
1168    }
1169    if normalized.contains("sound_pressure_level_db") {
1170        return Some("dB");
1171    }
1172    if normalized.contains("temperature_gradient") {
1173        return Some("K/m");
1174    }
1175    if normalized.contains("temperature") || normalized.contains("temperature_jump") {
1176        return Some("K");
1177    }
1178    if normalized.contains("electric_potential") {
1179        return Some("V");
1180    }
1181    if normalized.contains("electric_field") {
1182        return Some("V/m");
1183    }
1184    if normalized.contains("electric_flux_density") {
1185        return Some("C/m^2");
1186    }
1187    if normalized.contains("current_density") {
1188        return Some("A/m^2");
1189    }
1190    if normalized.contains("vector_potential") {
1191        return Some("Wb/m");
1192    }
1193    if normalized.contains("magnetic_flux_density") {
1194        return Some("T");
1195    }
1196    if normalized.contains("magnetic_field") {
1197        return Some("A/m");
1198    }
1199    if normalized.contains("poynting_vector")
1200        || normalized.contains("boundary_heat_flux")
1201        || normalized.contains("interface_heat_flux")
1202        || normalized.ends_with(".heat_flux")
1203        || normalized.contains(".heat_flux.")
1204    {
1205        return Some("W/m^2");
1206    }
1207    if normalized.contains("power_loss_density")
1208        || normalized.contains("joule_heat")
1209        || normalized.contains("heat_source")
1210    {
1211        return Some("W/m^3");
1212    }
1213    if normalized.contains("energy_density") {
1214        return Some("J/m^3");
1215    }
1216    if normalized.contains("kinetic_energy")
1217        || normalized.contains("strain_energy")
1218        || normalized.contains("total_strain_energy")
1219    {
1220        return Some("J");
1221    }
1222    if normalized.contains("modal_mass") {
1223        return Some("kg");
1224    }
1225    if normalized.contains("modal_stiffness") {
1226        return Some("N/m");
1227    }
1228    if normalized.contains("wall_shear_stress")
1229        || normalized.contains("contact_pressure")
1230        || normalized.contains("interface_pressure")
1231        || normalized.contains("fluid_pressure")
1232        || normalized.contains(".pressure")
1233        || normalized.contains("stress")
1234        || normalized.contains("traction")
1235        || normalized.contains("von_mises")
1236    {
1237        return Some("Pa");
1238    }
1239    if normalized.contains("reaction_force")
1240        || normalized.contains("beam_axial_force")
1241        || normalized.contains("beam_shear_force")
1242        || normalized.contains("shell_membrane_force")
1243        || normalized.contains("shell_transverse_shear")
1244    {
1245        return Some("N");
1246    }
1247    if normalized.contains("reaction_moment")
1248        || normalized.contains("beam_torsion_moment")
1249        || normalized.contains("beam_bending_moment")
1250        || normalized.contains("shell_bending_moment")
1251    {
1252        return Some("N*m");
1253    }
1254    if normalized.contains("rotation") {
1255        return Some("rad");
1256    }
1257    if normalized.contains("contact_gap")
1258        || normalized.contains("displacement")
1259        || normalized.contains("mode_shape")
1260    {
1261        return Some("m");
1262    }
1263    if normalized.contains("velocity") {
1264        return Some("m/s");
1265    }
1266    if normalized.contains("acceleration") {
1267        return Some("m/s^2");
1268    }
1269    if normalized.contains("strain")
1270        || normalized.contains("residual")
1271        || normalized.contains("equation_scale")
1272        || normalized.contains("load_factor")
1273        || normalized.contains("coupling_iteration_count")
1274        || normalized.contains("reynolds_number")
1275        || normalized.contains("phase")
1276        || normalized.contains("orthogonality")
1277        || normalized.contains("participation_factor")
1278        || normalized.contains("relative_frequency_separation")
1279    {
1280        return Some("1");
1281    }
1282    None
1283}
1284
1285fn infer_field_location(field_id: &str) -> AnalysisFieldLocation {
1286    let normalized = field_id.to_ascii_lowercase();
1287    if normalized.contains("vector_potential") {
1288        return AnalysisFieldLocation::Edge;
1289    }
1290    if normalized.contains("wall_shear_stress") || normalized.contains("boundary_heat_flux") {
1291        return AnalysisFieldLocation::BoundaryFace;
1292    }
1293    if normalized.contains("interface_")
1294        || normalized.contains("contact_pressure")
1295        || normalized.contains("contact_gap")
1296    {
1297        return AnalysisFieldLocation::InterfaceFace;
1298    }
1299    if normalized.starts_with("modal.")
1300        && !normalized.starts_with("modal.mode_shape.")
1301        && !normalized.contains("orthogonality")
1302    {
1303        return AnalysisFieldLocation::Mode;
1304    }
1305    if normalized.contains("residual")
1306        || normalized.contains("equation_scale")
1307        || normalized.contains("energy")
1308        || normalized.contains("load_factor")
1309        || normalized.contains("coupling_iteration_count")
1310        || normalized.contains("orthogonality")
1311    {
1312        return AnalysisFieldLocation::Global;
1313    }
1314    if normalized.starts_with("acoustic.") {
1315        return AnalysisFieldLocation::Node;
1316    }
1317    if normalized.contains("beam_") || normalized.contains("shell_") {
1318        return AnalysisFieldLocation::Element;
1319    }
1320    if normalized.contains("temperature_gradient")
1321        || normalized.contains("heat_flux")
1322        || normalized.contains("heat_source")
1323        || normalized.contains("joule_heat")
1324        || normalized.contains("magnetic_flux_density")
1325        || normalized.contains("magnetic_field")
1326        || normalized.contains("electric_field")
1327        || normalized.contains("electric_flux_density")
1328        || normalized.contains("current_density")
1329        || normalized.contains("poynting_vector")
1330        || normalized.contains("vorticity")
1331        || normalized.contains("pressure")
1332        || normalized.contains("stress")
1333        || normalized.contains("strain")
1334        || normalized.contains("von_mises")
1335    {
1336        return AnalysisFieldLocation::Element;
1337    }
1338    if normalized.contains("displacement")
1339        || normalized.contains("mode_shape")
1340        || normalized.contains("temperature")
1341        || normalized.starts_with("acoustic.")
1342        || normalized.contains("electric_potential")
1343    {
1344        return AnalysisFieldLocation::Node;
1345    }
1346    AnalysisFieldLocation::Element
1347}
1348
1349fn infer_field_kind(field_id: &str, shape: &[usize]) -> AnalysisFieldKind {
1350    let normalized = field_id.to_ascii_lowercase();
1351    if normalized.contains("magnitude") {
1352        return AnalysisFieldKind::Scalar;
1353    }
1354    if normalized.contains("heat_flux") {
1355        return match shape {
1356            [_, components] if (2..=3).contains(components) => AnalysisFieldKind::Vector,
1357            _ => AnalysisFieldKind::Scalar,
1358        };
1359    }
1360    if normalized.contains("beam_shear_force")
1361        || normalized.contains("beam_bending_moment")
1362        || normalized.contains("beam_bending_stress")
1363        || normalized.contains("shell_membrane_force")
1364        || normalized.contains("shell_bending_moment")
1365        || normalized.contains("shell_transverse_shear")
1366    {
1367        return AnalysisFieldKind::Vector;
1368    }
1369    if normalized.contains("shell_von_mises") {
1370        return AnalysisFieldKind::Scalar;
1371    }
1372    if normalized.contains("beam_axial_force")
1373        || normalized.contains("beam_torsion_moment")
1374        || normalized.contains("beam_torsion_stress")
1375    {
1376        return AnalysisFieldKind::Scalar;
1377    }
1378    if normalized.contains("temperature_gradient")
1379        || normalized.contains("wall_shear_stress")
1380        || normalized.contains("vorticity")
1381        || normalized.contains("velocity")
1382        || normalized.contains("traction")
1383        || normalized.contains("magnetic_field")
1384        || normalized.contains("electric_field")
1385        || normalized.contains("current_density")
1386    {
1387        return AnalysisFieldKind::Vector;
1388    }
1389    if normalized.contains("von_mises")
1390        || normalized.contains("equivalent_plastic_strain")
1391        || normalized.contains("pressure")
1392        || normalized.contains("phase")
1393        || normalized.contains("reynolds_number")
1394        || normalized.contains("temperature")
1395        || normalized.contains("energy")
1396        || normalized.contains("residual")
1397    {
1398        return AnalysisFieldKind::Scalar;
1399    }
1400    if normalized.contains("stress") || normalized.contains("strain") {
1401        return AnalysisFieldKind::Tensor;
1402    }
1403    if normalized.contains("orthogonality") {
1404        return AnalysisFieldKind::Tensor;
1405    }
1406    if normalized.contains("displacement")
1407        || normalized.contains("rotation")
1408        || normalized.contains("mode_shape")
1409        || normalized.contains("reaction_force")
1410        || normalized.contains("reaction_moment")
1411        || normalized.contains("vector")
1412        || normalized.contains("flux")
1413    {
1414        return AnalysisFieldKind::Vector;
1415    }
1416    match shape {
1417        [] | [_] => AnalysisFieldKind::Scalar,
1418        [_, 1] => AnalysisFieldKind::Scalar,
1419        [_, 2] | [_, 3] => AnalysisFieldKind::Vector,
1420        [_, _, ..] => AnalysisFieldKind::Tensor,
1421    }
1422}
1423
1424fn infer_component_count(field_id: &str, shape: &[usize]) -> Option<usize> {
1425    let normalized = field_id.to_ascii_lowercase();
1426    if normalized.contains("magnitude") {
1427        return None;
1428    }
1429    if normalized.contains("equivalent_plastic_strain") {
1430        return None;
1431    }
1432    if normalized.contains("heat_flux") {
1433        return match shape {
1434            [_, components] if (2..=3).contains(components) => Some(*components),
1435            _ => None,
1436        };
1437    }
1438    if normalized.contains("beam_shear_force")
1439        || normalized.contains("beam_bending_moment")
1440        || normalized.contains("beam_bending_stress")
1441    {
1442        return Some(2);
1443    }
1444    if normalized.contains("shell_membrane_force") || normalized.contains("shell_bending_moment") {
1445        return Some(3);
1446    }
1447    if normalized.contains("shell_transverse_shear") {
1448        return Some(2);
1449    }
1450    if normalized.contains("shell_von_mises") {
1451        return None;
1452    }
1453    if normalized.contains("beam_axial_force")
1454        || normalized.contains("beam_torsion_moment")
1455        || normalized.contains("beam_torsion_stress")
1456    {
1457        return None;
1458    }
1459    if normalized.contains("temperature_gradient")
1460        || normalized.contains("wall_shear_stress")
1461        || normalized.contains("vorticity")
1462        || normalized.contains("velocity")
1463        || normalized.contains("traction")
1464        || normalized.contains("magnetic_field")
1465        || normalized.contains("electric_field")
1466        || normalized.contains("current_density")
1467    {
1468        return Some(
1469            shape
1470                .last()
1471                .copied()
1472                .filter(|value| (2..=3).contains(value))
1473                .unwrap_or(3),
1474        );
1475    }
1476    if normalized.contains("von_mises")
1477        || normalized.contains("equivalent_plastic_strain")
1478        || normalized.contains("pressure")
1479        || normalized.contains("phase")
1480        || normalized.contains("reynolds_number")
1481        || normalized.contains("temperature")
1482        || normalized.contains("energy")
1483        || normalized.contains("residual")
1484    {
1485        return None;
1486    }
1487    if normalized.contains("displacement")
1488        || normalized.contains("rotation")
1489        || normalized.contains("mode_shape")
1490        || normalized.contains("reaction_force")
1491        || normalized.contains("reaction_moment")
1492        || normalized.contains("particle_velocity")
1493        || normalized.contains("vector")
1494        || normalized.contains("flux")
1495    {
1496        return Some(
1497            shape
1498                .last()
1499                .copied()
1500                .filter(|value| (2..=6).contains(value))
1501                .unwrap_or(3),
1502        );
1503    }
1504    if normalized.contains("stress") || normalized.contains("strain") {
1505        return Some(
1506            shape
1507                .last()
1508                .copied()
1509                .filter(|value| *value == 6)
1510                .unwrap_or(6),
1511        );
1512    }
1513    if normalized.contains("orthogonality") {
1514        return None;
1515    }
1516    match shape {
1517        [_, count] if (1..=6).contains(count) => Some(*count),
1518        _ => None,
1519    }
1520}
1521
1522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1523pub struct AnalysisResultsSummary {
1524    pub field_count: usize,
1525    pub total_elements: usize,
1526    pub mode_count: usize,
1527    pub available_mode_indices: Vec<usize>,
1528    pub min_frequency_hz: Option<f64>,
1529    pub max_frequency_hz: Option<f64>,
1530    pub max_modal_residual_norm: Option<f64>,
1531    pub first_mode_converged: Option<bool>,
1532    pub snapshot_count: usize,
1533    pub time_start_s: Option<f64>,
1534    pub time_end_s: Option<f64>,
1535    pub max_transient_residual_norm: Option<f64>,
1536    pub final_step_converged: Option<bool>,
1537    pub increment_count: usize,
1538    pub failed_increment_count: Option<usize>,
1539    pub max_nonlinear_residual_norm: Option<f64>,
1540    pub max_nonlinear_increment_norm: Option<f64>,
1541    pub max_nonlinear_iteration_count: Option<usize>,
1542    pub final_increment_converged: Option<bool>,
1543    pub nonlinear_line_search_backtracks: Option<usize>,
1544    pub nonlinear_max_backtracks_per_increment: Option<usize>,
1545    pub nonlinear_tangent_rebuild_count: Option<usize>,
1546    pub nonlinear_iteration_spike_count: Option<usize>,
1547    pub nonlinear_convergence_stall_count: Option<usize>,
1548    pub nonlinear_backtrack_burst_count: Option<usize>,
1549    pub prep_calibration_profile: Option<String>,
1550    pub prep_calibration_fingerprint: Option<u64>,
1551    pub prep_acceptance_score: Option<f64>,
1552    pub prep_acceptance_passed: Option<bool>,
1553    pub prep_acceptance_fingerprint: Option<u64>,
1554    pub thermo_coupling_enabled: Option<bool>,
1555    pub thermo_coupling_fingerprint: Option<u64>,
1556    pub thermo_constitutive_temperature_factor: Option<f64>,
1557    pub thermo_effective_modulus_scale: Option<f64>,
1558    pub thermo_constitutive_material_spread_ratio: Option<f64>,
1559    pub thermo_assignment_heterogeneity_index: Option<f64>,
1560    pub thermo_region_delta_count: Option<f64>,
1561    pub thermo_spatial_coverage_ratio: Option<f64>,
1562    pub thermo_field_extrapolation_ratio: Option<f64>,
1563    pub thermo_field_clamp_ratio: Option<f64>,
1564    pub thermo_transient_severity: Option<f64>,
1565    pub thermo_nonlinear_severity: Option<f64>,
1566    pub electro_thermal_coupling_enabled: Option<bool>,
1567    pub electro_thermal_coupling_fingerprint: Option<u64>,
1568    pub electro_joule_heating_scale: Option<f64>,
1569    pub electro_conductivity_spread_ratio: Option<f64>,
1570    pub electro_transient_severity: Option<f64>,
1571    pub electro_transient_time_scale_mean: Option<f64>,
1572    pub electro_nonlinear_severity: Option<f64>,
1573    pub electro_nonlinear_time_scale_mean: Option<f64>,
1574    pub plastic_nonlinear_severity: Option<f64>,
1575    pub plastic_nonlinear_severity_mean: Option<f64>,
1576    pub plastic_load_realization_ratio: Option<f64>,
1577    pub plastic_load_amplification_ratio: Option<f64>,
1578    pub contact_nonlinear_severity: Option<f64>,
1579    pub contact_nonlinear_severity_mean: Option<f64>,
1580    pub contact_load_realization_ratio: Option<f64>,
1581    pub contact_load_amplification_ratio: Option<f64>,
1582    pub thermal_max_residual_norm: Option<f64>,
1583    pub thermal_min_temperature_k: Option<f64>,
1584    pub thermal_max_temperature_k: Option<f64>,
1585    pub thermal_conductivity_spread_ratio: Option<f64>,
1586    pub thermal_heat_capacity_spread_ratio: Option<f64>,
1587    pub thermal_spatial_gradient_index: Option<f64>,
1588    pub thermal_monotonic_response_fraction: Option<f64>,
1589    pub thermal_response_realization_ratio: Option<f64>,
1590    pub electromagnetic_enabled: Option<bool>,
1591    pub electromagnetic_formulation_coverage_ratio: Option<f64>,
1592    pub electromagnetic_magnetostatic_curl_curl_coverage_ratio: Option<f64>,
1593    pub electromagnetic_magnetoquasistatic_eddy_current_coverage_ratio: Option<f64>,
1594    pub electromagnetic_full_wave_displacement_current_coverage_ratio: Option<f64>,
1595    pub electromagnetic_displacement_to_conduction_ratio: Option<f64>,
1596    pub electromagnetic_material_frequency_response_coverage_ratio: Option<f64>,
1597    pub electromagnetic_reference_frequency_hz: Option<f64>,
1598    pub electromagnetic_applied_current_a: Option<f64>,
1599    pub electromagnetic_solve_quality: Option<f64>,
1600    pub electromagnetic_conductivity_spread_ratio: Option<f64>,
1601    pub electromagnetic_relative_permittivity_spread_ratio: Option<f64>,
1602    pub electromagnetic_relative_permeability_spread_ratio: Option<f64>,
1603    pub electromagnetic_material_heterogeneity_index: Option<f64>,
1604    pub electromagnetic_assignment_coverage_ratio: Option<f64>,
1605    pub electromagnetic_assigned_coefficient_coverage_ratio: Option<f64>,
1606    pub electromagnetic_region_coefficient_contrast_index: Option<f64>,
1607    pub electromagnetic_condition_number_estimate: Option<f64>,
1608    pub electromagnetic_source_realization_ratio: Option<f64>,
1609    pub electromagnetic_source_region_coverage_ratio: Option<f64>,
1610    pub electromagnetic_source_material_alignment_ratio: Option<f64>,
1611    pub electromagnetic_source_localization_ratio: Option<f64>,
1612    pub electromagnetic_source_overlap_ratio: Option<f64>,
1613    pub electromagnetic_source_interference_index: Option<f64>,
1614    pub electromagnetic_boundary_anchor_ratio: Option<f64>,
1615    pub electromagnetic_boundary_condition_localization_ratio: Option<f64>,
1616    pub electromagnetic_ground_anchor_effectiveness_ratio: Option<f64>,
1617    pub electromagnetic_insulation_leakage_ratio: Option<f64>,
1618    pub electromagnetic_flux_divergence_ratio: Option<f64>,
1619    pub electromagnetic_energy_imbalance_ratio: Option<f64>,
1620    pub electromagnetic_boundary_energy_ratio: Option<f64>,
1621    pub electromagnetic_boundary_penalty_conditioning_contribution: Option<f64>,
1622    pub electromagnetic_source_region_energy_consistency_ratio: Option<f64>,
1623    pub electromagnetic_real_residual_norm: Option<f64>,
1624    pub electromagnetic_imag_residual_norm: Option<f64>,
1625    pub electromagnetic_sweep_count: Option<f64>,
1626    pub electromagnetic_resonance_peak_frequency_hz: Option<f64>,
1627    pub electromagnetic_resonance_peak_flux_density: Option<f64>,
1628    pub electromagnetic_resonance_bandwidth_hz: Option<f64>,
1629    pub electromagnetic_resonance_quality_factor: Option<f64>,
1630    pub electromagnetic_resonance_flux_gain: Option<f64>,
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1634pub struct AnalysisResultsData {
1635    #[serde(default)]
1636    pub field_descriptors: Vec<AnalysisFieldDescriptor>,
1637    pub fields: Vec<AnalysisField>,
1638    pub modal_results: Option<ModalResultsData>,
1639    #[serde(default)]
1640    pub thermal_results: Option<ThermalResultsData>,
1641    pub transient_results: Option<TransientResultsData>,
1642    pub nonlinear_results: Option<NonlinearResultsData>,
1643    #[serde(default)]
1644    pub electromagnetic_results: Option<ElectromagneticResultsData>,
1645    pub diagnostics: Option<Vec<FeaDiagnostic>>,
1646    pub run_status: RunStatus,
1647    pub publishable: bool,
1648    pub quality_reasons: Vec<QualityReason>,
1649    pub provenance: RunProvenance,
1650    pub summary: AnalysisResultsSummary,
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1654pub struct AnalysisResultsCompareQuery {
1655    pub baseline_run_id: String,
1656    pub candidate_run_id: String,
1657}
1658
1659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1660pub struct AnalysisResultsCompareData {
1661    pub baseline_run_id: String,
1662    pub candidate_run_id: String,
1663    pub publishable_changed: bool,
1664    pub run_status_changed: bool,
1665    pub quality_reason_count_delta: i64,
1666    pub failed_increment_delta: Option<i64>,
1667    pub max_iteration_delta: Option<i64>,
1668    pub nonlinear_spike_count_delta: Option<i64>,
1669    pub nonlinear_stall_count_delta: Option<i64>,
1670    pub solve_ms_delta: Option<f64>,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1674pub struct AnalysisTrendsQuery {
1675    pub window_size: usize,
1676}
1677
1678impl Default for AnalysisTrendsQuery {
1679    fn default() -> Self {
1680        Self { window_size: 16 }
1681    }
1682}
1683
1684#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1685pub struct AnalysisStudySpec {
1686    pub study_id: String,
1687    pub geometry: GeometryAsset,
1688    pub create_model_intent: AnalysisCreateModelIntentSpec,
1689    #[serde(default)]
1690    pub model: Option<AnalysisModel>,
1691    pub run_kind: AnalysisRunKind,
1692    pub backend: ComputeBackend,
1693    #[serde(default)]
1694    pub linear_static_run_options: Option<AnalysisRunOptions>,
1695    #[serde(default)]
1696    pub modal_run_options: Option<AnalysisModalRunOptions>,
1697    #[serde(default)]
1698    pub acoustic_run_options: Option<AnalysisAcousticRunOptions>,
1699    #[serde(default)]
1700    pub thermal_run_options: Option<AnalysisThermalRunOptions>,
1701    #[serde(default)]
1702    pub transient_run_options: Option<AnalysisTransientRunOptions>,
1703    #[serde(default)]
1704    pub cfd_run_options: Option<AnalysisCfdRunOptions>,
1705    #[serde(default)]
1706    pub cht_run_options: Option<AnalysisChtRunOptions>,
1707    #[serde(default)]
1708    pub fsi_run_options: Option<AnalysisFsiRunOptions>,
1709    #[serde(default)]
1710    pub nonlinear_run_options: Option<AnalysisNonlinearRunOptions>,
1711    #[serde(default)]
1712    pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
1713}
1714
1715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1716pub struct AnalysisStudyValidateResult {
1717    pub valid: bool,
1718    pub issue_codes: Vec<String>,
1719    #[serde(default)]
1720    pub issues: Vec<AnalysisStudyIssue>,
1721    pub evidence_artifact_path: String,
1722}
1723
1724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1725pub struct AnalysisStudyIssue {
1726    pub code: String,
1727    pub message: String,
1728}
1729
1730#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1731pub struct AnalysisStudyPlanData {
1732    pub study_id: String,
1733    pub model_id: String,
1734    pub run_kind: AnalysisRunKind,
1735    pub backend: ComputeBackend,
1736    #[serde(default)]
1737    pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
1738    #[serde(default)]
1739    pub run_options: serde_json::Value,
1740    pub operation_sequence: Vec<String>,
1741    pub run_operation: String,
1742    pub run_op_version: String,
1743    pub study_fingerprint: String,
1744    pub evidence_artifact_path: String,
1745}
1746
1747#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1748pub struct AnalysisStudyRunData {
1749    pub study_id: String,
1750    pub model_id: String,
1751    pub run_kind: AnalysisRunKind,
1752    pub backend: ComputeBackend,
1753    #[serde(default)]
1754    pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
1755    #[serde(default)]
1756    pub run_options: serde_json::Value,
1757    #[serde(default)]
1758    pub prep_artifact_id: Option<String>,
1759    pub study_fingerprint: String,
1760    pub operation_sequence: Vec<String>,
1761    pub run_operation: String,
1762    pub run_op_version: String,
1763    pub run_id: String,
1764    pub run_status: RunStatus,
1765    pub publishable: bool,
1766    pub solver_convergence: QualityGate,
1767    pub result_quality: QualityGate,
1768    #[serde(default)]
1769    pub quality_reasons: Vec<QualityReason>,
1770    pub provenance: RunProvenance,
1771    pub evidence_artifact_path: String,
1772}
1773
1774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1775pub struct AnalysisStudySweepSpec {
1776    pub sweep_id: String,
1777    pub studies: Vec<AnalysisStudySpec>,
1778    #[serde(default = "default_study_sweep_fail_fast")]
1779    pub fail_fast: bool,
1780}
1781
1782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1783pub struct AnalysisStudySweepValidateEntry {
1784    pub study_id: String,
1785    pub valid: bool,
1786    pub issue_codes: Vec<String>,
1787    #[serde(default)]
1788    pub issues: Vec<AnalysisStudyIssue>,
1789}
1790
1791#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1792pub struct AnalysisStudySweepValidateData {
1793    pub sweep_id: String,
1794    pub valid: bool,
1795    pub issue_codes: Vec<String>,
1796    pub study_entries: Vec<AnalysisStudySweepValidateEntry>,
1797    pub evidence_artifact_path: String,
1798}
1799
1800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1801pub struct AnalysisStudySweepPlanEntry {
1802    pub study_id: String,
1803    pub model_id: String,
1804    pub run_kind: AnalysisRunKind,
1805    pub backend: ComputeBackend,
1806    #[serde(default)]
1807    pub electromagnetic_run_options: Option<AnalysisElectromagneticRunOptions>,
1808    #[serde(default)]
1809    pub run_options: serde_json::Value,
1810    pub operation_sequence: Vec<String>,
1811    pub run_operation: String,
1812    pub run_op_version: String,
1813    pub study_fingerprint: String,
1814}
1815
1816#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1817pub struct AnalysisStudySweepPlanData {
1818    pub sweep_id: String,
1819    pub study_count: usize,
1820    pub planned_count: usize,
1821    pub failed_count: usize,
1822    #[serde(default)]
1823    pub failure_entries: Vec<AnalysisStudySweepFailureEntry>,
1824    pub plan_entries: Vec<AnalysisStudySweepPlanEntry>,
1825    pub evidence_artifact_path: String,
1826}
1827
1828#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1829pub struct AnalysisStudySweepRunEntry {
1830    pub study_id: String,
1831    pub run_kind: AnalysisRunKind,
1832    pub run_id: String,
1833    pub run_status: RunStatus,
1834    pub publishable: bool,
1835    pub run_operation: String,
1836    pub run_op_version: String,
1837}
1838
1839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1840pub struct AnalysisStudySweepData {
1841    pub sweep_id: String,
1842    pub study_count: usize,
1843    pub success_count: usize,
1844    pub failed_count: usize,
1845    #[serde(default)]
1846    pub failure_entries: Vec<AnalysisStudySweepFailureEntry>,
1847    pub run_entries: Vec<AnalysisStudySweepRunEntry>,
1848    pub evidence_artifact_path: String,
1849}
1850
1851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1852pub struct AnalysisStudySweepFailureEntry {
1853    pub study_id: String,
1854    pub study_index: usize,
1855    pub error_code: String,
1856    pub message: String,
1857}
1858
1859fn default_study_sweep_fail_fast() -> bool {
1860    true
1861}
1862
1863#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1864#[serde(rename_all = "snake_case")]
1865pub enum AnalysisRunKind {
1866    LinearStatic,
1867    Modal,
1868    Acoustic,
1869    Thermal,
1870    Transient,
1871    Cfd,
1872    Cht,
1873    Fsi,
1874    Nonlinear,
1875    Electromagnetic,
1876}
1877
1878impl AnalysisCreateModelProfile {
1879    pub fn derived_run_kind(self) -> AnalysisRunKind {
1880        match self {
1881            AnalysisCreateModelProfile::LinearStaticStructural => AnalysisRunKind::LinearStatic,
1882            AnalysisCreateModelProfile::ThermoMechanicalCoupled => AnalysisRunKind::Transient,
1883            AnalysisCreateModelProfile::ThermalStandalone => AnalysisRunKind::Thermal,
1884            AnalysisCreateModelProfile::ModalStructural => AnalysisRunKind::Modal,
1885            AnalysisCreateModelProfile::AcousticHarmonic => AnalysisRunKind::Acoustic,
1886            AnalysisCreateModelProfile::TransientStructural => AnalysisRunKind::Transient,
1887            AnalysisCreateModelProfile::NonlinearStructural => AnalysisRunKind::Nonlinear,
1888            AnalysisCreateModelProfile::ElectromagneticStatic => AnalysisRunKind::Electromagnetic,
1889            AnalysisCreateModelProfile::CfdSteadyState
1890            | AnalysisCreateModelProfile::CfdTransient => AnalysisRunKind::Cfd,
1891            AnalysisCreateModelProfile::ChtCoupled => AnalysisRunKind::Cht,
1892            AnalysisCreateModelProfile::FsiCoupled => AnalysisRunKind::Fsi,
1893        }
1894    }
1895}
1896
1897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1898pub struct AnalysisTrendKindSummary {
1899    pub run_kind: AnalysisRunKind,
1900    pub sample_count: usize,
1901    pub median_solve_ms: Option<f64>,
1902    pub p95_solve_ms: Option<f64>,
1903    pub publishable_rate: f64,
1904    pub failed_increment_rate: Option<f64>,
1905    pub mean_spike_count: Option<f64>,
1906    pub mean_stall_count: Option<f64>,
1907    pub prep_acceptance_rate: Option<f64>,
1908    pub prep_calibration_fast_rate: Option<f64>,
1909    pub prep_calibration_balanced_rate: Option<f64>,
1910    pub prep_calibration_conservative_rate: Option<f64>,
1911    pub thermo_coupling_enabled_rate: Option<f64>,
1912    pub thermo_transient_warn_rate: Option<f64>,
1913    pub thermo_nonlinear_warn_rate: Option<f64>,
1914    pub thermo_spread_breach_rate: Option<f64>,
1915    pub thermo_heterogeneity_breach_rate: Option<f64>,
1916    pub electro_thermal_coupling_enabled_rate: Option<f64>,
1917    pub electro_transient_warn_rate: Option<f64>,
1918    pub electro_nonlinear_warn_rate: Option<f64>,
1919    pub plastic_nonlinear_warn_rate: Option<f64>,
1920    pub contact_nonlinear_warn_rate: Option<f64>,
1921    pub thermal_stability_warn_rate: Option<f64>,
1922    pub thermal_constitutive_warn_rate: Option<f64>,
1923    pub thermal_spread_breach_rate: Option<f64>,
1924    pub electromagnetic_solve_warn_rate: Option<f64>,
1925    pub electromagnetic_spread_breach_rate: Option<f64>,
1926    pub electromagnetic_heterogeneity_breach_rate: Option<f64>,
1927    pub electromagnetic_coverage_breach_rate: Option<f64>,
1928    pub electromagnetic_contrast_breach_rate: Option<f64>,
1929    pub electromagnetic_conditioning_breach_rate: Option<f64>,
1930    pub electromagnetic_source_realization_breach_rate: Option<f64>,
1931    pub electromagnetic_source_region_coverage_breach_rate: Option<f64>,
1932    pub electromagnetic_source_material_alignment_breach_rate: Option<f64>,
1933    pub electromagnetic_source_overlap_breach_rate: Option<f64>,
1934    pub electromagnetic_source_interference_breach_rate: Option<f64>,
1935    pub electromagnetic_boundary_anchor_breach_rate: Option<f64>,
1936    pub electromagnetic_boundary_localization_breach_rate: Option<f64>,
1937    pub electromagnetic_ground_effectiveness_breach_rate: Option<f64>,
1938    pub electromagnetic_insulation_leakage_breach_rate: Option<f64>,
1939    pub electromagnetic_divergence_breach_rate: Option<f64>,
1940    pub electromagnetic_energy_imbalance_breach_rate: Option<f64>,
1941    pub electromagnetic_boundary_energy_breach_rate: Option<f64>,
1942    pub electromagnetic_boundary_penalty_contribution_breach_rate: Option<f64>,
1943    pub electromagnetic_source_region_energy_consistency_breach_rate: Option<f64>,
1944    pub electromagnetic_real_residual_breach_rate: Option<f64>,
1945    pub electromagnetic_imag_residual_breach_rate: Option<f64>,
1946    pub electromagnetic_sweep_coverage_breach_rate: Option<f64>,
1947    pub electromagnetic_resonance_sharpness_breach_rate: Option<f64>,
1948}
1949
1950#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1951pub struct AnalysisTrendsData {
1952    pub window_size: usize,
1953    pub summaries: Vec<AnalysisTrendKindSummary>,
1954}
1955
1956#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1957pub struct ModalResultsData {
1958    pub modal_payload_version: String,
1959    pub eigenvalues_hz: Vec<f64>,
1960    pub mode_shapes: Vec<AnalysisField>,
1961    pub residual_norms: Vec<f64>,
1962    pub mode_units: ModalFrequencyUnits,
1963    pub frequency_basis: ModalFrequencyBasis,
1964}
1965
1966#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1967pub struct ThermalResultsData {
1968    pub thermal_payload_version: String,
1969    pub time_points_s: Vec<f64>,
1970    pub temperature_snapshots: Vec<AnalysisField>,
1971    #[serde(default)]
1972    pub temperature_gradient_snapshots: Vec<AnalysisField>,
1973    #[serde(default)]
1974    pub heat_flux_snapshots: Vec<AnalysisField>,
1975    #[serde(default)]
1976    pub heat_source_snapshots: Vec<AnalysisField>,
1977    #[serde(default)]
1978    pub boundary_heat_flux_snapshots: Vec<AnalysisField>,
1979    pub residual_norms: Vec<f64>,
1980    pub reference_temperature_k: f64,
1981}
1982
1983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1984pub struct TransientResultsData {
1985    pub transient_payload_version: String,
1986    pub time_points_s: Vec<f64>,
1987    pub displacement_snapshots: Vec<AnalysisField>,
1988    #[serde(default)]
1989    pub rotation_snapshots: Vec<AnalysisField>,
1990    #[serde(default)]
1991    pub velocity_snapshots: Vec<AnalysisField>,
1992    #[serde(default)]
1993    pub angular_velocity_snapshots: Vec<AnalysisField>,
1994    #[serde(default)]
1995    pub acceleration_snapshots: Vec<AnalysisField>,
1996    #[serde(default)]
1997    pub angular_acceleration_snapshots: Vec<AnalysisField>,
1998    #[serde(default)]
1999    pub von_mises_snapshots: Vec<AnalysisField>,
2000    #[serde(default)]
2001    pub kinetic_energy_snapshots: Vec<AnalysisField>,
2002    #[serde(default)]
2003    pub strain_energy_snapshots: Vec<AnalysisField>,
2004    #[serde(default)]
2005    pub residual_norm_snapshots: Vec<AnalysisField>,
2006    #[serde(default)]
2007    pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
2008    #[serde(default)]
2009    pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
2010    #[serde(default)]
2011    pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
2012    #[serde(default)]
2013    pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
2014    #[serde(default)]
2015    pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
2016    #[serde(default)]
2017    pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
2018    #[serde(default)]
2019    pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
2020    #[serde(default)]
2021    pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
2022    pub residual_norms: Vec<f64>,
2023    pub integration_method: TransientIntegrationMethod,
2024}
2025
2026#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2027pub struct NonlinearResultsData {
2028    pub nonlinear_payload_version: String,
2029    pub load_factors: Vec<f64>,
2030    pub displacement_snapshots: Vec<AnalysisField>,
2031    #[serde(default)]
2032    pub rotation_snapshots: Vec<AnalysisField>,
2033    #[serde(default)]
2034    pub von_mises_snapshots: Vec<AnalysisField>,
2035    #[serde(default)]
2036    pub plastic_strain_snapshots: Vec<AnalysisField>,
2037    #[serde(default)]
2038    pub equivalent_plastic_strain_snapshots: Vec<AnalysisField>,
2039    #[serde(default)]
2040    pub contact_pressure_snapshots: Vec<AnalysisField>,
2041    #[serde(default)]
2042    pub contact_gap_snapshots: Vec<AnalysisField>,
2043    #[serde(default)]
2044    pub load_factor_snapshots: Vec<AnalysisField>,
2045    #[serde(default)]
2046    pub residual_norm_snapshots: Vec<AnalysisField>,
2047    #[serde(default)]
2048    pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
2049    #[serde(default)]
2050    pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
2051    #[serde(default)]
2052    pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
2053    #[serde(default)]
2054    pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
2055    #[serde(default)]
2056    pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
2057    #[serde(default)]
2058    pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
2059    #[serde(default)]
2060    pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
2061    #[serde(default)]
2062    pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
2063    pub residual_norms: Vec<f64>,
2064    #[serde(default)]
2065    pub increment_norms: Vec<f64>,
2066    #[serde(default)]
2067    pub iteration_counts: Vec<usize>,
2068    #[serde(default)]
2069    pub failed_increments: usize,
2070    #[serde(default)]
2071    pub line_search_backtracks: usize,
2072    #[serde(default)]
2073    pub max_line_search_backtracks_per_increment: usize,
2074    #[serde(default)]
2075    pub tangent_rebuild_count: usize,
2076    #[serde(default)]
2077    pub iteration_spike_count: usize,
2078    #[serde(default)]
2079    pub convergence_stall_count: usize,
2080    #[serde(default)]
2081    pub backtrack_burst_count: usize,
2082    pub method: NonlinearMethod,
2083}
2084
2085#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2086pub struct ElectromagneticResultsData {
2087    pub electromagnetic_payload_version: String,
2088    pub reference_frequency_hz: f64,
2089    pub applied_current_a: f64,
2090    pub vector_potential_real: AnalysisField,
2091    pub vector_potential_imag: AnalysisField,
2092    pub magnetic_flux_density_real: AnalysisField,
2093    pub magnetic_flux_density_imag: AnalysisField,
2094    pub magnetic_flux_density_magnitude: AnalysisField,
2095    pub magnetic_field_real: AnalysisField,
2096    pub magnetic_field_imag: AnalysisField,
2097    pub current_density_real: AnalysisField,
2098    pub current_density_imag: AnalysisField,
2099    pub electric_field_real: AnalysisField,
2100    pub electric_field_imag: AnalysisField,
2101    pub power_loss_density: AnalysisField,
2102    pub energy_density: AnalysisField,
2103    pub residual_real: AnalysisField,
2104    pub residual_imag: AnalysisField,
2105    pub electric_flux_density_real: AnalysisField,
2106    pub electric_flux_density_imag: AnalysisField,
2107    pub poynting_vector_real: AnalysisField,
2108    pub poynting_vector_imag: AnalysisField,
2109    #[serde(default)]
2110    pub sweep_frequency_hz: Vec<f64>,
2111    #[serde(default)]
2112    pub sweep_peak_flux_density: Vec<f64>,
2113    #[serde(default)]
2114    pub sweep_solve_quality: Vec<f64>,
2115    #[serde(default)]
2116    pub resonance_peak_frequency_hz: Option<f64>,
2117    #[serde(default)]
2118    pub resonance_peak_flux_density: Option<f64>,
2119    #[serde(default)]
2120    pub resonance_bandwidth_hz: Option<f64>,
2121    #[serde(default)]
2122    pub resonance_quality_factor: Option<f64>,
2123    #[serde(default)]
2124    pub resonance_flux_gain: Option<f64>,
2125}
2126
2127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2128#[serde(rename_all = "snake_case")]
2129pub enum TransientIntegrationMethod {
2130    ImplicitEuler,
2131}
2132
2133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2134#[serde(rename_all = "snake_case")]
2135pub enum NonlinearMethod {
2136    IncrementalNewtonRaphson,
2137}
2138
2139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2140#[serde(rename_all = "snake_case")]
2141pub enum ModalFrequencyUnits {
2142    Hz,
2143}
2144
2145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2146#[serde(rename_all = "snake_case")]
2147pub enum ModalFrequencyBasis {
2148    NativeEigenSolve,
2149}
2150
2151pub(crate) fn format_precision_mode(mode: PrecisionMode) -> String {
2152    match mode {
2153        PrecisionMode::Fp32 => "fp32".to_string(),
2154        PrecisionMode::Fp64 => "fp64".to_string(),
2155        PrecisionMode::Mixed => "mixed".to_string(),
2156    }
2157}
2158
2159pub(crate) fn format_quality_policy(mode: QualityPolicy) -> String {
2160    match mode {
2161        QualityPolicy::Strict => "strict".to_string(),
2162        QualityPolicy::Balanced => "balanced".to_string(),
2163        QualityPolicy::Exploratory => "exploratory".to_string(),
2164    }
2165}