1use carapace_core::analysis::{
17 modal_analysis, Algorithm, Analysis, AnalysisBuilder, AnalysisError, ArcFailure, ArcStepInfo,
18 ConstraintHandler, ConvergenceTest, GroundMotion, Integrator, Mode, RayleighDamping,
19 StopReason, TransientAnalysis,
20};
21use carapace_core::model::{
22 Domain, Element, Element3, ElementOps, LoadPatternId, Node3Id, NodeId, NDF, PLANAR_NDIM,
23 SPATIAL_NDF, SPATIAL_NDIM,
24};
25use serde::Serialize;
26use slotmap::Key;
27
28use super::error::ModelErrorDetail;
29use super::sequence::{FiberResponseKind, GaussQuantity, ModalDampingAnchors};
30
31fn fiber_value<const NDIM: usize, const NDOF: usize, NId, E>(
39 domain: &Domain<NDIM, NDOF, NId, E>,
40 element: E::Id,
41 point: u32,
42 fiber: u32,
43 response: FiberResponseKind,
44) -> Option<f64>
45where
46 NId: Key,
47 E: ElementOps<NDIM, NDOF, NId>,
48{
49 let responses = domain.element_fiber_responses(element)?;
50 let &(strain, stress) = responses.get(point as usize)?.get(fiber as usize)?;
51 Some(match response {
52 FiberResponseKind::Strain => strain,
53 FiberResponseKind::Stress => stress,
54 })
55}
56
57fn gauss_value<const NDIM: usize, const NDOF: usize, NId, E>(
58 domain: &Domain<NDIM, NDOF, NId, E>,
59 element: E::Id,
60 point: u32,
61 quantity: GaussQuantity,
62 component: u8,
63) -> Option<f64>
64where
65 NId: Key,
66 E: ElementOps<NDIM, NDOF, NId>,
67{
68 if let Some(responses) = domain.element_shell_responses(element) {
69 let response = responses.get(point as usize)?;
70 let values = match quantity {
71 GaussQuantity::Strain => response.strain,
72 GaussQuantity::Stress => response.resultant,
73 };
74 return values.get(component as usize).copied();
75 }
76 let response = *domain
77 .element_gauss_responses(element)?
78 .get(point as usize)?;
79 let values = match quantity {
80 GaussQuantity::Strain => response.strain,
81 GaussQuantity::Stress => response.stress,
82 };
83 values.get(component as usize).copied()
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, tsify::Tsify)]
90#[serde(
91 tag = "kind",
92 rename_all = "camelCase",
93 rename_all_fields = "camelCase"
94)]
95pub enum AnalysisErrorDetail {
96 FailedToConverge {
97 step: usize,
98 },
99 SingularSystem,
100 InvalidConstraint,
101 InvalidModeCount {
102 requested: usize,
103 free_dofs: usize,
104 },
105 InvalidOption {
110 field: &'static str,
111 },
112 UnsupportedLoadSeries,
114 ZeroLoadSensitivity,
116 InitialStateNotInEquilibrium {
119 measure: f64,
120 },
121 MissingSeedDirection,
123 CutbacksExhausted {
125 step: usize,
126 attempts: usize,
127 radius: f64,
128 last_failure: ArcFailureDetail,
129 },
130 ContinuationComplete,
132 InvalidModel {
136 error: ModelErrorDetail,
137 },
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, tsify::Tsify)]
142#[serde(rename_all = "camelCase")]
143pub enum ArcFailureDetail {
144 NotConverged,
145 Stagnated,
146 NoDescent,
147 NonFinite,
148 SingularSystem,
149 InaccurateSolve,
150 BranchOrientation,
151 DegenerateConstraint,
152}
153
154impl From<ArcFailure> for ArcFailureDetail {
155 fn from(failure: ArcFailure) -> Self {
156 match failure {
157 ArcFailure::NotConverged => Self::NotConverged,
158 ArcFailure::Stagnated => Self::Stagnated,
159 ArcFailure::NoDescent => Self::NoDescent,
160 ArcFailure::NonFinite => Self::NonFinite,
161 ArcFailure::SingularSystem => Self::SingularSystem,
162 ArcFailure::InaccurateSolve => Self::InaccurateSolve,
163 ArcFailure::BranchOrientation => Self::BranchOrientation,
164 ArcFailure::DegenerateConstraint => Self::DegenerateConstraint,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Serialize, tsify::Tsify)]
172#[serde(rename_all = "camelCase")]
173pub struct ContinuationDetail {
174 pub radius: f64,
175 pub next_radius: f64,
176 pub retries: usize,
177 pub corrector_solves: usize,
178 pub factorizations: usize,
179 pub force_measure: f64,
180 pub arc_error: f64,
181 pub chord_length: f64,
182 pub det_sign: Option<i8>,
183 pub bifurcation_suspected: bool,
184 pub displacement_scale: f64,
185 pub rotation_scale: Option<f64>,
186 pub stop: Option<ContinuationStopDetail>,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Serialize, tsify::Tsify)]
190#[serde(rename_all = "camelCase")]
191pub struct ContinuationStopDetail {
192 pub reason: StopReasonDetail,
193 pub landed_exactly: bool,
194 pub overshoot: f64,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, tsify::Tsify)]
198#[serde(rename_all = "camelCase")]
199pub enum StopReasonDetail {
200 DisplacementTarget,
201 LoadFactorTarget,
202 LoadFactorZeroCrossing,
203 ChordLength,
204 StepCount,
205}
206
207impl From<ArcStepInfo> for ContinuationDetail {
208 fn from(info: ArcStepInfo) -> Self {
209 Self {
210 radius: info.radius,
211 next_radius: info.next_radius,
212 retries: info.retries,
213 corrector_solves: info.corrector_solves,
214 factorizations: info.factorizations,
215 force_measure: info.force_measure,
216 arc_error: info.arc_error,
217 chord_length: info.chord_length,
218 det_sign: info.det_sign,
219 bifurcation_suspected: info.bifurcation_suspected,
220 displacement_scale: info.displacement_scale,
221 rotation_scale: info.rotation_scale,
222 stop: info.stop.map(|stop| ContinuationStopDetail {
223 reason: match stop.reason {
224 StopReason::DisplacementTarget => StopReasonDetail::DisplacementTarget,
225 StopReason::LoadFactorTarget => StopReasonDetail::LoadFactorTarget,
226 StopReason::LoadFactorZeroCrossing => StopReasonDetail::LoadFactorZeroCrossing,
227 StopReason::ChordLength => StopReasonDetail::ChordLength,
228 StopReason::StepCount => StopReasonDetail::StepCount,
229 },
230 landed_exactly: stop.landed_exactly,
231 overshoot: stop.overshoot,
232 }),
233 }
234 }
235}
236
237impl From<AnalysisError> for AnalysisErrorDetail {
238 fn from(error: AnalysisError) -> Self {
239 match error {
240 AnalysisError::FailedToConverge { step } => {
241 AnalysisErrorDetail::FailedToConverge { step }
242 }
243 AnalysisError::SingularSystem => AnalysisErrorDetail::SingularSystem,
244 AnalysisError::InvalidConstraint => AnalysisErrorDetail::InvalidConstraint,
245 AnalysisError::InvalidModeCount {
246 requested,
247 free_dofs,
248 } => AnalysisErrorDetail::InvalidModeCount {
249 requested,
250 free_dofs,
251 },
252 AnalysisError::InvalidOption { field } => AnalysisErrorDetail::InvalidOption { field },
253 AnalysisError::UnsupportedLoadSeries => AnalysisErrorDetail::UnsupportedLoadSeries,
254 AnalysisError::ZeroLoadSensitivity => AnalysisErrorDetail::ZeroLoadSensitivity,
255 AnalysisError::InitialStateNotInEquilibrium { measure } => {
256 AnalysisErrorDetail::InitialStateNotInEquilibrium { measure }
257 }
258 AnalysisError::MissingSeedDirection => AnalysisErrorDetail::MissingSeedDirection,
259 AnalysisError::CutbacksExhausted {
260 step,
261 attempts,
262 radius,
263 last_failure,
264 } => AnalysisErrorDetail::CutbacksExhausted {
265 step,
266 attempts,
267 radius,
268 last_failure: last_failure.into(),
269 },
270 AnalysisError::ContinuationComplete => AnalysisErrorDetail::ContinuationComplete,
271 AnalysisError::InvalidModel(error) => AnalysisErrorDetail::InvalidModel {
272 error: error.into(),
273 },
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq)]
284pub enum ResolvedRecorder<NId, EId> {
285 NodeDisp {
286 node: NId,
287 dof: u8,
288 },
289 NodeVel {
292 node: NId,
293 dof: u8,
294 },
295 NodeAccel {
296 node: NId,
297 dof: u8,
298 },
299 ElementForce {
300 element: EId,
301 component: u8,
302 },
303 ElementLoad {
305 element: EId,
306 component: u8,
307 },
308 ModeShape {
311 mode: u32,
312 node: NId,
313 dof: u8,
314 },
315 Reaction {
318 node: NId,
319 dof: u8,
320 },
321 Fiber {
324 element: EId,
325 point: u32,
326 fiber: u32,
327 response: super::sequence::FiberResponseKind,
328 },
329 GaussPoint {
332 element: EId,
333 point: u32,
334 quantity: super::sequence::GaussQuantity,
335 component: u8,
336 },
337}
338
339pub(super) struct CompiledStage<NId, EId, Load> {
340 pub id: String,
341 pub steps: u32,
342 pub kind: CompiledStageKind<NId>,
343 pub pending_nodal_loads: Vec<(LoadPatternId, NId, usize, f64)>,
346 pub pending_element_loads: Vec<(LoadPatternId, EId, Load)>,
347}
348
349pub(super) enum CompiledStageKind<NId> {
350 Static {
351 integrator: Integrator<NId>,
352 algorithm: Algorithm,
353 convergence: ConvergenceTest,
354 hold_patterns_after: Vec<LoadPatternId>,
355 },
356 Modal { num_modes: usize },
359 Reset,
361 Transient {
362 algorithm: Algorithm,
363 convergence: ConvergenceTest,
364 damping: RayleighDamping,
365 modal_anchors: Option<ModalDampingAnchors>,
366 dt: f64,
367 ground_motions: Vec<GroundMotion>,
368 },
369}
370
371enum StageRunner<const NDIM: usize, const NDOF: usize, NId, E>
372where
373 NId: Key + Copy,
374 E: ElementOps<NDIM, NDOF, NId> + Clone,
375 E::Load: Clone,
376{
377 Static {
378 analysis: Analysis<NDIM, NDOF, NId, E>,
379 steps_remaining: u32,
380 },
381 Modal {
388 domain: Domain<NDIM, NDOF, NId, E>,
389 modes: Vec<Mode>,
390 steps_remaining: u32,
391 },
392 Transient {
393 analysis: TransientAnalysis<NDIM, NDOF, NId, E>,
394 steps_remaining: u32,
395 },
396 Reset {
398 domain: Domain<NDIM, NDOF, NId, E>,
399 steps_remaining: u32,
400 },
401}
402
403#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
415#[serde(rename_all = "camelCase")]
416pub struct StepOutcome {
417 pub done: bool,
418 pub stage_complete: bool,
419 pub steps_taken: u32,
420 pub load_factor: f64,
421 pub error: Option<AnalysisErrorDetail>,
422 pub continuation: Option<ContinuationDetail>,
428 pub recorder_batches: Vec<RecorderBatch>,
436}
437
438#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
444#[serde(rename_all = "camelCase")]
445pub struct ModeResult {
446 pub frequency: f64,
448 pub shape: Vec<f64>,
449 pub participation: Vec<f64>,
450 pub mass_ratio: Vec<f64>,
451}
452
453#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
455#[serde(rename_all = "camelCase")]
456pub struct ModalStageResult {
457 pub stage_index: usize,
458 pub stage_id: String,
459 pub ndf: usize,
461 pub modes: Vec<ModeResult>,
462 pub total_mass: Vec<f64>,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
468#[serde(rename_all = "camelCase")]
469pub struct ModalResultsReport {
470 pub stages: Vec<ModalStageResult>,
471}
472
473#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
480#[serde(rename_all = "camelCase")]
481pub struct RecorderBatch {
482 pub recorder_index: usize,
483 pub stage_index: usize,
484 pub first_sample: u32,
485 pub samples: Vec<(f64, f64)>,
486}
487
488#[derive(Debug)]
491pub enum Session {
492 D2(Session2),
493 D3(Session3),
494}
495
496impl Session {
497 pub fn advance(&mut self, step_budget: u32) -> StepOutcome {
498 match self {
499 Session::D2(session) => session.advance(step_budget),
500 Session::D3(session) => session.advance(step_budget),
501 }
502 }
503
504 pub fn modal_results(&self) -> ModalResultsReport {
506 match self {
507 Session::D2(session) => session.modal_results(),
508 Session::D3(session) => session.modal_results(),
509 }
510 }
511
512 pub fn current_stage_id(&self) -> Option<&str> {
517 match self {
518 Session::D2(session) => session.current_stage_id(),
519 Session::D3(session) => session.current_stage_id(),
520 }
521 }
522}
523
524pub type Session2 = ModelSession<PLANAR_NDIM, NDF, NodeId, Element>;
525pub type Session3 = ModelSession<SPATIAL_NDIM, SPATIAL_NDF, Node3Id, Element3>;
526
527pub struct ModelSession<const NDIM: usize, const NDOF: usize, NId, E>
528where
529 NId: Key + Copy,
530 E: ElementOps<NDIM, NDOF, NId> + Clone,
531 E::Load: Clone,
532{
533 domain: Option<Domain<NDIM, NDOF, NId, E>>,
534 node_ids: Vec<NId>,
536 modal_results: Vec<ModalStageResult>,
537 pristine: Option<Domain<NDIM, NDOF, NId, E>>,
539 holds: Vec<(LoadPatternId, f64)>,
541 record_initial: bool,
542 stages: Vec<CompiledStage<NId, E::Id, E::Load>>,
543 current_stage: usize,
544 runner: Option<StageRunner<NDIM, NDOF, NId, E>>,
545 load_factor: f64,
546 error: Option<AnalysisErrorDetail>,
547 recorders: Vec<ResolvedRecorder<NId, E::Id>>,
548 continuation: Option<ContinuationDetail>,
550 current_batch: Vec<Vec<(f64, f64)>>,
555 recorder_sample_counts: Vec<u32>,
558}
559
560impl<const NDIM: usize, const NDOF: usize, NId, E> std::fmt::Debug
561 for ModelSession<NDIM, NDOF, NId, E>
562where
563 NId: Key + Copy,
564 E: ElementOps<NDIM, NDOF, NId> + Clone,
565 E::Load: Clone,
566{
567 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 f.debug_struct("ModelSession")
572 .field("current_stage", &self.current_stage)
573 .field("load_factor", &self.load_factor)
574 .field("error", &self.error)
575 .finish()
576 }
577}
578
579impl<const NDIM: usize, const NDOF: usize, NId, E> ModelSession<NDIM, NDOF, NId, E>
580where
581 NId: Key + Copy,
582 E: ElementOps<NDIM, NDOF, NId> + Clone,
583 E::Load: Clone,
584{
585 pub(super) fn new(
586 domain: Domain<NDIM, NDOF, NId, E>,
587 node_ids: Vec<NId>,
588 stages: Vec<CompiledStage<NId, E::Id, E::Load>>,
589 recorders: Vec<ResolvedRecorder<NId, E::Id>>,
590 record_initial: bool,
591 ) -> Self {
592 let pristine = stages
593 .iter()
594 .any(|stage| matches!(stage.kind, CompiledStageKind::Reset))
595 .then(|| domain.clone());
596 let current_batch = vec![Vec::new(); recorders.len()];
597 let recorder_sample_counts = vec![0; recorders.len()];
598 Self {
599 domain: Some(domain),
600 node_ids,
601 modal_results: Vec::new(),
602 pristine,
603 holds: Vec::new(),
604 record_initial,
605 stages,
606 current_stage: 0,
607 runner: None,
608 load_factor: 0.0,
609 error: None,
610 recorders,
611 continuation: None,
612 current_batch,
613 recorder_sample_counts,
614 }
615 }
616
617 pub fn modal_results(&self) -> ModalResultsReport {
618 ModalResultsReport {
619 stages: self.modal_results.clone(),
620 }
621 }
622
623 pub fn current_stage_id(&self) -> Option<&str> {
624 self.stages
625 .get(self.current_stage)
626 .map(|stage| stage.id.as_str())
627 }
628
629 pub fn advance(&mut self, step_budget: u32) -> StepOutcome {
635 for batch in &mut self.current_batch {
636 batch.clear();
637 }
638 self.continuation = None;
639
640 if self.error.is_none() && self.runner.is_none() && self.current_stage < self.stages.len() {
641 self.start_current_stage();
642 }
643 if self.error.is_some() || self.current_stage >= self.stages.len() {
644 return StepOutcome {
645 done: true,
646 stage_complete: false,
647 steps_taken: 0,
648 load_factor: self.load_factor,
649 error: self.error,
650 continuation: None,
651 recorder_batches: Vec::new(),
652 };
653 }
654
655 let stage_index_for_batch = self.current_stage;
659
660 let mut steps_taken = 0;
661 let mut stage_complete = false;
662 while steps_taken < step_budget {
663 if self.steps_remaining() == 0 {
664 stage_complete = true;
665 break;
666 }
667 match self.step_once() {
668 Ok(()) => {
669 steps_taken += 1;
670 if self.steps_remaining() == 0 {
671 stage_complete = true;
672 break;
673 }
674 }
675 Err(error) => {
676 self.error = Some(error.into());
677 break;
678 }
679 }
680 }
681
682 if stage_complete {
683 self.finish_current_stage();
684 }
685
686 let recorder_batches = self
687 .current_batch
688 .iter()
689 .zip(self.recorder_sample_counts.iter_mut())
690 .enumerate()
691 .filter_map(|(recorder_index, (samples, sample_count))| {
692 if samples.is_empty() {
693 return None;
694 }
695 let first_sample = *sample_count;
696 *sample_count += samples.len() as u32;
697 Some(RecorderBatch {
698 recorder_index,
699 stage_index: stage_index_for_batch,
700 first_sample,
701 samples: samples.clone(),
702 })
703 })
704 .collect();
705
706 StepOutcome {
707 done: self.error.is_some() || self.current_stage >= self.stages.len(),
708 stage_complete,
709 steps_taken,
710 load_factor: self.load_factor,
711 error: self.error,
712 continuation: self.continuation,
713 recorder_batches,
714 }
715 }
716
717 fn start_current_stage(&mut self) {
718 let stage = &self.stages[self.current_stage];
719 let mut domain = self.domain.take().expect("domain held between stages");
720 if matches!(stage.kind, CompiledStageKind::Reset) {
721 domain = self.pristine.clone().expect("kept for Reset stages");
727 for earlier in &self.stages[..self.current_stage] {
728 for &(pattern, node, dof, value) in &earlier.pending_nodal_loads {
729 domain.add_nodal_load(pattern, node, dof, value);
730 }
731 for (pattern, element, load) in &earlier.pending_element_loads {
732 domain.add_element_load(*pattern, *element, load.clone());
733 }
734 }
735 for &(pattern, factor) in &self.holds {
736 domain.hold_pattern_constant(pattern, factor);
737 }
738 self.runner = Some(StageRunner::Reset {
739 domain,
740 steps_remaining: 0,
741 });
742 self.load_factor = 0.0;
743 return;
744 }
745 for &(pattern, node, dof, value) in &stage.pending_nodal_loads {
746 domain.add_nodal_load(pattern, node, dof, value);
747 }
748 for (pattern, element, load) in &stage.pending_element_loads {
749 domain.add_element_load(*pattern, *element, load.clone());
750 }
751 let constraint_handler = if domain.has_mp_constraints() {
758 ConstraintHandler::Transformation
759 } else {
760 ConstraintHandler::Plain
761 };
762 match &stage.kind {
763 CompiledStageKind::Static {
764 integrator,
765 algorithm,
766 convergence,
767 ..
768 } => {
769 match AnalysisBuilder::new()
770 .constraint_handler(constraint_handler)
771 .integrator(integrator.clone())
772 .algorithm(*algorithm)
773 .test(*convergence)
774 .try_build(domain)
775 {
776 Ok(analysis) => {
777 self.runner = Some(StageRunner::Static {
778 analysis,
779 steps_remaining: stage.steps,
780 });
781 }
782 Err(error) => self.error = Some(AnalysisError::InvalidModel(error).into()),
783 }
784 }
785 CompiledStageKind::Modal { num_modes } => match modal_analysis(&mut domain, *num_modes)
790 {
791 Ok(modes) => {
792 self.runner = Some(StageRunner::Modal {
793 domain,
794 modes,
795 steps_remaining: stage.steps,
796 });
797 }
798 Err(error) => self.error = Some(error.into()),
799 },
800 CompiledStageKind::Reset => unreachable!("handled above"),
801 CompiledStageKind::Transient {
802 algorithm,
803 convergence,
804 damping,
805 modal_anchors,
806 dt,
807 ground_motions,
808 } => {
809 let resolved = (|| -> Result<RayleighDamping, AnalysisError> {
810 let Some(anchors) = modal_anchors else {
811 return Ok(*damping);
812 };
813 let count = anchors.mode1.max(anchors.mode2) as usize;
814 let modes = modal_analysis(&mut domain, count)?;
815 let w1 = modes.get(anchors.mode1 as usize - 1).map(|m| m.frequency);
816 let w2 = modes.get(anchors.mode2 as usize - 1).map(|m| m.frequency);
817 let (Some(w1), Some(w2)) = (w1, w2) else {
818 return Err(AnalysisError::InvalidModeCount {
819 requested: count,
820 free_dofs: modes.len(),
821 });
822 };
823 if !w1.is_finite() || !w2.is_finite() || w1 <= 0.0 || w2 <= 0.0 {
824 return Err(AnalysisError::InvalidOption {
825 field: "damping.modalAnchors",
826 });
827 }
828 Ok(RayleighDamping::new(
829 2.0 * anchors.ratio * w1 * w2 / (w1 + w2),
830 2.0 * anchors.ratio / (w1 + w2),
831 ))
832 })();
833 match resolved.and_then(|damping| TransientAnalysis::new(domain, damping, *dt)) {
834 Ok(mut analysis) => {
835 analysis = analysis.with_algorithm(*algorithm, *convergence);
836 for motion in ground_motions.iter().cloned() {
837 analysis = analysis.with_ground_motion(motion);
838 }
839 self.runner = Some(StageRunner::Transient {
840 analysis,
841 steps_remaining: stage.steps,
842 });
843 }
844 Err(error) => self.error = Some(error.into()),
845 }
846 }
847 }
848 self.load_factor = 0.0;
849 if self.record_initial
850 && matches!(
851 self.stages[self.current_stage].kind,
852 CompiledStageKind::Static { .. } | CompiledStageKind::Transient { .. }
853 )
854 {
855 self.record_sample();
856 }
857 }
858
859 fn steps_remaining(&self) -> u32 {
860 match self.runner.as_ref().expect("stage started above") {
861 StageRunner::Static {
862 steps_remaining, ..
863 }
864 | StageRunner::Modal {
865 steps_remaining, ..
866 }
867 | StageRunner::Reset {
868 steps_remaining, ..
869 }
870 | StageRunner::Transient {
871 steps_remaining, ..
872 } => *steps_remaining,
873 }
874 }
875
876 fn step_once(&mut self) -> Result<(), AnalysisError> {
882 match self.runner.as_mut().expect("stage started above") {
883 StageRunner::Static {
884 analysis,
885 steps_remaining,
886 } => {
887 let result = analysis.step()?;
888 self.load_factor = result.load_factor;
889 *steps_remaining -= 1;
890 if let Some(info) = result.arc {
891 self.continuation = Some(info.into());
892 if info.stop.is_some() {
894 *steps_remaining = 0;
895 }
896 }
897 }
898 StageRunner::Transient {
899 analysis,
900 steps_remaining,
901 } => {
902 let result = analysis.step()?;
903 self.load_factor = result.time;
904 *steps_remaining -= 1;
905 }
906 StageRunner::Modal {
907 steps_remaining, ..
908 } => {
909 *steps_remaining -= 1;
910 }
911 StageRunner::Reset { .. } => {}
912 }
913 self.record_sample();
914 Ok(())
915 }
916
917 fn finish_current_stage(&mut self) {
925 let hold_patterns_after = match &self.stages[self.current_stage].kind {
926 CompiledStageKind::Static {
927 hold_patterns_after,
928 ..
929 } => hold_patterns_after.clone(),
930 CompiledStageKind::Modal { .. }
931 | CompiledStageKind::Transient { .. }
932 | CompiledStageKind::Reset => Vec::new(),
933 };
934 let load_factor = self.load_factor;
935
936 let mut domain = match self.runner.take().expect("stage running") {
937 StageRunner::Static { analysis, .. } => analysis.into_domain(),
938 StageRunner::Modal {
939 mut domain, modes, ..
940 } => {
941 let result = self.modal_stage_result(&mut domain, &modes);
942 self.modal_results.push(result);
943 domain
944 }
945 StageRunner::Transient { analysis, .. } => analysis.into_domain(),
946 StageRunner::Reset { domain, .. } => domain,
947 };
948 for pattern in hold_patterns_after {
949 domain.hold_pattern_constant(pattern, load_factor);
950 self.holds.push((pattern, load_factor));
951 }
952 self.domain = Some(domain);
953 self.current_stage += 1;
954 }
955
956 fn modal_stage_result(
957 &self,
958 domain: &mut Domain<NDIM, NDOF, NId, E>,
959 modes: &[Mode],
960 ) -> ModalStageResult {
961 let mass = domain.assemble_mass_diagonal();
962 let influence: Vec<Vec<f64>> = (0..NDIM)
966 .map(|direction| {
967 domain
968 .direction_incidence(direction)
969 .iter()
970 .copied()
971 .collect()
972 })
973 .collect();
974 let total_mass: Vec<f64> = influence
975 .iter()
976 .map(|r| r.iter().zip(mass.iter()).map(|(r, m)| r * r * m).sum())
977 .collect();
978 let modes = modes
979 .iter()
980 .map(|mode| {
981 let sign = mode
982 .shape
983 .iter()
984 .copied()
985 .fold(
986 0.0_f64,
987 |best, v| if v.abs() > best.abs() { v } else { best },
988 )
989 .signum();
990 let sign = if sign == 0.0 { 1.0 } else { sign };
991 let mut shape = Vec::with_capacity(self.node_ids.len() * NDOF);
992 for &node in &self.node_ids {
993 for dof in 0..NDOF {
994 shape.push(sign * domain.value_at(&mode.shape, node, dof));
995 }
996 }
997 let participation: Vec<f64> = influence
998 .iter()
999 .map(|r| {
1000 sign * (0..mass.len())
1001 .map(|i| mode.shape[i] * mass[i] * r[i])
1002 .sum::<f64>()
1003 })
1004 .collect();
1005 let mass_ratio = participation
1006 .iter()
1007 .zip(&total_mass)
1008 .map(|(g, total)| if *total > 0.0 { g * g / total } else { 0.0 })
1009 .collect();
1010 ModeResult {
1011 frequency: mode.frequency,
1012 shape,
1013 participation,
1014 mass_ratio,
1015 }
1016 })
1017 .collect();
1018 ModalStageResult {
1019 stage_index: self.current_stage,
1020 stage_id: self.stages[self.current_stage].id.clone(),
1021 ndf: NDOF,
1022 modes,
1023 total_mass,
1024 }
1025 }
1026
1027 fn record_sample(&mut self) {
1028 let Some(runner) = &self.runner else {
1029 return;
1030 };
1031 match runner {
1032 StageRunner::Static { analysis, .. } => {
1033 let progress = self.load_factor;
1034 for (recorder, batch) in self.recorders.iter().zip(self.current_batch.iter_mut()) {
1035 let value = match *recorder {
1036 ResolvedRecorder::NodeDisp { node, dof } => {
1037 Some(analysis.domain().node(node).displacement[dof as usize])
1038 }
1039 ResolvedRecorder::ElementForce { element, component } => Some(
1040 analysis.domain().element_end_force(element, progress)
1041 [component as usize],
1042 ),
1043 ResolvedRecorder::ElementLoad { element, component } => {
1044 Some(analysis.domain().element_load_component(
1045 element,
1046 progress,
1047 component as usize,
1048 ))
1049 }
1050 ResolvedRecorder::Reaction { node, dof } => {
1051 Some(analysis.domain().reaction(node, dof as usize, progress))
1052 }
1053 ResolvedRecorder::Fiber {
1054 element,
1055 point,
1056 fiber,
1057 response,
1058 } => fiber_value(analysis.domain(), element, point, fiber, response),
1059 ResolvedRecorder::GaussPoint {
1060 element,
1061 point,
1062 quantity,
1063 component,
1064 } => gauss_value(analysis.domain(), element, point, quantity, component),
1065 ResolvedRecorder::NodeVel { .. }
1066 | ResolvedRecorder::NodeAccel { .. }
1067 | ResolvedRecorder::ModeShape { .. } => None,
1068 };
1069 if let Some(value) = value {
1070 batch.push((progress, value));
1071 }
1072 }
1073 }
1074 StageRunner::Transient { analysis, .. } => {
1075 let progress = self.load_factor; for (recorder, batch) in self.recorders.iter().zip(self.current_batch.iter_mut()) {
1077 let value = match *recorder {
1078 ResolvedRecorder::NodeDisp { node, dof } => {
1079 Some(analysis.domain().node(node).displacement[dof as usize])
1080 }
1081 ResolvedRecorder::NodeVel { node, dof } => {
1082 Some(analysis.domain().node(node).velocity[dof as usize])
1083 }
1084 ResolvedRecorder::NodeAccel { node, dof } => {
1085 Some(analysis.domain().node(node).acceleration[dof as usize])
1086 }
1087 ResolvedRecorder::ElementForce { element, component } => Some(
1088 analysis.domain().element_end_force(element, progress)
1089 [component as usize],
1090 ),
1091 ResolvedRecorder::ElementLoad { element, component } => {
1092 Some(analysis.domain().element_load_component(
1093 element,
1094 progress,
1095 component as usize,
1096 ))
1097 }
1098 ResolvedRecorder::Reaction { node, dof } => {
1099 Some(analysis.reaction(node, dof as usize))
1100 }
1101 ResolvedRecorder::Fiber {
1102 element,
1103 point,
1104 fiber,
1105 response,
1106 } => fiber_value(analysis.domain(), element, point, fiber, response),
1107 ResolvedRecorder::GaussPoint {
1108 element,
1109 point,
1110 quantity,
1111 component,
1112 } => gauss_value(analysis.domain(), element, point, quantity, component),
1113 ResolvedRecorder::ModeShape { .. } => None,
1114 };
1115 if let Some(value) = value {
1116 batch.push((progress, value));
1117 }
1118 }
1119 }
1120 StageRunner::Reset { .. } => {}
1121 StageRunner::Modal { domain, modes, .. } => {
1122 for (recorder, batch) in self.recorders.iter().zip(self.current_batch.iter_mut()) {
1123 let ResolvedRecorder::ModeShape { mode, node, dof } = *recorder else {
1124 continue;
1125 };
1126 let Some(computed) = modes.get(mode as usize) else {
1127 continue;
1128 };
1129 let value = domain.value_at(&computed.shape, node, dof as usize);
1134 batch.push((computed.frequency, value));
1135 }
1136 }
1137 }
1138 }
1139}