Resources Numerical accuracy Calculation policy

Material flow simulation

Runs a discrete-event simulation of conveyors, stations, buffers, gates and automated vehicles and reports throughput, lead times, queue lengths and utilization.

How the precision is kept

Times and positions are 64-bit numbers in seconds and meters. Events are ordered by time, then priority, then a sequence number, so a run with the same seed repeats exactly. The results are statistics of one random run: they depend on the seed, the run length and the warm-up, and every reported statistic is rounded to 6 decimals before it is returned.

Open item. Return the statistics unrounded and round to 6 decimals only where they are shown.

Calculations under this policy

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

addons/flow/lib/sim.js ModelCraft 0.8.1 64-bit 18 calculations Reviewed

Tests None found by the review

Event order

64-bit

Decides which of two queued events runs first.

earlier time first; equal times: lower priority number first; then lower sequence number

Times are compared exactly, with no tolerance. Two events computed to the same moment by different formulas can differ in the last bit and then run in time order instead of priority order.

  • addons/flow/lib/sim.js
  • line 52
  • ModelCraft 0.8.1
less(x, y) { return x.t < y.t || (x.t === y.t && (x.p < y.p || (x.p === y.p && x.s < y.s))); }

Time-weighted mean and maximum

64-bit

The mean and maximum over time of a queue length, a count of busy servers or an on/off state.

area = Σ value × (time it held); mean = (area + value × (now − last change)) ÷ (now − start)

The area is a running sum in 64-bit, added in event order. The values are small whole numbers and the time steps are differences of clock times, so each term is a product of an integer and a difference.

  • addons/flow/lib/sim.js
  • lines 78 to 84
  • ModelCraft 0.8.1
class TW {
  constructor(sim) { this.sim = sim; this.v = 0; this.t = sim.now; this.t0 = sim.now; this.area = 0; this.max = 0; sim.tws.push(this); }
  set(v) { const n = this.sim.now; this.area += this.v * (n - this.t); this.t = n; this.v = v; if (v > this.max) this.max = v; }
  add(d) { this.set(this.v + d); }
  reset() { this.set(this.v); this.area = 0; this.t0 = this.sim.now; this.max = this.v; }
  mean() { const n = this.sim.now, span = n - this.t0; return span > 0 ? (this.area + this.v * (n - this.t)) / span : this.v; }
}

Rounding of reported statistics

64-bit

Rounds a statistic to 6 decimals before it goes into the results.

r6(x) = round(x × 10⁶) ÷ 10⁶

Every mean, fraction, rate, distance and lead time in the results passes through this. The rounding is absolute (1e-6 of a second, a meter, a fraction or a load per hour), so the returned value keeps 6 decimals, not 15 significant digits.

  • addons/flow/lib/sim.js
  • line 86
  • ModelCraft 0.8.1
const r6 = (x) => (Number.isFinite(x) ? Math.round(x * 1e6) / 1e6 : x);

Load type by weight

64-bit

Picks the type of a new load in proportion to the types' weights.

u = random × Σ weights; subtract each weight in turn until u < 0

The weights are subtracted from u one by one in 64-bit. If rounding leaves u at or above 0 after the last weight, the last type is returned.

  • addons/flow/lib/sim.js
  • lines 136 to 141
  • ModelCraft 0.8.1
pickType() {
  if (this.types.length === 1) return this.types[0];
  let u = this.rt.next() * this.wsum;
  for (const x of this.types) { if ((u -= x.weight) < 0) return x; }
  return this.types[this.types.length - 1];
}

Throughput and lead time at a sink

64-bit

Counts finished loads and works out loads per hour and the mean and maximum time from creation to the sink.

lead = now − born; per hour = n ÷ span × 3600; mean lead = Σ lead ÷ n

Lead times are added in arrival order into one 64-bit sum. The three reported values are rounded to 6 decimals by r6.

  • addons/flow/lib/sim.js
  • lines 162 to 176
  • ModelCraft 0.8.1
