forked from 1b-agent/fieldcraft-upstream
mesh: a node move measures its region in parallel and can reuse its baseline
move_nodes_with takes the region's quality before the move from an earlier run and a coarser guard bisection, for a live preview; each pass measures the elements in parallel (fixed blocks). The relaxation's state sizes its elements (a certified check each) only for the strong mode, which alone uses the size: on the crank arm's hex20 mesh that was 114 ms of every 130 ms preview. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
53a370ed01
commit
1dd2afe525
4 files changed
+76
-20
No files matched your search
Generated
+1
@@ -1433,6 +1433,7 @@ name = "fieldcraft-mesh"
|
|||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fieldcraft-model",
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-parallel",
|
||||||
"libm",
|
"libm",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-rational",
|
"num-rational",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fieldcraft-model = { workspace = true }
|
fieldcraft-model = { workspace = true }
|
||||||
|
fieldcraft-parallel = { workspace = true }
|
||||||
# Pure-Rust maths (a port of musl's libm) for sin, cos, log, exp, pow, hypot, acos: the same
|
# Pure-Rust maths (a port of musl's libm) for sin, cos, log, exp, pow, hypot, acos: the same
|
||||||
# bits on every platform, where the system library's are not correctly rounded and differ (the
|
# bits on every platform, where the system library's are not correctly rounded and differ (the
|
||||||
# plate's hex mesh differed between macOS and Linux, #438). atan2 and cbrt are crate::vec3's.
|
# plate's hex mesh differed between macOS and Linux, #438). atan2 and cbrt are crate::vec3's.
|
||||||
|
|||||||
@@ -275,7 +275,8 @@ struct Elem {
|
|||||||
nodes: Vec<u32>,
|
nodes: Vec<u32>,
|
||||||
/// A shell's normal as it was: its side, which a move must keep.
|
/// A shell's normal as it was: its side, which a move must keep.
|
||||||
normal: Option<P>,
|
normal: Option<P>,
|
||||||
/// Its size as it was ([`strong::size`]): the strong mode keeps sizes near it.
|
/// Its size as it was ([`strong::size`]): the strong mode keeps sizes near it (only the strong
|
||||||
|
/// mode measures it, a certified check per element; 0 otherwise).
|
||||||
size0: f64,
|
size0: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +571,13 @@ impl<'a> State<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.elems.push(Elem { label, kind: e.kind, normal: shell_normal(e.kind, &x), size0: strong::size(e.kind, &x), nodes: e.nodes.clone() });
|
state.elems.push(Elem {
|
||||||
|
label,
|
||||||
|
kind: e.kind,
|
||||||
|
normal: shell_normal(e.kind, &x),
|
||||||
|
size0: if pin_curves { strong::size(e.kind, &x) } else { 0.0 },
|
||||||
|
nodes: e.nodes.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
@@ -941,8 +948,8 @@ pub fn apply_field(
|
|||||||
mod moving;
|
mod moving;
|
||||||
mod strong;
|
mod strong;
|
||||||
pub use moving::{
|
pub use moving::{
|
||||||
DEFAULT_MIN_QUALITY, ElementChange, GUARD_STEPS, Guard, Influence, MIN_RELATIVE_RADIUS, Moved, Propagate, Unconstrained, centroid, move_nodes,
|
DEFAULT_MIN_QUALITY, ElementChange, GUARD_STEPS, Guard, Influence, MIN_RELATIVE_RADIUS, MoveRun, Moved, Propagate, Unconstrained, centroid,
|
||||||
move_weights, nearest_corner,
|
move_nodes, move_nodes_with, move_weights, nearest_corner,
|
||||||
};
|
};
|
||||||
pub use strong::{DEFAULT_REACH_LENGTHS, Guards, MAX_ROUNDS, MorphOptions, Morphed, Round, morph};
|
pub use strong::{DEFAULT_REACH_LENGTHS, Guards, MAX_ROUNDS, MorphOptions, Morphed, Round, morph};
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
|
|
||||||
use fieldcraft_model::Model;
|
use fieldcraft_model::Model;
|
||||||
|
|
||||||
use super::{Constraints, Falloff, Freedom, P, RegionQuality, State, add, corner_nodes, dist, dot, measure, region_quality, scale, sub};
|
use super::{Constraints, Falloff, Freedom, P, RegionQuality, State, add, corner_nodes, dist, dot, measure, scale, sub};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
/// How the displacement reaches the nodes round the grabbed ones.
|
/// How the displacement reaches the nodes round the grabbed ones.
|
||||||
@@ -273,6 +273,51 @@ const SAME: f64 = 1e-9;
|
|||||||
/// Move each node of `weights` by its share of `d`, as `geometry` lets it, the displacement
|
/// Move each node of `weights` by its share of `d`, as `geometry` lets it, the displacement
|
||||||
/// scaled back under `guard`. The model is not changed: the result has the new positions.
|
/// scaled back under `guard`. The model is not changed: the result has the new positions.
|
||||||
pub fn move_nodes(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guard, geometry: &dyn Constraints) -> Result<Moved> {
|
pub fn move_nodes(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guard, geometry: &dyn Constraints) -> Result<Moved> {
|
||||||
|
move_nodes_with(model, weights, d, guard, geometry, &MoveRun::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How [`move_nodes_with`] runs: what a live preview reuses and how finely it guards.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MoveRun {
|
||||||
|
/// The region's elements' quality and validity before any move, in label order (from an
|
||||||
|
/// earlier run's [`Moved::elements`] with the same weights); measured again when absent or
|
||||||
|
/// of another length.
|
||||||
|
pub baseline: Option<Vec<(f64, bool)>>,
|
||||||
|
/// Halvings of the guard's bisection; 0 is [`GUARD_STEPS`].
|
||||||
|
pub guard_steps: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each element's quality and validity at the state's positions, measured in parallel (fixed
|
||||||
|
/// blocks: the same result on any thread count).
|
||||||
|
fn measure_all(elems: &[super::Elem], pos: &BTreeMap<u32, P>) -> Vec<(f64, bool)> {
|
||||||
|
fieldcraft_parallel::map(elems.len(), |i| {
|
||||||
|
let Some(e) = elems.get(i) else { return (-1.0, false) };
|
||||||
|
let x: Vec<P> = e.nodes.iter().map(|n| pos.get(n).copied().unwrap_or([0.0; 3])).collect();
|
||||||
|
measure(e, &x)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The region's quality from its elements' measures.
|
||||||
|
fn summary(state: &State<'_>, values: &[(f64, bool)]) -> RegionQuality {
|
||||||
|
let mut r = RegionQuality { elements: state.elems.len(), min_quality: f64::INFINITY, max_edge_ratio: 0.0, invalid: 0, worst: Vec::new() };
|
||||||
|
let mut all = Vec::with_capacity(values.len());
|
||||||
|
for (e, &(q, valid)) in state.elems.iter().zip(values) {
|
||||||
|
r.min_quality = r.min_quality.min(q);
|
||||||
|
r.max_edge_ratio = r.max_edge_ratio.max(super::edge_ratio(e.kind, &state.coords(e)));
|
||||||
|
r.invalid += usize::from(!valid);
|
||||||
|
all.push((e.label, q));
|
||||||
|
}
|
||||||
|
all.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
|
||||||
|
all.truncate(super::WORST_LISTED);
|
||||||
|
r.worst = all;
|
||||||
|
if r.elements == 0 {
|
||||||
|
r.min_quality = 0.0;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`move_nodes`], reusing what `run` carries.
|
||||||
|
pub fn move_nodes_with(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guard, geometry: &dyn Constraints, run: &MoveRun) -> Result<Moved> {
|
||||||
if !d.iter().all(|x| x.is_finite()) || dot(d, d).sqrt() > fieldcraft_model::MAX_COORD {
|
if !d.iter().all(|x| x.is_finite()) || dot(d, d).sqrt() > fieldcraft_model::MAX_COORD {
|
||||||
return Err(Error::Invalid("the displacement must be finite".into()));
|
return Err(Error::Invalid("the displacement must be finite".into()));
|
||||||
}
|
}
|
||||||
@@ -287,11 +332,15 @@ pub fn move_nodes(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guar
|
|||||||
if let Some((n, w)) = weights.iter().find(|(_, w)| !(**w > 0.0 && **w <= 1.0)) {
|
if let Some((n, w)) = weights.iter().find(|(_, w)| !(**w > 0.0 && **w <= 1.0)) {
|
||||||
return Err(Error::Invalid(format!("node {n} has share {w}; shares are in (0, 1]")));
|
return Err(Error::Invalid(format!("node {n} has share {w}; shares are in (0, 1]")));
|
||||||
}
|
}
|
||||||
|
let steps = if run.guard_steps == 0 { GUARD_STEPS } else { run.guard_steps.min(GUARD_STEPS) };
|
||||||
let movers: BTreeSet<u32> = weights.keys().copied().collect();
|
let movers: BTreeSet<u32> = weights.keys().copied().collect();
|
||||||
let mut state = State::new(model, &movers, geometry, false)?;
|
let mut state = State::new(model, &movers, geometry, false)?;
|
||||||
let original = state.pos.clone();
|
let original = state.pos.clone();
|
||||||
let start: Vec<(f64, bool)> = state.elems.iter().map(|e| measure(e, &state.coords(e))).collect();
|
let start = match &run.baseline {
|
||||||
let before = region_quality(&state);
|
Some(b) if b.len() == state.elems.len() => b.clone(),
|
||||||
|
_ => measure_all(&state.elems, &state.pos),
|
||||||
|
};
|
||||||
|
let before = summary(&state, &start);
|
||||||
let freedom: BTreeMap<u32, Freedom> = movers.iter().map(|&n| (n, state.freedom(n))).collect();
|
let freedom: BTreeMap<u32, Freedom> = movers.iter().map(|&n| (n, state.freedom(n))).collect();
|
||||||
// The state with the displacement applied at scale `t`.
|
// The state with the displacement applied at scale `t`.
|
||||||
let apply = |state: &mut State<'_>, t: f64| {
|
let apply = |state: &mut State<'_>, t: f64| {
|
||||||
@@ -308,34 +357,33 @@ pub fn move_nodes(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guar
|
|||||||
};
|
};
|
||||||
// Whether every element valid before is still valid, and no worse than the guard's floor
|
// Whether every element valid before is still valid, and no worse than the guard's floor
|
||||||
// (or its own quality, when that was lower).
|
// (or its own quality, when that was lower).
|
||||||
let sound = |state: &State<'_>| {
|
let sound = |now: &[(f64, bool)]| now.iter().zip(&start).all(|(&(q, valid), &(q0, was))| !was || (valid && q >= floor.min(q0)));
|
||||||
state.elems.iter().zip(&start).all(|(e, &(q0, was))| {
|
|
||||||
let (q, valid) = measure(e, &state.coords(e));
|
|
||||||
!was || (valid && q >= floor.min(q0))
|
|
||||||
})
|
|
||||||
};
|
|
||||||
apply(&mut state, 1.0);
|
apply(&mut state, 1.0);
|
||||||
|
let mut now = measure_all(&state.elems, &state.pos);
|
||||||
let mut t = 1.0;
|
let mut t = 1.0;
|
||||||
if guard != Guard::None && !sound(&state) {
|
if guard != Guard::None && !sound(&now) {
|
||||||
let (mut lo, mut hi) = (0.0, 1.0);
|
let (mut lo, mut hi) = (0.0, 1.0);
|
||||||
for _ in 0..GUARD_STEPS {
|
let mut at_lo = None;
|
||||||
|
for _ in 0..steps {
|
||||||
fieldcraft_model::progress::check()?;
|
fieldcraft_model::progress::check()?;
|
||||||
let mid = 0.5 * (lo + hi);
|
let mid = 0.5 * (lo + hi);
|
||||||
apply(&mut state, mid);
|
apply(&mut state, mid);
|
||||||
if sound(&state) {
|
let m = measure_all(&state.elems, &state.pos);
|
||||||
|
if sound(&m) {
|
||||||
lo = mid;
|
lo = mid;
|
||||||
|
at_lo = Some(m);
|
||||||
} else {
|
} else {
|
||||||
hi = mid;
|
hi = mid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t = lo;
|
t = lo;
|
||||||
apply(&mut state, t);
|
apply(&mut state, t);
|
||||||
|
now = at_lo.unwrap_or_else(|| start.clone());
|
||||||
}
|
}
|
||||||
let after = region_quality(&state);
|
let after = summary(&state, &now);
|
||||||
let mut out = Moved { before, after, scale: t, ..Moved::default() };
|
let mut out = Moved { before, after, scale: t, ..Moved::default() };
|
||||||
let (mut sum_before, mut sum_after) = (0.0, 0.0);
|
let (mut sum_before, mut sum_after) = (0.0, 0.0);
|
||||||
for (e, &(q0, v0)) in state.elems.iter().zip(&start) {
|
for (e, (&(q0, v0), &(q1, v1))) in state.elems.iter().zip(start.iter().zip(&now)) {
|
||||||
let (q1, v1) = measure(e, &state.coords(e));
|
|
||||||
sum_before += q0;
|
sum_before += q0;
|
||||||
sum_after += q1;
|
sum_after += q1;
|
||||||
let change = ElementChange { label: e.label, before: q0, after: q1, valid_before: v0, valid_after: v1 };
|
let change = ElementChange { label: e.label, before: q0, after: q1, valid_before: v0, valid_after: v1 };
|
||||||
@@ -347,7 +395,6 @@ pub fn move_nodes(model: &Model, weights: &BTreeMap<u32, f64>, d: P, guard: Guar
|
|||||||
out.mean_before = sum_before / state.elems.len() as f64;
|
out.mean_before = sum_before / state.elems.len() as f64;
|
||||||
out.mean_after = sum_after / state.elems.len() as f64;
|
out.mean_after = sum_after / state.elems.len() as f64;
|
||||||
}
|
}
|
||||||
out.elements.sort_by_key(|c| c.label);
|
|
||||||
for (n, p) in &state.pos {
|
for (n, p) in &state.pos {
|
||||||
let Some(o) = original.get(n) else { continue };
|
let Some(o) = original.get(n) else { continue };
|
||||||
let moved = dist(*o, *p);
|
let moved = dist(*o, *p);
|
||||||
|
|||||||
Reference in new issue
Block a user