122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
/**
|
|
* OEE and line KPI rollups.
|
|
*
|
|
* Everything is computed over a rolling window of simulated time so the numbers
|
|
* actually move during a demo. A cumulative-since-reset OEE barely budges in
|
|
* twenty minutes, which reads as a broken dashboard.
|
|
*
|
|
* OEE = Availability x Performance x Quality, using the standard definitions:
|
|
* Availability = run time / planned production time
|
|
* Performance = (total parts x ideal cycle time) / run time
|
|
* Quality = good parts / total parts
|
|
*/
|
|
|
|
/** Rolling window length, in simulated seconds. */
|
|
export const KPI_WINDOW_SEC = 1200;
|
|
|
|
export class KpiTracker {
|
|
constructor(idealCycleTime) {
|
|
this.idealCycleTime = idealCycleTime;
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
/** Ring of cumulative counters, so any window is a difference of two samples. */
|
|
this.samples = [];
|
|
this.cum = { plannedSec: 0, runSec: 0, total: 0, good: 0, rejected: 0 };
|
|
}
|
|
|
|
/**
|
|
* Accumulate one simulation sub-step.
|
|
*
|
|
* lineUp: false while any station is in a fault state (planned time still
|
|
* accrues, run time does not - that is what Availability measures).
|
|
*/
|
|
accumulate(dt, lineUp, producedTotal, producedGood, producedRejected) {
|
|
this.cum.plannedSec += dt;
|
|
if (lineUp) this.cum.runSec += dt;
|
|
this.cum.total = producedTotal;
|
|
this.cum.good = producedGood;
|
|
this.cum.rejected = producedRejected;
|
|
}
|
|
|
|
/** Record a window sample. Call once per broadcast tick, not per sub-step. */
|
|
mark(simTime) {
|
|
this.samples.push({ t: simTime, ...this.cum });
|
|
while (this.samples.length > 2 && simTime - this.samples[0].t > KPI_WINDOW_SEC) {
|
|
this.samples.shift();
|
|
}
|
|
}
|
|
|
|
/** Compute KPIs over the rolling window. */
|
|
compute(stations, instantPowerKw) {
|
|
const first = this.samples[0];
|
|
const last = this.samples[this.samples.length - 1];
|
|
|
|
if (!first || !last || last.t - first.t < 1) {
|
|
return {
|
|
oee: 0, availability: 0, performance: 0, quality: 0,
|
|
throughputPerHour: 0, scrapRate: 0, energyKw: instantPowerKw,
|
|
energyPerUnit: 0, windowSec: 0,
|
|
produced: this.cum.total, good: this.cum.good, rejected: this.cum.rejected,
|
|
};
|
|
}
|
|
|
|
const dPlanned = last.plannedSec - first.plannedSec;
|
|
const dRun = last.runSec - first.runSec;
|
|
const dTotal = last.total - first.total;
|
|
const dGood = last.good - first.good;
|
|
const dRejected = last.rejected - first.rejected;
|
|
|
|
const availability = dPlanned > 0 ? dRun / dPlanned : 0;
|
|
|
|
// Performance is capped at 100% by definition: the ideal cycle time is the
|
|
// fastest the line can physically go, so exceeding it is impossible. Draining
|
|
// a WIP buffer can briefly produce faster than the bottleneck, which would
|
|
// otherwise show as OEE above 100% and read as a broken dashboard.
|
|
const performance = dRun > 0 ? Math.min(1, (dTotal * this.idealCycleTime) / dRun) : 0;
|
|
const quality = dTotal > 0 ? dGood / dTotal : dRun > 0 ? 1 : 0;
|
|
const oee = availability * performance * quality;
|
|
|
|
const hours = dPlanned / 3600;
|
|
const throughputPerHour = hours > 0 ? dGood / hours : 0;
|
|
|
|
return {
|
|
oee,
|
|
availability,
|
|
performance,
|
|
quality,
|
|
throughputPerHour,
|
|
scrapRate: dTotal > 0 ? dRejected / dTotal : 0,
|
|
energyKw: instantPowerKw,
|
|
energyPerUnit: throughputPerHour > 0 ? instantPowerKw / throughputPerHour : 0,
|
|
windowSec: dPlanned,
|
|
produced: this.cum.total,
|
|
good: this.cum.good,
|
|
rejected: this.cum.rejected,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Instantaneous line power draw, kW. */
|
|
export function instantPower(stations) {
|
|
let kw = 0;
|
|
for (const st of stations) {
|
|
switch (st.kind) {
|
|
case 'conveyor':
|
|
kw += st.signals.motorAmps * 0.62;
|
|
break;
|
|
case 'cnc':
|
|
kw += (st.signals.spindleLoad / 100) * st.power;
|
|
break;
|
|
case 'oven':
|
|
// An oven keeps drawing standby heat even when the line is stopped.
|
|
kw += (st.signals.burnerDuty / 100) * st.power + 6;
|
|
break;
|
|
default:
|
|
kw += st.state === 'running' ? st.power : st.power * 0.2;
|
|
}
|
|
}
|
|
return kw;
|
|
}
|