class Sink extends El {
  init() { this.n = 0; this.lead = 0; this.leadMax = 0; this.byType = {}; }
  reset() { super.reset(); this.init(); }
  canAccept() { return true; }
  accept(load) {
    const lt = this.sim.now - load.born;
    this.n++; this.lead += lt; if (lt > this.leadMax) this.leadMax = lt;
    this.byType[load.type] = (this.byType[load.type] || 0) + 1;
    this.sim.finish(load, lt);
  }
  stats() {
    const span = this.sim.now - this.sim.statsFrom;
    return { type: this.type, done: this.n, perHour: r6(span > 0 ? (this.n / span) * 3600 : 0), leadMean: r6(this.n ? this.lead / this.n : 0), leadMax: r6(this.leadMax), byType: { ...this.byType } };
  }
}

Station utilization, blocking and waiting

64-bit

The share of server time spent busy, blocked and idle, the mean queue and the mean wait at a station.

utilization = mean busy ÷ servers; blocked = mean blocked ÷ servers; idle = 1 − (mean busy + mean blocked) ÷ servers; wait = Σ wait ÷ starts

Each value is a time-weighted mean divided by the number of servers, then rounded to 6 decimals. Idle is 1 minus the other two before rounding, so the three rounded fractions can differ from 1 by up to 1.5e-6.

  • addons/flow/lib/sim.js
  • lines 279 to 286
  • ModelCraft 0.8.1
    utilisation: r6(this.busy.mean() / this.c), blocked: r6(this.blk.mean() / this.c), idle: r6(1 - (this.busy.mean() + this.blk.mean()) / this.c),
    queueMean: r6(this.q.mean()), queueMax: this.q.max, waitMean: r6(this.wn ? this.wsum / this.wn : 0),
  };
  if (this.downTW) { out.availability = r6(1 - this.downTW.mean()); out.failures = this.failures; }
  if (this.pool) out.pool = this.pool.id;
  if (this.openTW) out.calendarOpen = r6(this.openTW.mean());
  return out;
}

Gate cycle phase

64-bit

Whether a cycling gate starts open and when it first switches.

period = open + closed; phase = ((0 − offset) mod period + period) mod period; open when phase < open

Two 64-bit remainders. Later switches are scheduled by adding the open or closed time to the current time, so the switch times are a running sum.

  • addons/flow/lib/sim.js
  • lines 309 to 312
  • ModelCraft 0.8.1
const per = s.open + s.closed, off = s.offset || 0;
const ph = (((0 - off) % per) + per) % per;
this.isOpen = ph < s.open;
this.sim.at(this.isOpen ? s.open - ph : per - ph, 0, () => this.toggle());

Position of a load on a belt

64-bit

Where the tail of a load is now, on an accumulating belt or on a belt that stops as a whole.

accumulating: p = min(v × t + c, S); stopping belt: p = D(t) − D at entry, D(t) = D₀ + v × (t − t₀)

On an accumulating belt c is about −v × (entry time), so v × t + c subtracts two numbers that both grow with the clock. The absolute rounding error of the position therefore grows with the simulated time; comparisons allow EPS = 1e-7.

  • addons/flow/lib/sim.js
  • lines 499 to 500
  • ModelCraft 0.8.1
/** Tail position of entry e now. */
pos(e) { return this.acc ? (e.frozen !== undefined ? e.frozen : Math.min(this.v * this.sim.now + e.c, e.S)) : this.D() - e.De; }

When the next load can get on a belt

64-bit

The time at which the last load has moved far enough for the next one to enter.

pitch = load length + gap (or the fixed zone pitch); accumulating: t = (pitch − c_last) ÷ v; stopping belt: t = now + (pitch − gap so far) ÷ v

