Initial commit with Dockerfile and demo code
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user