runmat_analysis_fea/
progress.rs1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum FeaProgressPhase {
10 GeometryLoad,
11 RegionResolution,
12 MeshPrep,
13 ModelAssembly,
14 Solve,
15 Postprocess,
16 ArtifactPersistence,
17 Complete,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum FeaProgressStatus {
23 Started,
24 Advanced,
25 Completed,
26 Failed,
27 Cancelled,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct FeaProgressEvent {
33 pub operation: String,
34 pub phase: FeaProgressPhase,
35 pub status: FeaProgressStatus,
36 pub message: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub current: Option<u64>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub total: Option<u64>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub fraction: Option<f64>,
43 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
44 pub metadata: BTreeMap<String, String>,
45}
46
47pub type FeaProgressHandler = Arc<dyn Fn(FeaProgressEvent) + Send + Sync + 'static>;
48pub type FeaCancellationPredicate = Arc<dyn Fn() -> bool + Send + Sync + 'static>;
49
50#[derive(Clone)]
51struct FeaProgressContext {
52 handler: Option<FeaProgressHandler>,
53 cancellation: Option<FeaCancellationPredicate>,
54}
55
56thread_local! {
57 static FEA_PROGRESS_CONTEXT: RefCell<Option<FeaProgressContext>> = const { RefCell::new(None) };
58}
59
60pub struct FeaProgressContextGuard {
61 previous: Option<FeaProgressContext>,
62}
63
64impl Drop for FeaProgressContextGuard {
65 fn drop(&mut self) {
66 FEA_PROGRESS_CONTEXT.with(|slot| {
67 slot.replace(self.previous.take());
68 });
69 }
70}
71
72pub fn replace_fea_progress_context(
73 handler: Option<FeaProgressHandler>,
74 cancellation: Option<FeaCancellationPredicate>,
75) -> FeaProgressContextGuard {
76 let previous = FEA_PROGRESS_CONTEXT.with(|slot| {
77 slot.replace(Some(FeaProgressContext {
78 handler,
79 cancellation,
80 }))
81 });
82 FeaProgressContextGuard { previous }
83}
84
85pub fn emit_progress(event: FeaProgressEvent) {
86 let handler = FEA_PROGRESS_CONTEXT.with(|slot| {
87 slot.borrow()
88 .as_ref()
89 .and_then(|context| context.handler.clone())
90 });
91 if let Some(handler) = handler {
92 handler(event);
93 }
94}
95
96pub fn emit_phase(
97 operation: impl Into<String>,
98 phase: FeaProgressPhase,
99 status: FeaProgressStatus,
100 message: impl Into<String>,
101 current: Option<u64>,
102 total: Option<u64>,
103) {
104 let fraction = match (current, total) {
105 (Some(current), Some(total)) if total > 0 => Some((current as f64 / total as f64).min(1.0)),
106 _ => None,
107 };
108 emit_progress(FeaProgressEvent {
109 operation: operation.into(),
110 phase,
111 status,
112 message: message.into(),
113 current,
114 total,
115 fraction,
116 metadata: BTreeMap::new(),
117 });
118}
119
120pub fn is_cancelled() -> bool {
121 FEA_PROGRESS_CONTEXT.with(|slot| {
122 slot.borrow()
123 .as_ref()
124 .and_then(|context| context.cancellation.clone())
125 .map(|is_cancelled| is_cancelled())
126 .unwrap_or(false)
127 })
128}
129
130pub fn check_cancelled(operation: impl Into<String>) -> Result<(), crate::contracts::FeaRunError> {
131 if is_cancelled() {
132 emit_phase(
133 operation,
134 FeaProgressPhase::Complete,
135 FeaProgressStatus::Cancelled,
136 "FEA solve cancelled",
137 None,
138 None,
139 );
140 return Err(crate::contracts::FeaRunError::Cancelled);
141 }
142 Ok(())
143}