A full belt is detected with S < pitch − 1e-7 and an open gap with gap ≥ pitch − 1e-7, both absolute in meters. The division by the belt speed is not guarded here; checkGraph refuses a speed that is not above 0 before a run starts.

  • addons/flow/lib/sim.js
  • lines 501 to 514
  • ModelCraft 0.8.1
entryTime(load) {
  if (this.broken) return Infinity;   // nothing gets on a belt that is down (its repair wakes the waiting ones)
  const need = this.pitch(load), now = this.sim.now, v = this.v;
  if (this.acc) {
    if (!this.loads.length) return this.prev ? this.prev.x + (need - (this.len - this.prev.L)) / v : -Infinity;
    const last = this.loads[this.loads.length - 1];
    if (last.S < need - EPS) return Infinity;
    return (need - last.c) / v;
  }
  if (!this.loads.length) return -Infinity;
  const gapD = this.D() - this.loads[this.loads.length - 1].De;
  if (gapD >= need - EPS) return -Infinity;
  return this.running ? now + (need - gapD) / v : Infinity;
}

A load gets on a belt

64-bit

Sets the two numbers that describe the new load's motion: where it is held back and where it stops.

empty belt: c = −v × now (or less, to keep the pitch behind the load that just left), S = length − L; else c = min(−v × now, c_last − pitch), S = S_last − pitch

S is a chain of subtractions from the belt length, one pitch per load, so its rounding error grows with the number of loads on the belt. All values are 64-bit.

  • addons/flow/lib/sim.js
  • lines 525 to 547
  • ModelCraft 0.8.1
  const sim = this.sim, now = sim.now, v = this.v;
  sim.enter(load, this);
  this.on.add(1);
  let e;
  if (this.acc) {
    const need = this.pitch(load);
    let c, S;
    if (!this.loads.length) {
      c = -v * now; if (this.prev) c = Math.min(c, this.len - this.prev.L - v * this.prev.x - need);
      S = this.len - load.L;
    } else {
      const last = this.loads[this.loads.length - 1];
      c = Math.min(-v * now, last.c - need); S = last.S - need;
    }
    e = { load, e: now, c, S, arrived: false };
    if (load.rec) load.rec.k = [[now, c, S]];
  } else {
    e = { load, De: this.D(), arrived: false };
    if (load.rec) load.rec.De = e.De;
  }
  this.loads.push(e);
  if (this.loads.length === 1) this.scheduleArrival();
}

When the head load reaches the end

64-bit

The time the first load's head arrives at the end of the belt.

accumulating: t = (length − L − c) ÷ v; stopping belt: t = t₀ + (D at entry + length − L − D₀) ÷ v

One 64-bit expression each. A result earlier than the current time is replaced by the current time.

  • addons/flow/lib/sim.js
  • lines 548 to 557
  • ModelCraft 0.8.1
scheduleArrival() {
  const h = this.loads[0];
  const tok = ++this.tok;
  if (!h || this.broken) return;
  let t;
  if (this.acc) t = (this.len - h.load.L - h.c) / this.v;
  else { if (!this.running) return; t = this.t0 + (h.De + this.len - h.load.L - this.D0) / this.v; }
  if (t < this.sim.now) t = this.sim.now;
  this.sim.at(t, 0, () => { if (tok !== this.tok) return; h.arrived = true; this.retry(); });
}

Loads close up when the head leaves

64-bit

Works out again, for every load left on the belt, where it is held back and where it stops.

first: c = min(−v × entry, length − L_gone − v × now − pitch), S = length − L; others: c = min(−v × entry, c_front − pitch), S = S_front − pitch

Recomputed from the belt length for the whole belt each time, so errors do not carry over from one release to the next. A change is recorded only when c or S differs exactly from the old value.

  • addons/flow/lib/sim.js
  • lines 565 to 581
  • ModelCraft 0.8.1
release(load) {
  const now = this.sim.now, v = this.v;
  this.loads.shift(); this.on.add(-1); this.hw.set(0); this.headBlocked = false;
  for (const sn of this.sensors) sn.count++;
  if (this.acc) {
    this.prev = { x: now, L: load.L };
    for (let i = 0; i < this.loads.length; i++) {
      const e = this.loads[i], need = this.pitch(e.load);
      let c, S;
      if (i === 0) { c = Math.min(-v * e.e, this.len - load.L - v * now - need); S = this.len - e.load.L; }
      else { const f = this.loads[i - 1]; c = Math.min(-v * e.e, f.c - need); S = f.S - need; }
      if (c !== e.c || S !== e.S) { e.c = c; e.S = S; if (e.load.rec && e.load.rec.k) e.load.rec.k.push([now, c, S]); }
    }
  } else if (!this.running) { this.running = true; this.t0 = now; this.belt(); }
  this.scheduleArrival();
  this.notify();
}

A belt starts again after a breakdown

64-bit

Shifts every load's timeline by the time the belt stood still.

dur = now − time it stopped; c = c − v × dur; entry = entry + dur

The stop length is a difference of two clock times. Each breakdown adds one more subtraction to c, so a load that sits through many breakdowns carries their summed rounding.

  • addons/flow/lib/sim.js
  • lines 484 to 489
  • ModelCraft 0.8.1
const sim = this.sim, now = sim.now, dur = now - this.tDown;
this.broken = false; this.downTW.set(0);
if (this.acc) {
  // the clock went on while the belt stood still: every load's timeline moves on by the time it stood
  for (const e of this.loads) { e.c -= this.v * dur; e.e += dur; delete e.frozen; if (e.load.rec && e.load.rec.k) e.load.rec.k.push([now, e.c, e.S]); }
  if (this.prev) this.prev.x += dur;

Photo-eye covered by a load

64-bit

Whether a load is over a sensor placed a given distance along a belt.

covered when p − 1e-7 ≤ at ≤ p + L + 1e-7

The load's tail position comes from the belt's position formula. The 1e-7 margin is absolute, in meters, on both ends of the load.

  • addons/flow/lib/sim.js
  • lines 597 to 601
  • ModelCraft 0.8.1
covered() {
  const c = this.conv, at = this.spec.at;
  if (!c) return false;
  return c.loads.some((e) => { const p = c.pos(e); return p - EPS <= at && at <= p + e.load.L + EPS; });
}

Shortest vehicle routes

64-bit

The shortest path distance from one path node to every other, used to pick the nearest idle vehicle and its route.

Dijkstra: dist(to) = min over edges of dist(from) + edge length

Distances are sums of edge lengths in 64-bit. A new distance replaces the old one only if it is shorter by more than 1e-7 m, so ties go to the edge listed first. The next node is found by a linear scan of all nodes.

  • addons/flow/lib/sim.js
  • lines 710 to 728
  • ModelCraft 0.8.1
/** Shortest paths from a node (Dijkstra, ties to the edge listed first) → {dist: Map, via: Map node → edge}. */
from(n) {
  let p = this.paths.get(n);
  if (p) return p;
  const dist = new Map([[n, 0]]), via = new Map(), done = new Set();
  for (;;) {
    let best = null, bd = Infinity;
    for (const [k, d] of dist) if (!done.has(k) && d < bd) { bd = d; best = k; }
    if (best === null) break;
    done.add(best);
    for (const e of this.out.get(best) || []) {
      const nd = bd + e.length;
      if (nd < (dist.get(e.to) ?? Infinity) - EPS) { dist.set(e.to, nd); via.set(e.to, e); }
    }
  }
  p = { dist, via };
  this.paths.set(n, p);
  return p;
}

Vehicle travel time on a segment

64-bit

When a vehicle that enters a path segment reaches its end.

t₁ = now + length ÷ speed

One division and one addition in 64-bit. The speed falls back to 1 when the fleet gives none or 0.

  • addons/flow/lib/sim.js
  • line 803
  • ModelCraft 0.8.1
const t1 = now + e.length / this.speed;

Vehicle time shares and distances

64-bit

For each vehicle: the share of time in each state, the distance driven loaded and empty, and how busy it was.

share = time in state ÷ span; busy = 1 − idle share − down share

Times in each state are running sums of clock differences. Each share is rounded to 6 decimals first, and busy is then computed from the rounded shares and rounded again.

  • addons/flow/lib/sim.js
  • lines 903 to 909
  • ModelCraft 0.8.1
  const now = this.sim.now, span = now - this.sim.statsFrom;
  return this.vehicles.map((v) => {
    const t = { ...v.t }; t[v.st] += now - v.since;
    const frac = Object.fromEntries(ST.map((s) => [s, r6(span > 0 ? t[s] / span : 0)]));
    return { vehicle: v.i + 1, delivered: v.delivered, distLoaded: r6(v.distLoaded), distEmpty: r6(v.distEmpty), time: frac, busy: r6(1 - frac.idle - frac.down), ...(this.mtbf ? { availability: r6(1 - frac.down), failures: v.failures } : {}) };
  });
}

Run results

64-bit

Throughput per hour and the lead time count, mean, minimum, median, 95th percentile and maximum for the whole run.

per hour = done ÷ span × 3600; mean = Σ lead ÷ n; percentile p = sorted[min(n − 1, floor(p × n))]

Lead times are copied to a Float64Array and sorted numerically; the mean adds them smallest first. Percentiles take one sorted value with no interpolation. Every value is rounded to 6 decimals by r6 before it is returned.

  • addons/flow/lib/sim.js
  • lines 1107 to 1125
  • ModelCraft 0.8.1
results() {
  const ls = Float64Array.from(this.leads).sort();
  const q = (p) => (ls.length ? ls[Math.min(ls.length - 1, Math.floor(p * ls.length))] : 0);
  const sum = ls.reduce((a, x) => a + x, 0);
  const span = this.now - this.statsFrom;
  const elements = {};
  for (const [id, el] of this.els) elements[id] = el.stats();
  const byType = {};
  for (const [k, b] of Object.entries(this.byType)) byType[k] = { done: b.done, leadMean: r6(b.done ? b.lead / b.done : 0) };
  if (this.trace && this.trace.full) this.warnings.push({ code: 'trace-full' });
  return {
    ok: !this.failed, failed: this.failed, version: VERSION, seed: this.seed, until: this.until, warmup: this.warmup, end: r6(this.now),
    events: this.events, created: this.created, done: this.done, wip: this.alive,
    perHour: r6(span > 0 ? (this.done / span) * 3600 : 0),
    lead: { n: ls.length, mean: r6(ls.length ? sum / ls.length : 0), min: r6(ls.length ? ls[0] : 0), p50: r6(q(0.5)), p95: r6(q(0.95)), max: r6(ls.length ? ls[ls.length - 1] : 0) },
    byType, elements, pools: [...this.pools.values()].map((p) => p.stats()), vehicles: this.agv ? this.agv.stats() : [], warnings: this.warnings,
    trace: this.trace,
  };
}

In detail

Method
Event queue as a binary heap. Loads on an accumulating belt move as p(t) = min(v × t + c, S); a belt that stops keeps one belt distance D(t). Time-weighted means are area ÷ span. Vehicle routes are shortest paths (Dijkstra).
Number types
64-bit numbers; lead times are sorted in a Float64Array.
Tolerances
EPS = 1e-7, absolute, in seconds or meters depending on the comparison: belt spacing, entry times, sensor cover, path ties and the load-too-long check.
Limits
One run is one sample of a random process. Percentiles take the sorted value at index floor(p × n) without interpolation. At most 2,000,000 lead times are kept for the percentiles. Reported statistics are rounded to 1e-6.

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