forked from 1b-agent/fieldcraft-upstream
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:
1 parent
b2fcf2c5bc
commit
b5f71612af
4 files changed
+73
-11
No files matched your search
@@ -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, ¶ms)))
|
||||
};
|
||||
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());
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user