Resources Numerical accuracy Calculation policy

Parameters and equations

Evaluates named parameters and the equations that drive dimensions, writes the results into the drawing or part, and solves backward for a parameter when a driven value is edited.

How the precision is kept

Forward evaluation is 64-bit: parameters in dependency order, then each equation, written to its target when it differs by more than 1e-12 relative. Backward solving is numeric, stopping at a residual of 1e-10 relative, and the solution is then rounded to the roundest number that still meets that tolerance. Values written back into a parameter are stored as text with 12 significant digits (expr.js formatNumber).

Open item. Store parameter values with 17 significant digits instead of 12 and tighten the back-solve residual from 1e-10, so a driven value typed by a person is reproduced to 15 digits.

Calculations under this policy

9 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.

params.js CadCraft 0.8.2 64-bit 9 calculations Reviewed

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

Parameter values in dependency order

64-bit

Evaluates every parameter after the ones it uses and reports cycles and missing values.

Each expression is evaluated once by expr.js in 64-bit, in depth-first dependency order, so a value is never taken from a stale copy. An override replaces a parameter's value as given, with no rounding.

  • params.js
  • lines 84 to 123
  • CadCraft 0.8.2
/**
 * Evaluate every variable. → { values: Map, errors: Map(name → message), order: [names], uses: Map(name → [names]) }.
 * `overrides` (name → number) replace those variables' values (design tables, solving backward).
 */
export function evaluateVars(p, overrides = null) {
  const names = new Set(p.vars.map((v) => v.name));
  const parsed = new Map(), uses = new Map(), errors = new Map();
  for (const v of p.vars) {
    if (overrides && overrides.has(v.name)) { uses.set(v.name, []); continue; }
    const r = parse(v.expr, [...names]);
    if (!r.ok) { errors.set(v.name, r.error); uses.set(v.name, []); continue; }
    parsed.set(v.name, r.ast); uses.set(v.name, r.uses);
  }
  // dependency order (DFS); a cycle marks every variable on it
  const order = [], state = new Map(), stack = [];
  const visit = (n) => {
    if (state.get(n) === 2) return;
    if (state.get(n) === 1) {
      const loop = stack.slice(stack.indexOf(n));
      for (const m of loop) errors.set(m, `${loop.join(', ')} ${loop.length === 1 ? tr('param.uses_itself') : tr('param.use_each_other')}.`);
      return;
    }
    state.set(n, 1); stack.push(n);
    for (const d of uses.get(n) || []) visit(d);
    stack.pop(); state.set(n, 2); order.push(n);
  };
  for (const v of p.vars) visit(v.name);
  const values = new Map();
  for (const n of order) {
    if (overrides && overrides.has(n)) { values.set(n, overrides.get(n)); continue; }
    if (errors.has(n)) continue;
    const missing = (uses.get(n) || []).filter((d) => !values.has(d));
    if (missing.length) { errors.set(n, tr('param.uses_which_no_value', { v: missing.join(', '), n: missing.length })); continue; }
    try { values.set(n, evaluate(parsed.get(n), values)); } catch (e) {
      if (!(e instanceof ExprError)) throw e;
      errors.set(n, e.message);
    }
  }
  return { values, errors, order, uses };
}

Equation results written to their targets

64-bit

Evaluates each equation and writes the result into the dimension or feature value it drives.

written when |current − v| > 1e-12 × max(1, |v|)

The result is written as the 64-bit number the expression gave; a count target is rounded to a whole number first. A value within 1e-12 × max(1, |v|) of the current one is left alone, so a change smaller than that is not written.

  • params.js
  • lines 151 to 156
  • CadCraft 0.8.2
try { v = evaluate(r.ast, ev.values); } catch (e) { if (!(e instanceof ExprError)) throw e; errors.push({ where: l.target, message: e.message }); continue; }
if (t.integer || t.kind === 'count') v = Math.round(v);
const vp = valueProblem(t, v);
if (vp) { errors.push({ where: l.target, message: vp }); continue; }
const cur = t.get();
if (!(Math.abs(cur - v) <= 1e-12 * Math.max(1, Math.abs(v)))) { t.set(v); changed.push(l.target); }

Limits on a driven value

64-bit

Checks a value against its target's kind: whole number, minimum, maximum.

A whole-number target accepts a value within 1e-9 of an integer; minimum and maximum allow 1e-12. All three tolerances are absolute, not scaled to the value.

  • params.js
  • lines 125 to 132
  • CadCraft 0.8.2
/** Check a value against a target's kind/limits; → message or null. */
export function valueProblem(t, v) {
  if (!Number.isFinite(v)) return tr('param.not_number');
  if (t.integer || t.kind === 'count') { if (Math.abs(v - Math.round(v)) > 1e-9) return tr('param.has_whole_number_came', { label: t.label, v: formatNumber(v) }); }
  if (t.min !== undefined && v < t.min - 1e-12) return tr('param.has_least_came_out', { label: t.label, min: formatNumber(t.min), v: formatNumber(v) });
  if (t.max !== undefined && v > t.max + 1e-12) return tr('param.can_most_came_out', { label: t.label, max: formatNumber(t.max), v: formatNumber(v) });
  return null;
}

Backward solve for a driving parameter

64-bit

Finds the parameter value that makes an equation give the value a person typed.

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

Stops when the residual is within 1e-10 × the larger of 1, the target and the start value, so the equation is met to a relative 1e-10 at best, not to 15 digits. Secant runs at most 80 steps, then steps outward doubling from max(1e-6, |x₀| × 1e-3) up to 1e9 and bisects at most 200 times, down to an interval of 1e-15 relative.

  • params.js
  • lines 222 to 251
  • CadCraft 0.8.2
/** Root-find chain(x) = value from x0 (secant, then bracket outward + bisect: expr.js solveFor's strategy on a function). */
function solveChain(chain, x0, value) {
  const f = (x) => { try { return chain(x) - value; } catch { return NaN; } };
  const scale = Math.max(1, Math.abs(value), Math.abs(x0));
  const close = (y) => Number.isFinite(y) && Math.abs(y) <= 1e-10 * scale;
  let a = x0, fa = f(a);
  if (close(fa)) return { ok: true, value: a };
  let b = x0 === 0 ? 1e-3 : x0 * (1 + 1e-4), fb = f(b);
  for (let k = 0; k < 80 && 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: nice(c, f, close) };
    a = b; fa = fb; b = c; fb = fc;
  }
  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: nice(s, f, close) };
      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: nice(mid, f, close) };
          if (Math.sign(gm) === Math.sign(glo)) { lo = mid; glo = gm; } else hi = mid;
        }
      }
    }
  }
  return { ok: false };
}

Rounding of the solved value

64-bit

Replaces the solved value by the roundest number near it that still solves the equation.

first of round(x, d decimals) for d = 0…10, then x to 12 significant digits, that keeps |f| within tolerance; else x

The value kept is rounded: to the fewest decimals (0 to 10) for which the residual stays within the 1e-10 tolerance, else to 12 significant digits. The rounding uses x × 10^d and a division by 10^d.

  • params.js
  • lines 252 to 256
  • CadCraft 0.8.2
/** The roundest number near x that still solves it (fewest decimals, then 12 significant digits). */
function nice(x, f, close) {
  for (let d = 0; d <= 10; d++) { const r = Math.round(x * 10 ** d) / 10 ** d; if (close(f(r))) return r; }
  const r = Number(x.toPrecision(12)); return close(f(r)) ? r : x;
}

Edited dimension written back to a parameter

64-bit

When a driven value is edited, changes the driving parameter behind its equation, or rewrites a constant equation to the new number.

The new parameter value, or the new constant, is stored as text through formatNumber, which keeps 12 significant digits (expr.js: toPrecision(12)). A value typed with more digits is shortened when it is stored.

  • params.js
  • lines 258 to 276
  • CadCraft 0.8.2
/**
 * Someone set the document value `key` to `value`. Unlinked → { ok, direct: true } (the caller writes it).
 * Linked → the driving variable behind the equation is changed in `p` (mutated): { ok, var, from, to }.
 * A link to a constant expression is rewritten to the new number: { ok, link: key, from, to }.
 */
export function driveBack(p, adapter, doc, key, value, { prefer = null } = {}) {
  const l = linkFor(p, key);
  if (!l) return { ok: true, direct: true };
  const t = adapter.targets(doc).get(key);
  const label = t ? t.label : key;
  if (t) { const vp = valueProblem(t, value); if (vp) return { ok: false, error: vp }; }
  if (literal(l.expr) !== null || !parse(l.expr).ok || !parse(l.expr).uses.length) {
    const from = l.expr; l.expr = formatNumber(value); return { ok: true, link: key, from, to: l.expr };
  }
  const r = solveBack(p, l.expr, value, prefer, label);
  if (!r.ok) return r;
  varByName(p, r.var).expr = formatNumber(r.to);
  return r;
}

Value typed for a parameter

64-bit

Sets a driving parameter to a typed value, or solves the driving parameter behind a driven one.

Both paths store the result as text through formatNumber, 12 significant digits.

  • params.js
  • lines 278 to 287
  • CadCraft 0.8.2
/** Someone typed a value for variable `name`. Driving → set. Driven → solve its driving variable(s). */
export function driveVar(p, name, value, { prefer = null } = {}) {
  const v = varByName(p, name);
  if (!v) return { ok: false, error: tr('param.there_no_parameter_called', { name }) };
  if (isDriving(v)) { const from = v.expr; v.expr = formatNumber(value); return { ok: true, var: name, from, to: v.expr }; }
  const r = solveBack(p, v.expr, value, prefer, name);
  if (!r.ok) return r;
  varByName(p, r.var).expr = formatNumber(r.to);
  return r;
}

Removing a parameter and writing its value in

64-bit

Removes a parameter and, when asked, writes its current value into every expression that used it.

The value is written into the other expressions as text through formatNumber, 12 significant digits, in parentheses. Results that depended on the parameter can change in the digits after the twelfth.

  • params.js
  • lines 326 to 341
  • CadCraft 0.8.2
/** Remove a variable; refused while anything uses it unless `inline` (then its current value is written in). */
export function removeVar(p, name, { inline = false } = {}) {
  const v = varByName(p, name);
  if (!v) return { ok: false, error: tr('param.there_no_parameter_called', { name }) };
  const u = usesOf(p, name);
  if ((u.vars.length || u.links.length) && !inline) return { ok: false, error: tr('param.is_used_by', { name, v: [...u.vars, ...u.links].join(', ') }), uses: u };
  if (inline) {
    const val = evaluateVars(p).values.get(name);
    if (val === undefined) return { ok: false, error: tr('param.has_no_value_write', { name }) };
    const lit = `(${formatNumber(val)})`;
    for (const w of p.vars) if (w !== v) w.expr = renameIn(w.expr, name, lit);
    for (const l of p.links) l.expr = renameIn(l.expr, name, lit);
  }
  p.vars.splice(p.vars.indexOf(v), 1);
  return { ok: true };
}

Design table row made permanent

64-bit

Writes a set of override values into the parameters.

Each value is stored as text through formatNumber, 12 significant digits.

  • params.js
  • lines 413 to 416
  • CadCraft 0.8.2
/** Overrides as variable edits (a design-table row made permanent, or CLI --set name=value). */
export function applyOverrides(p, overrides) {
  for (const [n, v] of overrides) { const w = varByName(p, n); if (!w) throw new Error(tr('param.there_no_parameter_called_3', { n })); w.expr = formatNumber(v); }
}

In detail

Method
Forward: depth-first dependency order, cycles reported. Backward: secant steps from the current value (up to 80), then bracketing outward by doubling steps up to 1e9 and bisection (up to 200 halvings).
Number types
64-bit numbers; parameter values are kept as expression text.
Tolerances
Back-solve residual: 1e-10 × max(1, |target|, |start|). Bisection interval: 1e-15 × max(1, |mid|). Write threshold: 1e-12 × max(1, |value|). Whole-number check: 1e-9 (absolute). Minimum and maximum checks: 1e-12 (absolute).
Limits
A solved or typed parameter value is stored with at most 12 significant digits, and a back-solved value meets its equation to a relative 1e-10, not to 15 digits.

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