diff --git a/crates/explicit/src/integrate.rs b/crates/explicit/src/integrate.rs index 194aa14..7e8a866 100644 --- a/crates/explicit/src/integrate.rs +++ b/crates/explicit/src/integrate.rs @@ -363,6 +363,7 @@ pub fn solve_explicit(model: &Model, step: Option<&str>, limits: &Limits) -> Res loop { if increments.is_multiple_of(PROGRESS_EVERY) { fieldcraft_model::progress::count(t); + fieldcraft_model::progress::check()?; } if increments >= max_increments { completed = false; diff --git a/crates/lbm/src/solve.rs b/crates/lbm/src/solve.rs index fa508ad..52e9ba9 100644 --- a/crates/lbm/src/solve.rs +++ b/crates/lbm/src/solve.rs @@ -137,6 +137,7 @@ pub fn run(setup: &Setup, flow: &mut Flow, limits: &Limits) -> Result], field: &dy }; let mut reasons: Vec = Vec::new(); for &method in methods { + fieldcraft_model::progress::check()?; let mut last = None; match method { HexMethod::Extrude | HexMethod::Revolve => { @@ -287,6 +288,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option], field: &dy } }; for setup in setups { + fieldcraft_model::progress::check()?; // The cross-section is laid out once per frame; refinement retries reuse it. let prepared = match layered::prepare(&ctx, setup) { Ok(p) => p, @@ -364,6 +366,7 @@ fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result Result) -> Result { let mut last = Error::Failed("no attempt".into()); for refine in 0..=MAX_REFINE { + fieldcraft_model::progress::check()?; match run(refine).and_then(|raw| finish(&Cells::one(ctx), raw, method)) { Ok(mesh) => return Ok(mesh), Err(e @ Error::Failed(_)) => last = e, diff --git a/crates/mesh/src/octree/mod.rs b/crates/mesh/src/octree/mod.rs index b4ecd32..f09c49d 100644 --- a/crates/mesh/src/octree/mod.rs +++ b/crates/mesh/src/octree/mod.rs @@ -251,6 +251,7 @@ pub fn octree_hex_mesh(surface: &BoundarySurface, field: &dyn SizeField, options let mut hints: Vec

= Vec::new(); let mut retries: Vec = Vec::new(); for attempt in 0..options.attempts { + fieldcraft_model::progress::check()?; let cap = if attempt == 0 { options.max_elements } else { diff --git a/crates/mesh/src/tet/improve.rs b/crates/mesh/src/tet/improve.rs index 487e631..3557ffd 100644 --- a/crates/mesh/src/tet/improve.rs +++ b/crates/mesh/src/tet/improve.rs @@ -460,6 +460,7 @@ pub fn improve(cx: &mut TetComplex, ctx: &Context, o: &ImproveOptions) -> Result stats.passes += 1; fieldcraft_model::progress::stage("Improvement pass", Some(o.max_passes as f64), ""); fieldcraft_model::progress::count(stats.passes as f64); + fieldcraft_model::progress::check()?; let before = match carried.take() { Some(b) => b, None => indicators(cx)?, diff --git a/crates/mesh/src/tet/mod.rs b/crates/mesh/src/tet/mod.rs index 4210b36..255e879 100644 --- a/crates/mesh/src/tet/mod.rs +++ b/crates/mesh/src/tet/mod.rs @@ -491,6 +491,7 @@ pub fn quality_mesh(surface: &BoundarySurface, cdt: &TetMesh, analytic: Vec Result 1 << 20 { queue.drain(..head); diff --git a/crates/solver/src/analysis.rs b/crates/solver/src/analysis.rs index 40722e1..3fee7e7 100644 --- a/crates/solver/src/analysis.rs +++ b/crates/solver/src/analysis.rs @@ -271,6 +271,7 @@ pub(crate) fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) - } FactorError::OutOfMemory { entries } => Error::Limit(format!("not enough memory for the factorization ({entries} entries)")), FactorError::Internal(what) => Error::Internal(what), + FactorError::Cancelled => Error::Model(fieldcraft_model::Error::Cancelled), FactorError::NotPositiveDefinite { column } => { let at = reduction.eq_dof.get(column).map_or_else(|| "an unknown degree of freedom".to_string(), |d| mesh.describe_dof(*d)); Error::Singular(format!( diff --git a/crates/solver/src/cholesky.rs b/crates/solver/src/cholesky.rs index 6b2bc73..11c0703 100644 --- a/crates/solver/src/cholesky.rs +++ b/crates/solver/src/cholesky.rs @@ -221,6 +221,8 @@ pub enum FactorError { OutOfMemory { entries: usize }, /// A broken internal invariant (a bug), reported instead of a panic. Internal(String), + /// The command was cancelled ([`fieldcraft_model::progress`]) between supernodes. + Cancelled, } impl FactorError { @@ -892,8 +894,17 @@ impl Symbolic { at = range.end; } let broken = std::sync::Mutex::new(None::); + // The pool's threads do not report: they ask the caller's progress whether to stop. + let progress = fieldcraft_model::progress::current(); + let cancelled = || progress.as_ref().is_some_and(|p| p.is_cancelled()); parallel::for_each_mut(&mut pieces, |_, (range, piece, failed)| { for j in range.clone() { + if cancelled() { + if let Ok(mut b) = broken.lock() { + b.get_or_insert(FactorError::Cancelled); + } + break; + } let (done, own) = piece.split_at_mut(j - range.start); let Some(own) = own.first_mut() else { break }; match supernode(&context, j, done, range.start, own) { @@ -923,6 +934,9 @@ impl Symbolic { None => e, }; for &j in &self.top { + if cancelled() { + return Err(FactorError::Cancelled); + } if earliest.as_ref().and_then(FactorError::column).is_some_and(|c| self.structure.cols(j).start > c) { break; } @@ -1078,6 +1092,14 @@ mod tests { } let e = Factor::new(&UpperColumns::from_symmetric(&dense_to_csr(&sing)), usize::MAX).unwrap_err(); assert_eq!(e, FactorError::NotPositiveDefinite { column: n - 1 }); + // A cancelled command stops between supernodes, in Cholesky and LDLᵀ alike. + let p = fieldcraft_model::progress::Progress::new(); + p.cancel(); + fieldcraft_model::progress::scoped(Some(p), || { + let upper = UpperColumns::from_symmetric(&csr); + assert_eq!(Factor::new(&upper, usize::MAX).unwrap_err(), FactorError::Cancelled); + assert_eq!(crate::ldlt::LdltFactor::new(&upper, usize::MAX).unwrap_err(), FactorError::Cancelled); + }); // Empty and 1 × 1 matrices. let empty = Csr { n: 0, ptr: vec![0], idx: vec![], val: vec![] }; let f = Factor::new(&UpperColumns::from_symmetric(&empty), 10).unwrap(); diff --git a/crates/solver/src/eigen.rs b/crates/solver/src/eigen.rs index 54e8ecf..fe3f61f 100644 --- a/crates/solver/src/eigen.rs +++ b/crates/solver/src/eigen.rs @@ -296,6 +296,7 @@ pub fn lanczos(op: &impl Operator, shift: f64, wanted: Wanted, block: usize, max let mut prev_b: Option>> = None; let mut next_check = (wanted.count + b).max(2 * b); loop { + fieldcraft_model::progress::check()?; let j0 = q.len(); // W = Op Q_j. let mut w: Vec> = Vec::with_capacity(b); diff --git a/crates/solver/src/heat.rs b/crates/solver/src/heat.rs index 0558423..15ead21 100644 --- a/crates/solver/src/heat.rs +++ b/crates/solver/src/heat.rs @@ -308,6 +308,7 @@ fn factor(a: &Csr, limits: &Limits, mesh: &HeatMesh, eqs: &Reduction) -> Result< } FactorError::OutOfMemory { entries } => Error::Limit(format!("not enough memory for the factorization ({entries} entries)")), FactorError::Internal(what) => Error::Internal(what), + FactorError::Cancelled => Error::Model(fieldcraft_model::Error::Cancelled), FactorError::NotPositiveDefinite { column } | FactorError::Singular { column } => { let node = eqs.eq_dof.get(column).and_then(|d| mesh.layout.locate(*d)).map(|(slot, _)| slot); let label = node.and_then(|n| mesh.labels.get(n)).copied().unwrap_or(0); @@ -434,6 +435,7 @@ fn run(model: &Model, resolved: &Resolved, step: &Step, limits: &Limits) -> Resu fieldcraft_model::progress::stage("Time increment", Some(steps as f64), ""); for i in 1..=steps { fieldcraft_model::progress::count((i - 1) as f64); + fieldcraft_model::progress::check()?; b.mul(&t, &mut rhs); for (r, fj) in rhs.iter_mut().zip(&f) { *r += dt * fj; diff --git a/crates/solver/src/nonlinear.rs b/crates/solver/src/nonlinear.rs index 40d37ad..dd70adc 100644 --- a/crates/solver/src/nonlinear.rs +++ b/crates/solver/src/nonlinear.rs @@ -702,7 +702,11 @@ impl Run<'_> { let mut dq = rf.clone(); let [kff] = red.reduce([&k])?; drop(k); - let Ok(chol) = factor_raw(&kff, self.limits) else { return Ok(None) }; + let chol = match factor_raw(&kff, self.limits) { + Ok(chol) => chol, + Err(FactorError::Cancelled) => return Err(Failure::Fatal(fieldcraft_model::Error::Cancelled.into())), + Err(_) => return Ok(None), + }; chol.solve(&mut dq)?; let energy = dot(&dq, &rf).abs(); Ok(dq.iter().all(|v| v.is_finite()).then(|| (q0.iter().zip(&dq).map(|(a, d)| a + d).collect(), energy))) @@ -744,6 +748,7 @@ impl Run<'_> { let mut flips = 0usize; let max_iterations = usize::try_from(self.settings.max_iterations).unwrap_or(usize::MAX); for _ in 0..max_iterations { + fieldcraft_model::progress::check().map_err(Error::from)?; // The round-off rule: nothing left to correct. if norm_inf(&r) <= ROUND_OFF * tol.force * self.scale() && self.force_floor.is_some() { return Ok(Converged { q, evaluation: eval, energy: first_energy }); @@ -1234,6 +1239,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec) -> R fieldcraft_model::progress::stage("Load factor", Some(1.0), ""); while t < 1.0 { fieldcraft_model::progress::count(lambda); + fieldcraft_model::progress::check()?; if history.increments.len() >= max_increments { return Err(Error::Limit(format!( "step `{}`: {} increments reached the step's cap at load factor {lambda:.6} (raise max_increments)", @@ -1663,6 +1669,7 @@ fn arc_length( let mut last: Option = None; let max_increments = usize::try_from(settings.max_increments).unwrap_or(usize::MAX); loop { + fieldcraft_model::progress::check()?; if history.increments.len() >= max_increments { warnings.push(format!( "the arc-length step `{}` ended at its cap of {} increments, at load factor {lambda:.6}",