Skip to main content

runmat_analysis_fea/contracts/
mod.rs

1use runmat_analysis_core::AnalysisField;
2use runmat_meshing_core::AnalysisMeshArtifact;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::solve::{
7    backend::kind::LinearAlgebraBackendKind, preconditioner::SpdPreconditionerKind,
8};
9
10pub const FEA_FIELD_STRUCTURAL_DISPLACEMENT: &str = "structural.displacement";
11pub const FEA_FIELD_STRUCTURAL_ROTATION: &str = "structural.rotation";
12pub const FEA_FIELD_STRUCTURAL_VON_MISES: &str = "structural.von_mises";
13pub const FEA_FIELD_STRUCTURAL_NODAL_VON_MISES: &str = "structural.nodal_von_mises";
14pub const FEA_FIELD_STRUCTURAL_STRAIN: &str = "structural.strain";
15pub const FEA_FIELD_STRUCTURAL_STRESS: &str = "structural.stress";
16pub const FEA_FIELD_STRUCTURAL_REACTION_FORCE: &str = "structural.reaction_force";
17pub const FEA_FIELD_STRUCTURAL_REACTION_MOMENT: &str = "structural.reaction_moment";
18pub const FEA_FIELD_STRUCTURAL_STRAIN_ENERGY_DENSITY: &str = "structural.strain_energy_density";
19pub const FEA_FIELD_STRUCTURAL_TOTAL_STRAIN_ENERGY: &str = "structural.total_strain_energy";
20pub const FEA_FIELD_STRUCTURAL_RESIDUAL_NORM: &str = "structural.residual_norm";
21pub const FEA_FIELD_STRUCTURAL_EQUATION_SCALE: &str = "structural.equation_scale";
22pub const FEA_FIELD_STRUCTURAL_BEAM_AXIAL_FORCE: &str = "structural.beam_axial_force";
23pub const FEA_FIELD_STRUCTURAL_BEAM_SHEAR_FORCE: &str = "structural.beam_shear_force";
24pub const FEA_FIELD_STRUCTURAL_BEAM_TORSION_MOMENT: &str = "structural.beam_torsion_moment";
25pub const FEA_FIELD_STRUCTURAL_BEAM_BENDING_MOMENT: &str = "structural.beam_bending_moment";
26pub const FEA_FIELD_STRUCTURAL_BEAM_BENDING_STRESS: &str = "structural.beam_bending_stress";
27pub const FEA_FIELD_STRUCTURAL_BEAM_TORSION_STRESS: &str = "structural.beam_torsion_stress";
28pub const FEA_FIELD_STRUCTURAL_SHELL_MEMBRANE_FORCE: &str = "structural.shell_membrane_force";
29pub const FEA_FIELD_STRUCTURAL_SHELL_BENDING_MOMENT: &str = "structural.shell_bending_moment";
30pub const FEA_FIELD_STRUCTURAL_SHELL_TRANSVERSE_SHEAR: &str = "structural.shell_transverse_shear";
31pub const FEA_FIELD_STRUCTURAL_SHELL_VON_MISES: &str = "structural.shell_von_mises";
32pub const FEA_FIELD_EM_VECTOR_POTENTIAL_REAL: &str = "em.vector_potential_real";
33pub const FEA_FIELD_EM_VECTOR_POTENTIAL_IMAG: &str = "em.vector_potential_imag";
34pub const FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_REAL: &str = "em.magnetic_flux_density_real";
35pub const FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_IMAG: &str = "em.magnetic_flux_density_imag";
36pub const FEA_FIELD_EM_MAGNETIC_FLUX_DENSITY_MAGNITUDE: &str = "em.magnetic_flux_density_magnitude";
37pub const FEA_FIELD_EM_MAGNETIC_FIELD_REAL: &str = "em.magnetic_field_real";
38pub const FEA_FIELD_EM_MAGNETIC_FIELD_IMAG: &str = "em.magnetic_field_imag";
39pub const FEA_FIELD_EM_CURRENT_DENSITY_REAL: &str = "em.current_density_real";
40pub const FEA_FIELD_EM_CURRENT_DENSITY_IMAG: &str = "em.current_density_imag";
41pub const FEA_FIELD_EM_ELECTRIC_FIELD_REAL: &str = "em.electric_field_real";
42pub const FEA_FIELD_EM_ELECTRIC_FIELD_IMAG: &str = "em.electric_field_imag";
43pub const FEA_FIELD_EM_POWER_LOSS_DENSITY: &str = "em.power_loss_density";
44pub const FEA_FIELD_EM_ENERGY_DENSITY: &str = "em.energy_density";
45pub const FEA_FIELD_EM_RESIDUAL_REAL: &str = "em.residual_real";
46pub const FEA_FIELD_EM_RESIDUAL_IMAG: &str = "em.residual_imag";
47pub const FEA_FIELD_EM_ELECTRIC_FLUX_DENSITY_REAL: &str = "em.electric_flux_density_real";
48pub const FEA_FIELD_EM_ELECTRIC_FLUX_DENSITY_IMAG: &str = "em.electric_flux_density_imag";
49pub const FEA_FIELD_EM_POYNTING_VECTOR_REAL: &str = "em.poynting_vector_real";
50pub const FEA_FIELD_EM_POYNTING_VECTOR_IMAG: &str = "em.poynting_vector_imag";
51
52pub fn fea_modal_mode_shape_field_id(mode_number: usize) -> String {
53    format!("modal.mode_shape.{mode_number}")
54}
55
56pub const FEA_FIELD_MODAL_FREQUENCY_HZ: &str = "modal.frequency_hz";
57pub const FEA_FIELD_MODAL_EIGENVALUE: &str = "modal.eigenvalue";
58pub const FEA_FIELD_MODAL_MODAL_MASS: &str = "modal.modal_mass";
59pub const FEA_FIELD_MODAL_MODAL_STIFFNESS: &str = "modal.modal_stiffness";
60pub const FEA_FIELD_MODAL_PARTICIPATION_FACTOR: &str = "modal.participation_factor";
61pub const FEA_FIELD_MODAL_RESIDUAL_NORM: &str = "modal.residual_norm";
62pub const FEA_FIELD_MODAL_RELATIVE_FREQUENCY_SEPARATION: &str =
63    "modal.relative_frequency_separation";
64pub const FEA_FIELD_MODAL_M_ORTHOGONALITY: &str = "modal.m_orthogonality";
65
66pub const FEA_FIELD_ACOUSTIC_PRESSURE_REAL: &str = "acoustic.pressure_real";
67pub const FEA_FIELD_ACOUSTIC_PRESSURE_IMAG: &str = "acoustic.pressure_imag";
68pub const FEA_FIELD_ACOUSTIC_PRESSURE_MAGNITUDE: &str = "acoustic.pressure_magnitude";
69pub const FEA_FIELD_ACOUSTIC_PHASE: &str = "acoustic.phase";
70pub const FEA_FIELD_ACOUSTIC_SOUND_PRESSURE_LEVEL_DB: &str = "acoustic.sound_pressure_level_db";
71pub const FEA_FIELD_ACOUSTIC_PARTICLE_VELOCITY: &str = "acoustic.particle_velocity";
72
73pub fn fea_acoustic_frequency_response_field_id(frequency_hz: f64) -> String {
74    let scaled_hz = (frequency_hz.max(0.0) * 1_000.0).round() as u64;
75    format!("acoustic.frequency_response.hz_x1000_{scaled_hz}")
76}
77
78pub const FEA_FIELD_CFD_VELOCITY: &str = "cfd.velocity";
79pub const FEA_FIELD_CFD_PRESSURE: &str = "cfd.pressure";
80pub const FEA_FIELD_CFD_VORTICITY: &str = "cfd.vorticity";
81pub const FEA_FIELD_CFD_WALL_SHEAR_STRESS: &str = "cfd.wall_shear_stress";
82pub const FEA_FIELD_CFD_RESIDUAL_MOMENTUM: &str = "cfd.residual_momentum";
83pub const FEA_FIELD_CFD_RESIDUAL_CONTINUITY: &str = "cfd.residual_continuity";
84pub const FEA_FIELD_CFD_REYNOLDS_NUMBER: &str = "cfd.reynolds_number";
85
86pub const FEA_FIELD_CHT_FLUID_VELOCITY: &str = "cht.fluid_velocity";
87pub const FEA_FIELD_CHT_FLUID_PRESSURE: &str = "cht.fluid_pressure";
88
89pub fn fea_cht_fluid_temperature_field_id(step_index: usize) -> String {
90    format!("cht.fluid_temperature.{step_index}")
91}
92
93pub fn fea_cht_solid_temperature_field_id(step_index: usize) -> String {
94    format!("cht.solid_temperature.{step_index}")
95}
96
97pub fn fea_cht_interface_heat_flux_field_id(step_index: usize) -> String {
98    format!("cht.interface_heat_flux.{step_index}")
99}
100
101pub fn fea_cht_interface_temperature_jump_field_id(step_index: usize) -> String {
102    format!("cht.interface_temperature_jump.{step_index}")
103}
104
105pub fn fea_cht_energy_residual_field_id(step_index: usize) -> String {
106    format!("cht.energy_residual.{step_index}")
107}
108
109pub fn fea_fsi_fluid_velocity_field_id(step_index: usize) -> String {
110    format!("fsi.fluid_velocity.{step_index}")
111}
112
113pub fn fea_fsi_fluid_pressure_field_id(step_index: usize) -> String {
114    format!("fsi.fluid_pressure.{step_index}")
115}
116
117pub fn fea_fsi_structural_displacement_field_id(step_index: usize) -> String {
118    format!("fsi.structural_displacement.{step_index}")
119}
120
121pub fn fea_fsi_interface_pressure_field_id(step_index: usize) -> String {
122    format!("fsi.interface_pressure.{step_index}")
123}
124
125pub fn fea_fsi_interface_traction_field_id(step_index: usize) -> String {
126    format!("fsi.interface_traction.{step_index}")
127}
128
129pub fn fea_fsi_interface_displacement_field_id(step_index: usize) -> String {
130    format!("fsi.interface_displacement.{step_index}")
131}
132
133pub fn fea_fsi_interface_residual_field_id(step_index: usize) -> String {
134    format!("fsi.interface_residual.{step_index}")
135}
136
137pub fn fea_fsi_coupling_iteration_count_field_id(step_index: usize) -> String {
138    format!("fsi.coupling_iteration_count.{step_index}")
139}
140
141pub fn fea_thermal_temperature_field_id(snapshot_index: usize) -> String {
142    format!("thermal.temperature.{snapshot_index}")
143}
144
145pub fn fea_thermal_temperature_gradient_field_id(snapshot_index: usize) -> String {
146    format!("thermal.temperature_gradient.{snapshot_index}")
147}
148
149pub fn fea_thermal_heat_flux_field_id(snapshot_index: usize) -> String {
150    format!("thermal.heat_flux.{snapshot_index}")
151}
152
153pub fn fea_thermal_heat_source_field_id(snapshot_index: usize) -> String {
154    format!("thermal.heat_source.{snapshot_index}")
155}
156
157pub fn fea_thermal_boundary_heat_flux_field_id(snapshot_index: usize) -> String {
158    format!("thermal.boundary_heat_flux.{snapshot_index}")
159}
160
161pub fn fea_transient_displacement_field_id(snapshot_index: usize) -> String {
162    format!("transient.displacement.{snapshot_index}")
163}
164
165pub fn fea_transient_rotation_field_id(snapshot_index: usize) -> String {
166    format!("transient.rotation.{snapshot_index}")
167}
168
169pub fn fea_transient_velocity_field_id(snapshot_index: usize) -> String {
170    format!("transient.velocity.{snapshot_index}")
171}
172
173pub fn fea_transient_angular_velocity_field_id(snapshot_index: usize) -> String {
174    format!("transient.angular_velocity.{snapshot_index}")
175}
176
177pub fn fea_transient_acceleration_field_id(snapshot_index: usize) -> String {
178    format!("transient.acceleration.{snapshot_index}")
179}
180
181pub fn fea_transient_angular_acceleration_field_id(snapshot_index: usize) -> String {
182    format!("transient.angular_acceleration.{snapshot_index}")
183}
184
185pub fn fea_transient_von_mises_field_id(snapshot_index: usize) -> String {
186    format!("transient.von_mises.{snapshot_index}")
187}
188
189pub fn fea_transient_kinetic_energy_field_id(snapshot_index: usize) -> String {
190    format!("transient.kinetic_energy.{snapshot_index}")
191}
192
193pub fn fea_transient_strain_energy_field_id(snapshot_index: usize) -> String {
194    format!("transient.strain_energy.{snapshot_index}")
195}
196
197pub fn fea_transient_residual_norm_field_id(snapshot_index: usize) -> String {
198    format!("transient.residual_norm.{snapshot_index}")
199}
200
201pub fn fea_nonlinear_displacement_field_id(increment_index: usize) -> String {
202    format!("nonlinear.displacement.{increment_index}")
203}
204
205pub fn fea_nonlinear_rotation_field_id(increment_index: usize) -> String {
206    format!("nonlinear.rotation.{increment_index}")
207}
208
209pub fn fea_nonlinear_von_mises_field_id(increment_index: usize) -> String {
210    format!("nonlinear.von_mises.{increment_index}")
211}
212
213pub fn fea_nonlinear_plastic_strain_field_id(increment_index: usize) -> String {
214    format!("nonlinear.plastic_strain.{increment_index}")
215}
216
217pub fn fea_nonlinear_equivalent_plastic_strain_field_id(increment_index: usize) -> String {
218    format!("nonlinear.equivalent_plastic_strain.{increment_index}")
219}
220
221pub fn fea_nonlinear_contact_pressure_field_id(increment_index: usize) -> String {
222    format!("nonlinear.contact_pressure.{increment_index}")
223}
224
225pub fn fea_nonlinear_contact_gap_field_id(increment_index: usize) -> String {
226    format!("nonlinear.contact_gap.{increment_index}")
227}
228
229pub fn fea_nonlinear_load_factor_field_id(increment_index: usize) -> String {
230    format!("nonlinear.load_factor.{increment_index}")
231}
232
233pub fn fea_nonlinear_residual_norm_field_id(increment_index: usize) -> String {
234    format!("nonlinear.residual_norm.{increment_index}")
235}
236
237pub fn fea_thermo_mechanical_temperature_field_id(step_index: usize) -> String {
238    format!("thermo_mechanical.temperature.{step_index}")
239}
240
241pub fn fea_thermo_mechanical_thermal_strain_field_id(step_index: usize) -> String {
242    format!("thermo_mechanical.thermal_strain.{step_index}")
243}
244
245pub fn fea_thermo_mechanical_thermal_stress_field_id(step_index: usize) -> String {
246    format!("thermo_mechanical.thermal_stress.{step_index}")
247}
248
249pub fn fea_thermo_mechanical_displacement_field_id(step_index: usize) -> String {
250    format!("thermo_mechanical.displacement.{step_index}")
251}
252
253pub fn fea_thermo_mechanical_von_mises_field_id(step_index: usize) -> String {
254    format!("thermo_mechanical.von_mises.{step_index}")
255}
256
257pub fn fea_thermo_mechanical_coupling_residual_field_id(step_index: usize) -> String {
258    format!("thermo_mechanical.coupling_residual.{step_index}")
259}
260
261pub const FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_POTENTIAL: &str = "electro_thermal.electric_potential";
262pub const FEA_FIELD_ELECTRO_THERMAL_ELECTRIC_FIELD: &str = "electro_thermal.electric_field";
263pub const FEA_FIELD_ELECTRO_THERMAL_CURRENT_DENSITY: &str = "electro_thermal.current_density";
264pub const FEA_FIELD_ELECTRO_THERMAL_JOULE_HEAT: &str = "electro_thermal.joule_heat";
265
266pub fn fea_electro_thermal_temperature_field_id(step_index: usize) -> String {
267    format!("electro_thermal.temperature.{step_index}")
268}
269
270pub fn fea_electro_thermal_thermal_residual_field_id(step_index: usize) -> String {
271    format!("electro_thermal.thermal_residual.{step_index}")
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum ComputeBackend {
277    Cpu,
278    Gpu,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct FeaRunResult {
283    pub backend: ComputeBackend,
284    pub solver_backend: String,
285    pub solver_device_apply_k_ratio: f64,
286    pub solver_method: String,
287    pub preconditioner: String,
288    pub solver_host_sync_count: u32,
289    pub diagnostics: Vec<crate::diagnostics::FeaDiagnostic>,
290    pub fields: Vec<AnalysisField>,
291}
292
293impl FeaRunResult {
294    pub fn field(&self, field_id: &str) -> Option<&AnalysisField> {
295        self.fields.iter().find(|field| field.field_id == field_id)
296    }
297
298    pub fn fields_are_empty(&self) -> bool {
299        self.fields.is_empty() || self.fields.iter().all(AnalysisField::is_empty)
300    }
301
302    pub fn field_ids(&self) -> Vec<String> {
303        self.fields
304            .iter()
305            .map(|field| field.field_id.clone())
306            .collect()
307    }
308}
309
310fn default_prep_coordinate_span_m() -> f64 {
311    1.0
312}
313
314fn default_prep_coordinate_secondary_span_m() -> f64 {
315    0.0
316}
317
318fn default_prep_coordinate_active_dimension_count() -> usize {
319    1
320}
321
322fn default_prep_coordinate_characteristic_length_m() -> f64 {
323    1.0
324}
325
326fn default_zero_usize() -> usize {
327    0
328}
329
330fn default_zero_f64() -> f64 {
331    0.0
332}
333
334fn default_reference_element_coordinates_m() -> [[f64; 3]; 3] {
335    [[0.0; 3]; 3]
336}
337
338fn default_element_topology_sample_edge_nodes() -> [[u32; 2]; 8] {
339    [[0; 2]; 8]
340}
341
342fn default_element_topology_sample_node_coordinates_m() -> [[f64; 3]; 8] {
343    [[0.0; 3]; 8]
344}
345
346fn default_element_topology_sample_element_edges() -> [[u32; 3]; 4] {
347    [[0; 3]; 4]
348}
349
350fn default_element_topology_sample_element_orientations() -> [[i8; 3]; 4] {
351    [[0; 3]; 4]
352}
353
354fn default_element_topology_sample_element_areas_m2() -> [f64; 4] {
355    [0.0; 4]
356}
357
358fn default_element_topology_node_coordinates_m() -> Vec<[f64; 3]> {
359    Vec::new()
360}
361
362fn default_element_topology_edge_nodes() -> Vec<[u32; 2]> {
363    Vec::new()
364}
365
366fn default_element_topology_element_edges() -> Vec<[u32; 3]> {
367    Vec::new()
368}
369
370fn default_element_topology_element_orientations() -> Vec<[i8; 3]> {
371    Vec::new()
372}
373
374fn default_element_topology_element_areas_m2() -> Vec<f64> {
375    Vec::new()
376}
377
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379pub struct FeaPrepContext {
380    pub prepared_mesh_count: usize,
381    pub prepared_node_count: usize,
382    pub prepared_element_count: usize,
383    pub mapped_region_count: usize,
384    pub min_scaled_jacobian: f64,
385    pub mean_aspect_ratio: f64,
386    pub inverted_element_count: usize,
387    pub mapped_load_count: usize,
388    pub mapped_bc_count: usize,
389    pub layout_seed: u64,
390    pub topology_dof_multiplier: f64,
391    pub topology_bandwidth_estimate: u32,
392    pub mapped_region_participation_ratio: f64,
393    pub topology_surface_patch_ratio: f64,
394    pub topology_volume_core_ratio: f64,
395    pub topology_mixed_family_ratio: f64,
396    pub topology_region_span_mean: f64,
397    pub topology_region_block_count: usize,
398    pub topology_region_mesh_mean: f64,
399    pub topology_region_mesh_variance: f64,
400    pub topology_triangle_family_ratio: f64,
401    pub topology_quad_family_ratio: f64,
402    pub topology_tetrahedron_family_ratio: f64,
403    pub topology_hex_family_ratio: f64,
404    #[serde(default = "default_prep_coordinate_span_m")]
405    pub coordinate_span_x_m: f64,
406    #[serde(default = "default_prep_coordinate_secondary_span_m")]
407    pub coordinate_span_y_m: f64,
408    #[serde(default = "default_prep_coordinate_secondary_span_m")]
409    pub coordinate_span_z_m: f64,
410    #[serde(default = "default_prep_coordinate_active_dimension_count")]
411    pub coordinate_active_dimension_count: usize,
412    #[serde(default = "default_prep_coordinate_characteristic_length_m")]
413    pub coordinate_characteristic_length_m: f64,
414    #[serde(default = "default_zero_usize")]
415    pub element_geometry_node_count: usize,
416    #[serde(default = "default_zero_usize")]
417    pub element_geometry_edge_count: usize,
418    #[serde(default = "default_zero_f64")]
419    pub mean_element_edge_length_m: f64,
420    #[serde(default = "default_zero_f64")]
421    pub mean_element_area_m2: f64,
422    #[serde(default = "default_zero_f64")]
423    pub element_geometry_coverage_ratio: f64,
424    #[serde(default = "default_reference_element_coordinates_m")]
425    pub reference_element_coordinates_m: [[f64; 3]; 3],
426    #[serde(default = "default_zero_f64")]
427    pub reference_element_area_m2: f64,
428    #[serde(default = "default_zero_usize")]
429    pub element_topology_sample_element_count: usize,
430    #[serde(default = "default_zero_usize")]
431    pub element_topology_sample_edge_count: usize,
432    #[serde(default = "default_element_topology_sample_edge_nodes")]
433    pub element_topology_sample_edge_nodes: [[u32; 2]; 8],
434    #[serde(default = "default_element_topology_sample_node_coordinates_m")]
435    pub element_topology_sample_node_coordinates_m: [[f64; 3]; 8],
436    #[serde(default = "default_element_topology_sample_element_edges")]
437    pub element_topology_sample_element_edges: [[u32; 3]; 4],
438    #[serde(default = "default_element_topology_sample_element_orientations")]
439    pub element_topology_sample_element_orientations: [[i8; 3]; 4],
440    #[serde(default = "default_element_topology_sample_element_areas_m2")]
441    pub element_topology_sample_element_areas_m2: [f64; 4],
442    #[serde(default = "default_element_topology_node_coordinates_m")]
443    pub element_topology_node_coordinates_m: Vec<[f64; 3]>,
444    #[serde(default = "default_element_topology_edge_nodes")]
445    pub element_topology_edge_nodes: Vec<[u32; 2]>,
446    #[serde(default = "default_element_topology_element_edges")]
447    pub element_topology_element_edges: Vec<[u32; 3]>,
448    #[serde(default = "default_element_topology_element_orientations")]
449    pub element_topology_element_orientations: Vec<[i8; 3]>,
450    #[serde(default = "default_element_topology_element_areas_m2")]
451    pub element_topology_element_areas_m2: Vec<f64>,
452    pub calibration_profile_override: Option<FeaPrepCalibrationProfile>,
453}
454
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456pub struct FeaThermoRegionTemperatureDelta {
457    pub region_id: String,
458    pub temperature_delta_k: f64,
459}
460
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462pub struct FeaThermoTimeProfilePoint {
463    pub normalized_time: f64,
464    pub scale: f64,
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(rename_all = "snake_case")]
469pub enum FeaThermoFieldInterpolationMode {
470    Linear,
471    Step,
472}
473
474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
475pub struct FeaThermoFieldSource {
476    pub source_id: String,
477    pub revision: u32,
478    pub interpolation_mode: Option<FeaThermoFieldInterpolationMode>,
479    pub expected_region_ids: Vec<String>,
480}
481
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
483pub struct FeaThermoMechanicalContext {
484    pub enabled: bool,
485    pub reference_temperature_k: f64,
486    pub applied_temperature_delta_k: f64,
487    pub thermal_expansion_coefficient: f64,
488    pub field_source: Option<FeaThermoFieldSource>,
489    pub region_temperature_deltas: Vec<FeaThermoRegionTemperatureDelta>,
490    pub time_profile: Vec<FeaThermoTimeProfilePoint>,
491}
492
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494pub struct FeaElectroRegionConductivityScale {
495    pub region_id: String,
496    pub conductivity_scale: f64,
497}
498
499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
500pub struct FeaElectroTimeProfilePoint {
501    pub normalized_time: f64,
502    pub current_scale: f64,
503}
504
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506pub struct FeaElectroThermalContext {
507    pub enabled: bool,
508    pub reference_temperature_k: f64,
509    pub applied_voltage_v: f64,
510    pub base_electrical_conductivity_s_per_m: f64,
511    pub resistive_heating_coefficient: f64,
512    pub region_conductivity_scales: Vec<FeaElectroRegionConductivityScale>,
513    pub time_profile: Vec<FeaElectroTimeProfilePoint>,
514}
515
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517pub struct FeaPlasticityConstitutiveContext {
518    pub enabled: bool,
519    pub yield_strain: f64,
520    pub hardening_modulus_ratio: f64,
521    pub saturation_exponent: f64,
522}
523
524#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
525pub struct FeaContactInterfaceContext {
526    pub enabled: bool,
527    pub penalty_stiffness_scale: f64,
528    pub max_penetration_ratio: f64,
529    pub friction_coefficient: f64,
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(rename_all = "snake_case")]
534pub enum FeaPrepCalibrationProfile {
535    Fast,
536    Balanced,
537    Conservative,
538}
539
540#[derive(Debug, Clone, PartialEq)]
541pub struct LinearStaticSolveOptions {
542    pub preconditioner_kind: SpdPreconditionerKind,
543    pub algebra_backend_kind: LinearAlgebraBackendKind,
544    pub prep_context: Option<FeaPrepContext>,
545    pub analysis_mesh_artifact_path: Option<String>,
546    pub analysis_mesh: Option<AnalysisMeshArtifact>,
547    pub require_analysis_mesh_for_solid: bool,
548    pub thermo_mechanical_context: Option<FeaThermoMechanicalContext>,
549    pub electro_thermal_context: Option<FeaElectroThermalContext>,
550}
551
552impl Default for LinearStaticSolveOptions {
553    fn default() -> Self {
554        Self {
555            preconditioner_kind: SpdPreconditionerKind::Jacobi,
556            algebra_backend_kind: LinearAlgebraBackendKind::CpuReference,
557            prep_context: None,
558            analysis_mesh_artifact_path: None,
559            analysis_mesh: None,
560            require_analysis_mesh_for_solid: true,
561            thermo_mechanical_context: None,
562            electro_thermal_context: None,
563        }
564    }
565}
566
567#[derive(Debug, Clone, PartialEq)]
568pub struct ModalSolveOptions {
569    pub mode_count: usize,
570    pub prep_context: Option<FeaPrepContext>,
571    pub thermo_mechanical_context: Option<FeaThermoMechanicalContext>,
572    pub electro_thermal_context: Option<FeaElectroThermalContext>,
573}
574
575impl Default for ModalSolveOptions {
576    fn default() -> Self {
577        Self {
578            mode_count: 3,
579            prep_context: None,
580            thermo_mechanical_context: None,
581            electro_thermal_context: None,
582        }
583    }
584}
585
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct FeaModalRunResult {
588    pub run: FeaRunResult,
589    pub eigenvalues_hz: Vec<f64>,
590    pub mode_shapes: Vec<AnalysisField>,
591    pub residual_norms: Vec<f64>,
592}
593
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
595pub struct FeaTransientRunResult {
596    pub run: FeaRunResult,
597    pub time_points_s: Vec<f64>,
598    pub displacement_snapshots: Vec<AnalysisField>,
599    pub rotation_snapshots: Vec<AnalysisField>,
600    pub velocity_snapshots: Vec<AnalysisField>,
601    pub angular_velocity_snapshots: Vec<AnalysisField>,
602    pub acceleration_snapshots: Vec<AnalysisField>,
603    pub angular_acceleration_snapshots: Vec<AnalysisField>,
604    pub von_mises_snapshots: Vec<AnalysisField>,
605    pub kinetic_energy_snapshots: Vec<AnalysisField>,
606    pub strain_energy_snapshots: Vec<AnalysisField>,
607    pub residual_norm_snapshots: Vec<AnalysisField>,
608    pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
609    pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
610    pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
611    pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
612    pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
613    pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
614    pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
615    pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
616    pub residual_norms: Vec<f64>,
617}
618
619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
620pub struct FeaThermalRunResult {
621    pub run: FeaRunResult,
622    pub time_points_s: Vec<f64>,
623    pub temperature_snapshots: Vec<AnalysisField>,
624    pub temperature_gradient_snapshots: Vec<AnalysisField>,
625    pub heat_flux_snapshots: Vec<AnalysisField>,
626    pub heat_source_snapshots: Vec<AnalysisField>,
627    pub boundary_heat_flux_snapshots: Vec<AnalysisField>,
628    pub residual_norms: Vec<f64>,
629    pub reference_temperature_k: f64,
630}
631
632#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
633pub struct FeaElectromagneticRunResult {
634    pub run: FeaRunResult,
635    pub reference_frequency_hz: f64,
636    pub applied_current_a: f64,
637    pub vector_potential_real_field: AnalysisField,
638    pub vector_potential_imag_field: AnalysisField,
639    pub magnetic_flux_density_real_field: AnalysisField,
640    pub magnetic_flux_density_imag_field: AnalysisField,
641    pub magnetic_flux_density_magnitude_field: AnalysisField,
642    pub magnetic_field_real_field: AnalysisField,
643    pub magnetic_field_imag_field: AnalysisField,
644    pub current_density_real_field: AnalysisField,
645    pub current_density_imag_field: AnalysisField,
646    pub electric_field_real_field: AnalysisField,
647    pub electric_field_imag_field: AnalysisField,
648    pub power_loss_density_field: AnalysisField,
649    pub energy_density_field: AnalysisField,
650    pub residual_real_field: AnalysisField,
651    pub residual_imag_field: AnalysisField,
652    pub electric_flux_density_real_field: AnalysisField,
653    pub electric_flux_density_imag_field: AnalysisField,
654    pub poynting_vector_real_field: AnalysisField,
655    pub poynting_vector_imag_field: AnalysisField,
656    pub max_residual_norm: f64,
657    pub solve_quality: f64,
658}
659
660#[derive(Debug, Clone, PartialEq)]
661pub struct ElectromagneticSolveOptions {
662    pub prep_context: Option<FeaPrepContext>,
663    pub residual_target: f64,
664    pub harmonic_tolerance: f64,
665    pub harmonic_max_iterations: usize,
666}
667
668impl Default for ElectromagneticSolveOptions {
669    fn default() -> Self {
670        Self {
671            prep_context: None,
672            residual_target: 1.0e-6,
673            harmonic_tolerance: 1.0e-7,
674            harmonic_max_iterations: 96,
675        }
676    }
677}
678
679#[derive(Debug, Clone, PartialEq)]
680pub struct ThermalSolveOptions {
681    pub step_count: usize,
682    pub time_step_s: f64,
683    pub residual_target: f64,
684    pub prep_context: Option<FeaPrepContext>,
685    pub thermo_mechanical_context: Option<FeaThermoMechanicalContext>,
686}
687
688impl Default for ThermalSolveOptions {
689    fn default() -> Self {
690        Self {
691            step_count: 10,
692            time_step_s: 1.0e-2,
693            residual_target: 1.0e-4,
694            prep_context: None,
695            thermo_mechanical_context: None,
696        }
697    }
698}
699
700#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
701pub struct FeaNonlinearRunResult {
702    pub run: FeaRunResult,
703    pub load_factors: Vec<f64>,
704    pub displacement_snapshots: Vec<AnalysisField>,
705    pub rotation_snapshots: Vec<AnalysisField>,
706    pub von_mises_snapshots: Vec<AnalysisField>,
707    pub plastic_strain_snapshots: Vec<AnalysisField>,
708    pub equivalent_plastic_strain_snapshots: Vec<AnalysisField>,
709    pub contact_pressure_snapshots: Vec<AnalysisField>,
710    pub contact_gap_snapshots: Vec<AnalysisField>,
711    pub load_factor_snapshots: Vec<AnalysisField>,
712    pub residual_norm_snapshots: Vec<AnalysisField>,
713    pub thermo_mechanical_temperature_snapshots: Vec<AnalysisField>,
714    pub thermo_mechanical_thermal_strain_snapshots: Vec<AnalysisField>,
715    pub thermo_mechanical_thermal_stress_snapshots: Vec<AnalysisField>,
716    pub thermo_mechanical_displacement_snapshots: Vec<AnalysisField>,
717    pub thermo_mechanical_von_mises_snapshots: Vec<AnalysisField>,
718    pub thermo_mechanical_coupling_residual_snapshots: Vec<AnalysisField>,
719    pub electro_thermal_temperature_snapshots: Vec<AnalysisField>,
720    pub electro_thermal_thermal_residual_snapshots: Vec<AnalysisField>,
721    pub residual_norms: Vec<f64>,
722    pub increment_norms: Vec<f64>,
723    pub iteration_counts: Vec<usize>,
724    pub failed_increments: usize,
725    pub line_search_backtracks: usize,
726    pub max_line_search_backtracks_per_increment: usize,
727    pub tangent_rebuild_count: usize,
728    pub iteration_spike_count: usize,
729    pub convergence_stall_count: usize,
730    pub backtrack_burst_count: usize,
731}
732
733#[derive(Debug, Error)]
734pub enum FeaRunError {
735    #[error("FEA_MODEL_INVALID: {0}")]
736    InvalidModel(String),
737    #[error("FEA_ASSEMBLY_FAILED: {0}")]
738    Assembly(String),
739    #[error("FEA_CANCELLED: execution cancelled by user")]
740    Cancelled,
741}