mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 03:50:04 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b6dde7d6e |
@@ -234,8 +234,9 @@ mod budget_tests {
|
||||
};
|
||||
let comp = Comp { model, built: vec![endpoint], weights: vec![1.0; 2], held: Vec::new(), tol: 1e-9 };
|
||||
assert!(analyse(&comp).dof.is_none());
|
||||
// Solving it is refused for the same reason, before any Jacobian is built.
|
||||
assert!(matches!(comp.run(None), Err(SolveError::TooLarge)));
|
||||
// Its residuals already hold (the other object is missing), so solving does not iterate
|
||||
// and is not refused; an unsatisfied system of this size is (see below).
|
||||
assert!(comp.run(None).is_ok_and(|o| o.converged));
|
||||
}
|
||||
|
||||
/// Many Tangent constraints on one large fit spline rebuild it once while being interpreted,
|
||||
|
||||
@@ -274,7 +274,8 @@ impl Comp {
|
||||
}
|
||||
/// Work of one solver iteration's residual evaluations over `free`: every residual twice per
|
||||
/// free parameter, plus a spline rebuild each time a spline's own defining points move (rebuilds
|
||||
/// are shared by residuals at the same points, see [`Model`]).
|
||||
/// are shared by residuals at the same points, see [`Model`]). A spline none of whose points
|
||||
/// is free never moves, so its one rebuild is reused by every evaluation.
|
||||
pub(crate) fn eval_work(&self, free: &[usize], skip: Option<usize>) -> f64 {
|
||||
let rows = self.rows(skip) as f64;
|
||||
let evals = 2.0 * free.len() as f64 + TRIAL_EVALS;
|
||||
@@ -291,11 +292,18 @@ impl Comp {
|
||||
.filter_map(|h| self.model.slot(*h))
|
||||
.map(|s| {
|
||||
let own = free.iter().filter(|j| (s.off..s.off + s.len).contains(*j)).count() as f64;
|
||||
(2.0 * own + TRIAL_EVALS + 1.0) * self.model.spline_reconstruct_cost(s.handle)
|
||||
let rebuilds = if own == 0.0 { 1.0 } else { 2.0 * own + TRIAL_EVALS + 1.0 };
|
||||
rebuilds * self.model.spline_reconstruct_cost(s.handle)
|
||||
})
|
||||
.sum();
|
||||
evals * rows + rebuilds
|
||||
}
|
||||
/// Do the residuals hold at `x`, as [`lm::solve`] decides before its first iteration?
|
||||
fn holds(&self, x: &[f64], skip: Option<usize>) -> bool {
|
||||
let mut out = Vec::new();
|
||||
self.eval(x, &mut out, skip);
|
||||
out.is_empty() || lm::max_abs(&out) <= self.tol
|
||||
}
|
||||
fn run(&self, skip: Option<usize>) -> Result<lm::Outcome, SolveError> {
|
||||
let free = self.free(skip);
|
||||
let rows = self.rows(skip);
|
||||
@@ -303,8 +311,11 @@ impl Comp {
|
||||
return Err(SolveError::TooLarge);
|
||||
}
|
||||
// Numerical differentiation rebuilds a fit spline through a dense solve for every move of
|
||||
// its own fit points: refuse before iterating when that alone is too much (CWE-407).
|
||||
if self.eval_work(&free, skip) > MAX_EVAL_WORK {
|
||||
// its own fit points: refuse before iterating when that alone is too much (CWE-407). A
|
||||
// system that already holds is not iterated (`lm::solve` stops after one evaluation), so
|
||||
// it is not refused: a large component left as it is must not stop the rest of the
|
||||
// drawing from being solved.
|
||||
if self.eval_work(&free, skip) > MAX_EVAL_WORK && !self.holds(&self.model.x0, skip) {
|
||||
return Err(SolveError::TooLarge);
|
||||
}
|
||||
let f = |x: &[f64], out: &mut Vec<f64>| self.eval(x, out, skip);
|
||||
@@ -790,6 +801,11 @@ pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u3
|
||||
&& nb.rows > 0
|
||||
{
|
||||
let free = comp.free(None);
|
||||
// `run` lets a system that already holds through without its work budget; this Jacobian
|
||||
// is built regardless, so it keeps that budget (CWE-407).
|
||||
if comp.eval_work(&free, None) > MAX_EVAL_WORK {
|
||||
return Err(SolveError::TooLarge);
|
||||
}
|
||||
let mut ordered: Vec<&Built> = comp.built.iter().filter(|b| b.id != id).collect();
|
||||
ordered.push(nb);
|
||||
let f = |x: &[f64], out: &mut Vec<f64>| {
|
||||
|
||||
@@ -516,3 +516,62 @@ fn next_name_skips_names_in_use() {
|
||||
});
|
||||
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d2");
|
||||
}
|
||||
|
||||
/// A straight open fit spline through `n` points from `from` in steps of `step`.
|
||||
fn straight_fit_spline(d: &mut Drawing, n: usize, from: (f64, f64), step: (f64, f64)) -> Handle {
|
||||
let pts: Vec<Vec2> = (0..n).map(|i| Vec2::new(from.0 + step.0 * i as f64, from.1 + step.1 * i as f64)).collect();
|
||||
add_e(d, EntityKind::Spline(cadcraft_geom::Spline::fit_with(&pts, false, Default::default())))
|
||||
}
|
||||
|
||||
fn push_con(d: &mut Drawing, kind: ConstraintKind, refs: &[GeomRef]) {
|
||||
let id = d.constraints.iter().map(|c| c.id).max().unwrap_or(0) + 1;
|
||||
d.constraints.push(Constraint { id, kind, refs: refs.to_vec(), name: String::new(), expr: String::new() });
|
||||
}
|
||||
|
||||
/// A component that already holds is not iterated, so its size does not keep the rest of the
|
||||
/// drawing from being solved: a Tangent on a 200-point fit spline that is already satisfied
|
||||
/// must not make re-solving an unrelated Horizontal fail as "too large".
|
||||
#[test]
|
||||
fn satisfied_large_spline_component_does_not_block_solve() {
|
||||
let mut d = Drawing::default();
|
||||
let sp = straight_fit_spline(&mut d, 200, (0.0, 0.0), (1.0, 0.0));
|
||||
let touching = line(&mut d, (199.0, 0.0), (220.0, 0.0));
|
||||
push_con(&mut d, ConstraintKind::Tangent, &[w(sp), w(touching)]);
|
||||
let other = line(&mut d, (0.0, 50.0), (10.0, 51.0));
|
||||
push_con(&mut d, ConstraintKind::Horizontal, &[w(other)]);
|
||||
let r = solve(&mut d, &SolveOptions::default());
|
||||
assert!(r.is_ok(), "{r:?}");
|
||||
let (a, b) = ln(&d, other);
|
||||
assert!((a.y - b.y).abs() < 1e-6);
|
||||
assert_eq!(ln(&d, touching), (Vec2::new(199.0, 0.0), Vec2::new(220.0, 0.0)));
|
||||
}
|
||||
|
||||
/// Fixed splines are not rebuilt while solving (their points never move, so the one rebuild is
|
||||
/// reused): two large fixed fit splines with a free line tangent to both still solve.
|
||||
#[test]
|
||||
fn fixed_large_splines_do_not_count_as_rebuilt_every_evaluation() {
|
||||
let mut d = Drawing::default();
|
||||
let a = straight_fit_spline(&mut d, 450, (0.0, 0.0), (1.0, 0.0));
|
||||
let b = straight_fit_spline(&mut d, 450, (1449.0, 0.0), (-1.0, 0.0));
|
||||
let l = line(&mut d, (449.0, 0.0), (1000.0, 0.5));
|
||||
push_con(&mut d, ConstraintKind::Fix, &[w(a)]);
|
||||
push_con(&mut d, ConstraintKind::Fix, &[w(b)]);
|
||||
push_con(&mut d, ConstraintKind::Tangent, &[w(a), w(l)]);
|
||||
push_con(&mut d, ConstraintKind::Tangent, &[w(b), w(l)]);
|
||||
let r = solve(&mut d, &SolveOptions::default());
|
||||
assert!(r.is_ok(), "{r:?}");
|
||||
let (p, q) = ln(&d, l);
|
||||
assert!(p.y.abs() < 1e-6 && q.y.abs() < 1e-6, "{p:?} {q:?}");
|
||||
}
|
||||
|
||||
/// Adding a constraint still checks redundancy within the work budget when the system already
|
||||
/// holds: an already tangent line on a free 400-point fit spline is refused, not differentiated.
|
||||
#[test]
|
||||
fn add_on_satisfied_large_spline_keeps_redundancy_budget() {
|
||||
let mut d = Drawing::default();
|
||||
let sp = straight_fit_spline(&mut d, 400, (0.0, 0.0), (1.0, 0.0));
|
||||
let l = line(&mut d, (399.0, 0.0), (420.0, 0.0));
|
||||
let r = add(&mut d, con(ConstraintKind::Tangent, &[w(sp), w(l)]), &SolveOptions::default());
|
||||
assert!(matches!(r, Err(SolveError::TooLarge)), "{r:?}");
|
||||
assert!(d.constraints.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user