Resources Numerical accuracy Calculation policy

Solving an expression for one parameter

Finds the value of one parameter that makes an expression give a wanted result, the other parameters held.

How the precision is kept

The answer is found by iteration and is accepted when the expression is within 1e-9 × max(1, |target|, |start value|) of the target. It is therefore good to about 9 digits of the result, not 15. Secant steps come first, then a search outward for a sign change, then bisection.

Open item. Tighten the acceptance tolerance, or polish an accepted answer with further steps, so a solved parameter reproduces its target to 15 digits where the expression allows.

Calculations under this policy

2 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

expr.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-cadcraft-constraints.mjs, bin/test-partcraft-params.mjs

Solve for a parameter

64-bit

The value of the named parameter for which the expression equals the target.

secant: c = b − f(b) × (b − a) ÷ (f(b) − f(a)); accept when |f| ≤ 1e-9 × max(1, |target|, |x₀|)

The first value that meets the residual tolerance is returned, so the expression matches the target to about 9 digits. A linear expression is solved by the first secant step, to rounding. The secant step divides by f(b) − f(a), which the loop requires to be non-zero; near a flat spot the step can be large and the bracket search takes over.

  • expr.js
  • lines 266 to 303
  • CadCraft 0.8.2
/**
 * Solve f(x) = target for one variable: the value of `name` that makes the expression give `target`,
 * the other values held. Starts at the variable's current value; secant steps, then a bracketing
 * search outward, then bisection. → { ok, value } | { ok: false, error }.
 */
export function solveFor(ast, values, name, target, { tol = 1e-9 } = {}) {
  const base = values instanceof Map ? Object.fromEntries(values) : { ...values };
  const x0 = Number.isFinite(base[name]) ? base[name] : 1;
  const f = (x) => { base[name] = x; try { return evaluate(ast, base) - target; } catch { return NaN; } };
  const scale = Math.max(1, Math.abs(target), Math.abs(x0));
  const close = (y) => Number.isFinite(y) && Math.abs(y) <= tol * scale;
  let a = x0, fa = f(a);
  if (close(fa)) return { ok: true, value: a };
  // secant from the current value (linear expressions land in one step)
  let b = x0 === 0 ? 1e-3 : x0 * (1 + 1e-4), fb = f(b);
  for (let k = 0; k < 60 && Number.isFinite(fa) && Number.isFinite(fb) && fb !== fa; k++) {
    const c = b - fb * (b - a) / (fb - fa), fc = f(c);
    if (close(fc)) return { ok: true, value: polish(c) };
    a = b; fa = fb; b = c; fb = fc;
  }
  // bracket outward from x0, then bisect
  const g0 = f(x0);
  for (let step = Math.max(1e-6, Math.abs(x0) * 1e-3); step < 1e9; step *= 2) {
    for (const s of [x0 + step, x0 - step]) {
      const gs = f(s);
      if (close(gs)) return { ok: true, value: polish(s) };
      if (Number.isFinite(g0) && Number.isFinite(gs) && Math.sign(gs) !== Math.sign(g0)) {
        let lo = x0, hi = s, glo = g0;
        for (let k = 0; k < 200; k++) {
          const mid = (lo + hi) / 2, gm = f(mid);
          if (close(gm) || Math.abs(hi - lo) <= 1e-15 * Math.max(1, Math.abs(mid))) return { ok: true, value: polish(mid) };
          if (Math.sign(gm) === Math.sign(glo)) { lo = mid; glo = gm; } else hi = mid;
        }
      }
    }
  }
  return { ok: false, error: tr('expr.no_value_gives', { name, target: formatNumber(target) }) };
}

Rounding of the answer

64-bit

Replaces the answer by its 13-digit rounding when the two are practically equal.

r = x to 13 significant digits; use r when |r − x| ≤ 1e-12 × max(1, |x|)

This turns 39.99999999999 into 40. It can move the answer by up to 1e-12 (relative), which is inside the residual tolerance the solver already accepted.

  • expr.js
  • line 304
  • CadCraft 0.8.2
const polish = (x) => { const r = Number(x.toPrecision(13)); return Math.abs(r - x) <= 1e-12 * Math.max(1, Math.abs(x)) ? r : x; };

In detail

Method
Up to 60 secant steps from the current value; then steps doubling from max(1e-6, |x₀| × 1e-3) up to 1e9 on both sides looking for a sign change; then up to 200 bisection steps.
Number types
64-bit numbers.
Tolerances
Residual |f(x) − target| ≤ 1e-9 × max(1, |target|, |x₀|). Bisection also stops when the bracket is narrower than 1e-15 × max(1, |mid|). The answer is rounded to 13 significant digits when that changes it by no more than 1e-12 (relative).
Limits
The residual tolerance sets the accuracy. A root that the outward search does not bracket is reported as not found.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach