mesh: mesh.plan.apply, mesh.cells and geometry.partition report their stages and pass a cancel through

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-17 08:12:04 -04:00
1 parent b2fcf2c5bc
commit b5f71612af
4 files changed
+73 -11

No files matched your search

+32 -4
View File
@@ -18,9 +18,15 @@ const PROMPT: Duration = Duration::from_secs(5);
/// (mid-run), or before it starts (`None`): it must fail with `cancelled`, soon after, leaving
/// the model and the journal as they were.
fn cancel(s: &mut Session, command: &str, params: Value, stage: Option<&str>) {
cancel_when(s, command, params, stage.map(|stage| move |now: &progress::State| now.stage.starts_with(stage) && now.done > 0.0));
}
/// [`cancel`] once the reported state passes `when` (before it starts with `None`).
fn cancel_when(s: &mut Session, command: &str, params: Value, when: Option<impl Fn(&progress::State) -> bool>) {
let (model, journal) = (s.model.clone(), s.journal.len());
let design = s.design.as_ref().map(|d| d.bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>());
let p = Progress::new();
if stage.is_none() {
if when.is_none() {
p.cancel();
}
let (r, waited) = std::thread::scope(|scope| {
@@ -31,18 +37,18 @@ fn cancel(s: &mut Session, command: &str, params: Value, stage: Option<&str>) {
scope.spawn(move || progress::scoped(Some(p), || s.execute(command, &params)))
};
let mut at = Instant::now();
if let Some(stage) = stage {
if let Some(when) = &when {
let deadline = Instant::now() + Duration::from_secs(600);
while !worker.is_finished() && Instant::now() < deadline {
let now = p.snapshot();
if now.stage.starts_with(stage) && now.done > 0.0 {
if when(&now) {
at = Instant::now();
p.cancel();
break;
}
std::thread::sleep(Duration::from_millis(1));
}
assert!(p.is_cancelled(), "{command}: never reached stage `{stage}` ({:?})", p.snapshot());
assert!(p.is_cancelled(), "{command}: never reached the stage ({:?})", p.snapshot());
}
let r = worker.join().unwrap();
(r, at.elapsed())
@@ -53,6 +59,7 @@ fn cancel(s: &mut Session, command: &str, params: Value, stage: Option<&str>) {
assert!(waited < PROMPT, "{command}: stopped {waited:?} after the cancel");
assert!(s.model == model, "{command}: the model changed");
assert_eq!(s.journal.len(), journal, "{command}: journalled");
assert_eq!(s.design.as_ref().map(|d| d.bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>()), design, "{command}: the design changed");
}
/// A sample's script run on a fresh session.
@@ -128,3 +135,24 @@ fn meshers_stop_and_leave_no_mesh() {
cancel(&mut s, "mesh.hex.octree", json!({"body": "Body1", "size": 5.0, "max_depth": 2}), None);
assert!(s.model.nodes().is_empty());
}
/// The partition, the cells mesher and the plan's apply (#545) stop at their stages: between
/// the kernel's cuts, between cells, between the projection's phases.
#[test]
fn partition_cells_and_plan_apply_stop_and_change_nothing() {
let (_dir, mut s) = plate();
s.execute("geometry.datum", &json!({"name": "X30", "by": {"type": "offset", "plane": "yz", "offset": 30}})).unwrap();
s.execute("geometry.datum", &json!({"name": "Y5", "by": {"type": "offset", "plane": "zx", "offset": 5}})).unwrap();
let plan = s.execute("mesh.plan", &json!({"body": "Body1"})).unwrap();
cancel(&mut s, "mesh.plan.apply", json!({"plan": plan["name"]}), None);
cancel_when(&mut s, "mesh.plan.apply", json!({"plan": plan["name"]}), Some(|now: &progress::State| now.stage.starts_with("Projection: ")));
cancel(&mut s, "geometry.partition", json!({"name": "Half", "datum": "X30"}), None);
s.execute("geometry.partition", &json!({"name": "Half", "datum": "X30"})).unwrap();
// The second partition cuts again by both: cancelled once the first cut is made.
cancel(&mut s, "geometry.partition", json!({"name": "Strip", "datum": "Y5"}), Some("Partition cuts"));
let cells = json!({"bodies": ["Body1/a", "Body1/b"], "default": "tet", "size": 0.5});
cancel(&mut s, "mesh.cells", cells.clone(), None);
// Inside a cell's tet mesher: the cancel reaches the command, never a fallback.
cancel(&mut s, "mesh.cells", cells, Some("Refinement round"));
assert!(s.model.nodes().is_empty());
}
+12
View File
@@ -201,6 +201,8 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
if members.len() < 2 {
continue;
}
fieldcraft_model::progress::stage(&format!("Cells: sweeping {source}'s cells together"), None, "");
fieldcraft_model::progress::check()?;
let inputs: Vec<Input> = members
.iter()
.filter_map(|&i| shapes.get(i))
@@ -227,6 +229,9 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
if joint.get(i).copied().flatten().is_some() {
continue;
}
fieldcraft_model::progress::stage(&format!("Cells: meshing {}", shape.name), Some(shapes.len() as f64), "cells");
fieldcraft_model::progress::count(i as f64);
fieldcraft_model::progress::check()?;
let method = method_of(&shape.name);
let input = || Ok::<_, EngineError>(Input { seeds: seeds.clone(), ..super::hex::shape_input(shape, Some(tolerance))? });
let hex = |m: HexMethod| -> Result<(Input<'static>, HexMesh)> {
@@ -246,6 +251,8 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
let info = json!({"method": mesh.report.method.name()});
(Meshed::Hex(Box::new((b, mesh))), info)
}
// A cancelled job stops; any other failure falls back to tet.
Err(e) if e.is_cancelled() => return Err(e),
Err(e) => (Meshed::Tet, json!({"method": "tet", "why": e.to_string()})),
},
};
@@ -301,6 +308,9 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
}
for (i, mut info) in tet_cells {
let Some(shape) = shapes.get(i) else { continue };
fieldcraft_model::progress::stage(&format!("Cells: meshing {} (tet)", shape.name), Some(shapes.len() as f64), "cells");
fieldcraft_model::progress::count(i as f64);
fieldcraft_model::progress::check()?;
let input = Input { seeds: seeds.clone(), ..super::hex::shape_input(shape, Some(tolerance))? };
let pyramids = if transition == Transition::Tie {
Err("`transition` is \"tie\"".to_string())
@@ -348,6 +358,8 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
.filter(|r| r.get("kind").and_then(Value::as_str).is_some_and(|k| k.starts_with("tet")))
.filter_map(|r| r.get("body").and_then(Value::as_str).map(String::from))
.collect();
fieldcraft_model::progress::stage("Cells: joining the cells", None, "");
fieldcraft_model::progress::check()?;
interfaces.extend(join(model, &names, &tet, COINCIDENT * diagonal)?);
explain_ties(&design, &report, &mut interfaces);
let (nodes, elements) = (model.nodes().len(), model.elements().len());
+4 -1
View File
@@ -527,7 +527,10 @@ fn partition(s: &mut Session, p: &Value) -> Result<Value> {
let cells: Vec<String> = design.bodies.iter().map(|b| b.name.clone()).filter(|b| split.iter().any(|x| b.starts_with(&format!("{x}/")))).collect();
let (faces, bodies): (usize, Vec<String>) = (report.map_or(0, |r| r.faces), design.bodies.iter().map(|b| b.name.clone()).collect());
let mut warnings: Vec<String> = report.map(|r| r.warnings.clone()).unwrap_or_default();
for cell in &cells {
fieldcraft_model::progress::stage("Partition: checking the cells", Some(cells.len() as f64), "cells");
for (k, cell) in cells.iter().enumerate() {
fieldcraft_model::progress::count(k as f64);
fieldcraft_model::progress::check()?;
if let Some(why) = design.body(cell).and_then(|shape| unmeshable(shape, None).err()) {
warnings.push(format!(
"partition `{name}`: cell `{cell}` has a boundary no mesher takes ({why}): the plane passes within the kernel's tolerance of a face, edge or vertex it cut (along a face, through the axis of a hole); move the plane off it (by a fraction of the element size)"
+25 -6
View File
@@ -1133,6 +1133,8 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
// The copies: each meshed by a plan of its own at this plan's size, saved beside it.
let mut copies = Vec::with_capacity(plan.copies.len());
for copy in &plan.copies {
fieldcraft_model::progress::stage(&format!("Plan: meshing the copy {copy}"), None, "");
fieldcraft_model::progress::check()?;
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
if design.source_body(copy).is_none() {
return Err(EngineError::Params(format!("mesh plan `{name}`: no copy `{copy}` in the design")));
@@ -1250,7 +1252,13 @@ fn apply_one(s: &mut Session, name: &str, plan: &MeshPlan) -> Result<Value> {
let body = cells.first().cloned().unwrap_or_else(|| plan.body.clone());
(super::hex::HEX_GENERATE.run)(s, &json!({"body": body, "size": plan.size, "method": "swept", "tolerance": tolerance}))
}
.map_err(|e| EngineError::Params(format!("mesh plan `{name}`: the cells {cells:?} (cuts {made:?}) do not mesh all-hex: {e}")))?;
.map_err(|e| {
if e.is_cancelled() {
e
} else {
EngineError::Params(format!("mesh plan `{name}`: the cells {cells:?} (cuts {made:?}) do not mesh all-hex: {e}"))
}
})?;
let summary = summary(&mesh, &cells);
// What today's meshers cannot express yet: the rings round circles are built by the
// projection mesher (#399); until then the frame cuts box the circle and the sweep meshes the
@@ -1446,6 +1454,8 @@ fn apply_symmetric(s: &mut Session, name: &str, plan: &MeshPlan, corners: bool)
let cells: Vec<String> = design.bodies.iter().filter(|b| design.source_of(&b.name) == Some(plan.body.as_str())).map(|b| b.name.clone()).collect();
let meshed_first = meshed.clone();
for (a, c) in &planes {
fieldcraft_model::progress::stage("Plan: mirroring the cell", None, "");
fieldcraft_model::progress::check()?;
(super::mirror::MIRROR.run)(s, &json!({"plane": {"point": a.map(|x| x * c), "normal": a}, "bodies": meshed}))?;
meshed = cells.iter().filter(|n| !s.model.body_elements(n).is_empty()).cloned().collect();
}
@@ -1632,6 +1642,7 @@ fn copy_corner(
}
Ok(Ok(copy))
}
Err(e) if e.is_cancelled() => Err(e),
// A corner copy that does not mesh leaves the cell to be meshed whole.
Err(e) => {
s.restore(before);
@@ -1663,9 +1674,13 @@ fn corner_meshes(
// planar, the corner taken from that mesh: independent meshes, made side by side. The best
// corner wins.
let whole = &whole_body;
let runs = fieldcraft_parallel::map(candidates.len() + 1, |k| match k.checked_sub(1).and_then(|i| candidates.get(i)) {
None => super::project::project_only(whole, cell, t0, &opts).map(|m| (m.report.min_scaled_jacobian, None)),
Some(&x1) => corner_of_whole(whole, cell, t0, a, [x1, 2.0 * c - x1], &opts).map(|r| (r.sj, Some(([x1, 2.0 * c - x1], r)))),
// The job's progress on the pool's threads too, so a cancel stops the candidates.
let progress = fieldcraft_model::progress::current();
let runs = fieldcraft_parallel::map(candidates.len() + 1, |k| {
fieldcraft_model::progress::scoped(progress.clone(), || match k.checked_sub(1).and_then(|i| candidates.get(i)) {
None => super::project::project_only(whole, cell, t0, &opts).map(|m| (m.report.min_scaled_jacobian, None)),
Some(&x1) => corner_of_whole(whole, cell, t0, a, [x1, 2.0 * c - x1], &opts).map(|r| (r.sj, Some(([x1, 2.0 * c - x1], r)))),
})
});
let mut reference: Option<f64> = None;
let mut best: Option<(f64, [f64; 2], CornerOfWhole)> = None;
@@ -1677,6 +1692,7 @@ fn corner_meshes(
best = Some((sj, at, corner));
}
}
Err(e) if e.is_cancelled() => return Err(e),
Err(_) => {}
}
}
@@ -1873,8 +1889,9 @@ fn apply_projection(s: &mut Session, name: &str, plan: &MeshPlan, tolerance: f64
let (fixed, fixed_faces) = cut_face_patches(s, &body, &plan.body, plan.size)?;
let field = super::sizes::sizing(s)?.local_field(plan.size)?;
let opts = fieldcraft_mesh::project::ProjectOptions { size: plan.size, fixed, field, ..Default::default() };
let mut mesh = super::project::project_body(s, &plan.body, body, &layout.topology, &opts, merge)
.map_err(|e| EngineError::Params(format!("mesh plan `{name}`: the projected layout does not mesh: {e}")))?;
let mut mesh = super::project::project_body(s, &plan.body, body, &layout.topology, &opts, merge).map_err(|e| {
if e.is_cancelled() { e } else { EngineError::Params(format!("mesh plan `{name}`: the projected layout does not mesh: {e}")) }
})?;
if let Some(m) = mesh.as_object_mut() {
// The block listing is long; the plan reports the layout instead.
m.remove("topology");
@@ -1946,6 +1963,8 @@ fn cut(
let cleared = super::datum::recut(s)?;
let mut skipped: Vec<(String, String)> = Vec::new();
for _ in 0..=names.len() {
fieldcraft_model::progress::stage("Plan: checking the cells", None, "");
fieldcraft_model::progress::check()?;
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first".into()))?;
let live: Vec<&String> = names.iter().filter(|n| !skipped.iter().any(|(k, _)| k == *n)).collect();
let mut failed: Vec<(String, String)> = live