Resources Numerical accuracy Calculation policy

Load reserve and escape route checks

Finds the load factor at which a design first reaches its limit, and the narrowest door on the best way out of every room.

How the precision is kept

The load factor is found by running the design's own check at trial load factors: doubling until it fails, then bisection until the bracket is within 0.01 % of the factor. The number reported is the middle of that bracket. Door widths are compared in 64-bit with small absolute tolerances.

Open item. Return the passing end of the bracket (lo) as the factor, as the comment above the loop says, and report the bracket width.

Calculations under this policy

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

study/breakit.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests tools/breakit-test/breakit.mjs

Load factor to the limit

64-bit

The factor by which every load can be multiplied before the design's utilization reaches 1.

find λ with util(λ) = 1: double λ until util ≥ 1, then bisect with mid = √(lo × hi) until hi − lo ≤ 10⁻⁴ × hi

The result is (lo + hi) ÷ 2, the middle of the final bracket, so it is known to about 0.005 % and may lie on the failing side; the comment above the loop says the passing side is reported. The loop also ends after 80 evaluations, in which case the bracket can be wider than the tolerance.

  • study/breakit.js
  • lines 27 to 54
  • CadCraft 0.8.2
/**
 * The load factor λ at which `util(λ)` reaches 1. `util` is the utilisation of the design when every load is multiplied by λ, computed by
 * the design's own solver (so nonlinear interaction formulas are honoured); it is assumed to rise with λ and to be 0 at 0. If util(1) is
 * already ≥ 1 the factor is below 1 (the design fails as loaded). If nothing fails up to `max`, `bounded` is false and `factor` is
 * Infinity (the design holds at least `atLeast`). `evals` counts the solves, so a caller can bound the cost.
 */
export async function reserve(util, { max = LIMITS.factorMax, tol = 1e-4, maxEvals = LIMITS.evals, cancelled = () => false } = {}) {
  let evals = 0;
  const f = async (l) => { evals++; const u = await util(l); if (!finite(u)) throw new Error('The check did not give a number.'); return u; };   // i18n-ignore: a programming error
  const u1 = await f(1);
  let lo, hi;
  if (u1 >= 1) { lo = 0; hi = 1; }
  else {
    lo = 1; hi = 2;
    for (;;) {
      if (cancelled() || evals >= maxEvals) return { factor: null, bounded: false, evals, cancelled: true };
      if (hi > max) return { factor: Infinity, bounded: false, atLeast: lo, evals };
      if ((await f(hi)) >= 1) break;
      lo = hi; hi *= 2;
    }
  }
  // lo holds (util < 1, or λ = 0), hi fails. Bisection on the ratio; the factor reported is the passing side, so "holds up to" is true.
  while (hi - lo > tol * hi && evals < maxEvals && !cancelled()) {
    const mid = lo > 0 ? Math.sqrt(lo * hi) : hi / 2;
    if ((await f(mid)) >= 1) hi = mid; else lo = mid;
  }
  return { factor: (lo + hi) / 2, bounded: true, evals, utilAsLoaded: u1 };
}

Escape route widths

64-bit

For each room, the route out whose narrowest door is widest, and its margin against the required width.

width of a route = min of its door widths; margin = width − required

Widths are compared, not computed, apart from the margin subtraction. A route replaces another only when it is wider by more than 1e-9, and a room fails when its width is more than 1e-6 below the rule; both tolerances are absolute, in the unit the widths are given in.

  • study/breakit.js
  • lines 56 to 101
  • CadCraft 0.8.2
/**
 * Escape routes. graph = { rooms: [{ id, label?, at? }], doors: [{ id, a, b, width, label? }] } — a door joins room `a` to room `b`;
 * `b: null` is a door to the outside. rule = { min (mm, the pack's smallest clear width), id, clause?, pack? }.
 * For each room the route out whose narrowest door is widest is found (a widest-path search); the row names that door.
 * A room with no way out is `reachable: false` and fails whatever the rule says.
 */
export function routes(graph, rule) {
  const rooms = new Map(graph.rooms.map((r) => [r.id, r]));
  const adj = new Map(graph.rooms.map((r) => [r.id, []]));
  const out = [];   // doors to the outside, with the room inside
  for (const d of graph.doors) {
    if (!rooms.has(d.a) && !(d.b !== null && rooms.has(d.b))) continue;
    if (d.b === null || d.b === undefined) { if (rooms.has(d.a)) out.push(d); continue; }
    if (rooms.has(d.a)) adj.get(d.a).push({ to: d.b, door: d });
    if (rooms.has(d.b)) adj.get(d.b).push({ to: d.a, door: d });
  }
  // widest path from the outside inwards: best[r] = the largest "narrowest door" over ways from r out
  const best = new Map(), via = new Map();
  const queue = [];
  for (const d of out) {
    if (!(best.has(d.a)) || d.width > best.get(d.a)) { best.set(d.a, d.width); via.set(d.a, { door: d, next: null, narrow: d }); queue.push(d.a); }
  }
  while (queue.length) {
    queue.sort((p, q) => best.get(q) - best.get(p));
    const r = queue.shift();
    for (const e of adj.get(r) || []) {
      const w = Math.min(best.get(r), e.door.width);
      if (!best.has(e.to) || w > best.get(e.to) + 1e-9) {
        best.set(e.to, w);
        const prev = via.get(r);
        via.set(e.to, { door: e.door, next: r, narrow: e.door.width <= prev.narrow.width ? e.door : prev.narrow });
        queue.push(e.to);
      }
    }
  }
  const min = finite(rule && rule.min) ? rule.min : 0;
  const rows = graph.rooms.map((r) => {
    if (!best.has(r.id)) return { room: r.id, label: r.label || r.id, at: r.at || null, reachable: false, bottleneck: null, door: null, margin: -Infinity, failing: true, path: [] };
    const path = []; let at = r.id;
    for (let guard = 0; guard < 200 && at !== null; guard++) { const v = via.get(at); path.push(v.door.id); at = v.next; }
    const narrow = via.get(r.id).narrow, w = best.get(r.id);
    return { room: r.id, label: r.label || r.id, at: r.at || null, reachable: true, bottleneck: w, door: narrow.id, doorLabel: narrow.label || narrow.id, margin: w - min, failing: w < min - 1e-6, path };
  });
  rows.sort((p, q) => p.margin - q.margin);
  return { rows, worst: rows[0] || null, failing: rows.filter((r) => r.failing), rule: rule || null };
}

Part reserve from a stress study

64-bit

The load factor to first yield and to buckling of a solved part.

λ = minimum safety factor ÷ wanted safety factor; utilization = 1 ÷ λ

Two 64-bit divisions on the numbers the stress study hands over; the result carries whatever precision that study's minimum safety factor has. The values shown in problem messages are rounded to 3 significant digits by a separate helper.

  • study/breakit.js
  • lines 103 to 122
  • CadCraft 0.8.2
/**
 * A solved part → findings. `result` is a static (maxVm, minSafety) or buckling (factors) result of the Simulation study; `sf` is the
 * safety factor a design wants (default 1: first yield). For a linear static study stress is proportional to load, so the load factor to
 * first yield is the minimum safety factor itself; the caller may confirm it with `reserve` on a rescaled solve.
 */
export function partFindings(result, { sf = 1, material = null } = {}) {
  const out = [];
  if (!result) return out;
  const at = (p) => (p && Array.isArray(p.at) ? { at: p.at.slice(0, 3) } : undefined);
  if (result.kind === 'static' && finite(result.minSafety)) {
    const lam = result.minSafety / sf;
    out.push({ id: 'part.yield', kind: 'yield', subject: { kind: 'part', id: 'part' }, util: lam > 0 ? 1 / lam : null, factor: lam, failing: lam < 1,
      what: { vm: result.maxVm && finite(result.maxVm.value) ? result.maxVm.value : null, yield: material && material.yield ? material.yield : (result.material && result.material.yield) || null }, where: at(result.maxVm) });
  }
  if (result.kind === 'buckling' && Array.isArray(result.factors) && finite(result.factors[0])) {
    const lam = result.factors[0];
    out.push({ id: 'part.buckling', kind: 'buckling', subject: { kind: 'part', id: 'part' }, util: lam > 0 ? 1 / lam : null, factor: lam, failing: lam < 1, what: { mode: 1 } });
  }
  return out;
}

In detail

Method
Bracketing by doubling and bisection on the geometric mean; widest-path search over the room and door graph.
Number types
64-bit numbers.
Tolerances
Bisection stops when hi − lo ≤ 1e-4 × hi, or after 80 evaluations. Search stops at a factor of 64. A route improves only by more than 1e-9; a route fails when its narrowest width is more than 1e-6 below the rule, both in the unit of the widths.
Limits
The check is assumed to rise with load. When the evaluation budget runs out the bracket may be wider than the tolerance and nothing in the result says so.

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