ademarteau commited on
Commit
e7f1f53
Β·
1 Parent(s): b2065cc

metrics: profit first, then service level, then fill rate (React UI)

Browse files
Files changed (1) hide show
  1. frontend/src/App.jsx +29 -12
frontend/src/App.jsx CHANGED
@@ -22,6 +22,10 @@ const CFG = {
22
  SIM_DAYS: 730,
23
  DECISION_INTERVAL: 5,
24
  MEMORY_SIZE: 200,
 
 
 
 
25
  };
26
 
27
  // ─── MATH HELPERS ─────────────────────────────────────────────────────────────
@@ -124,7 +128,7 @@ function runOneSimulation(computeROP, demandSeries, envKey) {
124
  const n = demandSeries.length;
125
  let inventory = 0;
126
  const orders = [];
127
- let totDemand = 0, totFulfilled = 0, totWriteOff = 0, stockOuts = 0, lostSales = 0;
128
  const timeline = [];
129
 
130
  for (let day = 0; day < n; day++) {
@@ -138,7 +142,7 @@ function runOneSimulation(computeROP, demandSeries, envKey) {
138
  const fulfilled = Math.min(demand, inventory);
139
  inventory = Math.max(0, inventory - demand);
140
  const lost = Math.max(0, demand - fulfilled);
141
- if (lost > 0) stockOuts++;
142
  lostSales += lost;
143
  let rop = 0, ordered = 0;
144
  if (hist.length >= 5 && day < n - CFG.LEAD_TIME) {
@@ -157,12 +161,19 @@ function runOneSimulation(computeROP, demandSeries, envKey) {
157
  }
158
  totDemand += demand;
159
  totFulfilled += fulfilled;
 
 
 
 
 
 
160
  const fillRateCum = totDemand > 0 ? totFulfilled / totDemand : 0;
161
  timeline.push({ day, demand, inventory: preInv, inventoryAfter: inventory, fulfilled, lost, rop: Math.round(rop), ordered, wo, delivered, fillRateCum });
162
  }
 
163
  return {
164
  timeline,
165
- metrics: { fillRate: totDemand > 0 ? totFulfilled / totDemand : 0, stockOuts, lostSales, totWriteOff, totDemand, totFulfilled },
166
  };
167
  }
168
 
@@ -234,7 +245,7 @@ async function runAgentLoop({ envKey, modelId, hfToken, onDay, onDecision, onSta
234
  const env = ENVS[envKey];
235
  let inventory = 0;
236
  const orders = [];
237
- let totDemand = 0, totFulfilled = 0, totWriteOff = 0, stockOuts = 0, lostSales = 0;
238
  const timeline = [];
239
  let currentROP = env.demMean * CFG.LEAD_TIME;
240
  let memory = [];
@@ -252,7 +263,7 @@ async function runAgentLoop({ envKey, modelId, hfToken, onDay, onDecision, onSta
252
  const fulfilled = Math.min(demand, inventory);
253
  inventory = Math.max(0, inventory - demand);
254
  const lost = Math.max(0, demand - fulfilled);
255
- if (lost > 0) stockOuts++;
256
  lostSales += lost;
257
  let ordered = 0;
258
  if (hist.length >= 5 && day < CFG.SIM_DAYS - CFG.LEAD_TIME && inventory <= currentROP) {
@@ -268,6 +279,12 @@ async function runAgentLoop({ envKey, modelId, hfToken, onDay, onDecision, onSta
268
  }
269
  totDemand += demand;
270
  totFulfilled += fulfilled;
 
 
 
 
 
 
271
  const fillRateCum = totDemand > 0 ? totFulfilled / totDemand : 0;
272
  const entry = { day, demand, inventory: preInv, inventoryAfter: inventory, fulfilled, lost, rop: Math.round(currentROP), ordered, wo, delivered, fillRateCum };
273
  timeline.push(entry);
@@ -313,7 +330,7 @@ async function runAgentLoop({ envKey, modelId, hfToken, onDay, onDecision, onSta
313
  }
314
  return {
315
  timeline,
316
- metrics: { fillRate: totDemand > 0 ? totFulfilled / totDemand : 0, stockOuts, lostSales, totWriteOff, totDemand, totFulfilled },
317
  memory,
318
  };
319
  }
@@ -463,7 +480,7 @@ function ComparePanel({ agentMetrics, agentLog, simTimeline, baselineResults })
463
  {agentMetrics && (
464
  <div style={{ background: "#0a1e18", border: `2px solid ${C.teal}40`, borderRadius: 10, padding: 14 }}>
465
  <div style={{ fontSize: 9, color: C.teal, letterSpacing: 3, marginBottom: 8 }}>πŸ€– LLM AGENT</div>
466
- {[["Fill Rate", <FillBadge rate={agentMetrics.fillRate} />], ["Stockouts", agentMetrics.stockOuts], ["Lost Sales", agentMetrics.lostSales.toLocaleString()], ["Write-Offs", agentMetrics.totWriteOff.toLocaleString()]].map(([l, v]) => (
467
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 5 }}>
468
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
469
  </div>
@@ -473,7 +490,7 @@ function ComparePanel({ agentMetrics, agentLog, simTimeline, baselineResults })
473
  {Object.entries(baselineResults).map(([bk, br]) => (
474
  <div key={bk} style={{ background: C.panel, border: `1px solid ${BASELINES[bk].color}30`, borderRadius: 10, padding: 14 }}>
475
  <div style={{ fontSize: 9, color: BASELINES[bk].color, letterSpacing: 3, marginBottom: 8 }}>{BASELINES[bk].label.toUpperCase()}</div>
476
- {[["Fill Rate", <FillBadge rate={br.metrics.fillRate} />], ["Stockouts", br.metrics.stockOuts], ["Lost Sales", br.metrics.lostSales.toLocaleString()], ["Write-Offs", br.metrics.totWriteOff.toLocaleString()]].map(([l, v]) => (
477
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 5 }}>
478
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
479
  </div>
@@ -600,10 +617,10 @@ function AgentSimView({ label, accentColor, modelId, hfToken, envKey, baselineRe
600
  </div>
601
  {metrics && (
602
  <div style={{ display: "flex", gap: 10, marginBottom: 14, flexWrap: "wrap" }}>
603
- <MetricBox label="FILL RATE" value={<FillBadge rate={metrics.fillRate} />} highlight color={accentColor} />
 
 
604
  <MetricBox label="STOCKOUTS" value={metrics.stockOuts} />
605
- <MetricBox label="LOST SALES" value={metrics.lostSales.toLocaleString()} />
606
- <MetricBox label="WRITE-OFFS" value={metrics.totWriteOff.toLocaleString()} />
607
  <MetricBox label="DECISIONS" value={log.length} />
608
  </div>
609
  )}
@@ -804,7 +821,7 @@ export default function StockOracle() {
804
  {Object.entries(baselineResults).map(([k, r]) => (
805
  <div key={k} style={{ background: C.panel, border: `1px solid ${BASELINES[k].color}30`, borderRadius: 10, padding: 16 }}>
806
  <div style={{ fontSize: 9, color: BASELINES[k].color, letterSpacing: 3, marginBottom: 10 }}>{BASELINES[k].label.toUpperCase()}</div>
807
- {[["Fill Rate", <FillBadge rate={r.metrics.fillRate} />], ["Stockouts", r.metrics.stockOuts], ["Lost Sales", r.metrics.lostSales.toLocaleString()], ["Write-Offs", r.metrics.totWriteOff.toLocaleString()]].map(([l, v]) => (
808
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 6 }}>
809
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
810
  </div>
 
22
  SIM_DAYS: 730,
23
  DECISION_INTERVAL: 5,
24
  MEMORY_SIZE: 200,
25
+ SELLING_PRICE: 25.0,
26
+ UNIT_COST: 10.0,
27
+ FIXED_ORDER_COST: 150.0,
28
+ HOLDING_RATE: 0.02,
29
  };
30
 
31
  // ─── MATH HELPERS ─────────────────────────────────────────────────────────────
 
128
  const n = demandSeries.length;
129
  let inventory = 0;
130
  const orders = [];
131
+ let totDemand = 0, totFulfilled = 0, totWriteOff = 0, stockOuts = 0, lostSales = 0, totProfit = 0, servicedays = 0;
132
  const timeline = [];
133
 
134
  for (let day = 0; day < n; day++) {
 
142
  const fulfilled = Math.min(demand, inventory);
143
  inventory = Math.max(0, inventory - demand);
144
  const lost = Math.max(0, demand - fulfilled);
145
+ if (lost > 0) stockOuts++; else servicedays++;
146
  lostSales += lost;
147
  let rop = 0, ordered = 0;
148
  if (hist.length >= 5 && day < n - CFG.LEAD_TIME) {
 
161
  }
162
  totDemand += demand;
163
  totFulfilled += fulfilled;
164
+ const revenue = fulfilled * CFG.SELLING_PRICE;
165
+ const holdingCost = inventory * CFG.UNIT_COST * CFG.HOLDING_RATE;
166
+ const stockoutPenalty = lost * (CFG.SELLING_PRICE - CFG.UNIT_COST);
167
+ const orderCost = (ordered > 0 ? CFG.FIXED_ORDER_COST : 0) + ordered * CFG.UNIT_COST;
168
+ const writeoffCost = wo * CFG.UNIT_COST;
169
+ totProfit += revenue - holdingCost - stockoutPenalty - orderCost - writeoffCost;
170
  const fillRateCum = totDemand > 0 ? totFulfilled / totDemand : 0;
171
  timeline.push({ day, demand, inventory: preInv, inventoryAfter: inventory, fulfilled, lost, rop: Math.round(rop), ordered, wo, delivered, fillRateCum });
172
  }
173
+ const daysElapsed = n;
174
  return {
175
  timeline,
176
+ metrics: { fillRate: totDemand > 0 ? totFulfilled / totDemand : 0, stockOuts, lostSales, totWriteOff, totDemand, totFulfilled, profit: totProfit, serviceLevel: daysElapsed > 0 ? servicedays / daysElapsed : 0 },
177
  };
178
  }
179
 
 
245
  const env = ENVS[envKey];
246
  let inventory = 0;
247
  const orders = [];
248
+ let totDemand = 0, totFulfilled = 0, totWriteOff = 0, stockOuts = 0, lostSales = 0, totProfit = 0, servicedays = 0;
249
  const timeline = [];
250
  let currentROP = env.demMean * CFG.LEAD_TIME;
251
  let memory = [];
 
263
  const fulfilled = Math.min(demand, inventory);
264
  inventory = Math.max(0, inventory - demand);
265
  const lost = Math.max(0, demand - fulfilled);
266
+ if (lost > 0) stockOuts++; else servicedays++;
267
  lostSales += lost;
268
  let ordered = 0;
269
  if (hist.length >= 5 && day < CFG.SIM_DAYS - CFG.LEAD_TIME && inventory <= currentROP) {
 
279
  }
280
  totDemand += demand;
281
  totFulfilled += fulfilled;
282
+ const revenue = fulfilled * CFG.SELLING_PRICE;
283
+ const holdingCost = inventory * CFG.UNIT_COST * CFG.HOLDING_RATE;
284
+ const stockoutPenalty = lost * (CFG.SELLING_PRICE - CFG.UNIT_COST);
285
+ const orderCost = (ordered > 0 ? CFG.FIXED_ORDER_COST : 0) + ordered * CFG.UNIT_COST;
286
+ const writeoffCost = wo * CFG.UNIT_COST;
287
+ totProfit += revenue - holdingCost - stockoutPenalty - orderCost - writeoffCost;
288
  const fillRateCum = totDemand > 0 ? totFulfilled / totDemand : 0;
289
  const entry = { day, demand, inventory: preInv, inventoryAfter: inventory, fulfilled, lost, rop: Math.round(currentROP), ordered, wo, delivered, fillRateCum };
290
  timeline.push(entry);
 
330
  }
331
  return {
332
  timeline,
333
+ metrics: { fillRate: totDemand > 0 ? totFulfilled / totDemand : 0, stockOuts, lostSales, totWriteOff, totDemand, totFulfilled, profit: totProfit, serviceLevel: CFG.SIM_DAYS > 0 ? servicedays / CFG.SIM_DAYS : 0 },
334
  memory,
335
  };
336
  }
 
480
  {agentMetrics && (
481
  <div style={{ background: "#0a1e18", border: `2px solid ${C.teal}40`, borderRadius: 10, padding: 14 }}>
482
  <div style={{ fontSize: 9, color: C.teal, letterSpacing: 3, marginBottom: 8 }}>πŸ€– LLM AGENT</div>
483
+ {[["Profit", `$${Math.round(agentMetrics.profit).toLocaleString()}`], ["Service Level", <FillBadge rate={agentMetrics.serviceLevel} />], ["Fill Rate", <FillBadge rate={agentMetrics.fillRate} />], ["Stockouts", agentMetrics.stockOuts]].map(([l, v]) => (
484
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 5 }}>
485
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
486
  </div>
 
490
  {Object.entries(baselineResults).map(([bk, br]) => (
491
  <div key={bk} style={{ background: C.panel, border: `1px solid ${BASELINES[bk].color}30`, borderRadius: 10, padding: 14 }}>
492
  <div style={{ fontSize: 9, color: BASELINES[bk].color, letterSpacing: 3, marginBottom: 8 }}>{BASELINES[bk].label.toUpperCase()}</div>
493
+ {[["Profit", `$${Math.round(br.metrics.profit).toLocaleString()}`], ["Service Level", <FillBadge rate={br.metrics.serviceLevel} />], ["Fill Rate", <FillBadge rate={br.metrics.fillRate} />], ["Stockouts", br.metrics.stockOuts]].map(([l, v]) => (
494
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 5 }}>
495
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
496
  </div>
 
617
  </div>
618
  {metrics && (
619
  <div style={{ display: "flex", gap: 10, marginBottom: 14, flexWrap: "wrap" }}>
620
+ <MetricBox label="PROFIT" value={`$${Math.round(metrics.profit).toLocaleString()}`} highlight color={accentColor} />
621
+ <MetricBox label="SERVICE LEVEL" value={<FillBadge rate={metrics.serviceLevel} />} />
622
+ <MetricBox label="FILL RATE" value={<FillBadge rate={metrics.fillRate} />} />
623
  <MetricBox label="STOCKOUTS" value={metrics.stockOuts} />
 
 
624
  <MetricBox label="DECISIONS" value={log.length} />
625
  </div>
626
  )}
 
821
  {Object.entries(baselineResults).map(([k, r]) => (
822
  <div key={k} style={{ background: C.panel, border: `1px solid ${BASELINES[k].color}30`, borderRadius: 10, padding: 16 }}>
823
  <div style={{ fontSize: 9, color: BASELINES[k].color, letterSpacing: 3, marginBottom: 10 }}>{BASELINES[k].label.toUpperCase()}</div>
824
+ {[["Profit", `$${Math.round(r.metrics.profit).toLocaleString()}`], ["Service Level", <FillBadge rate={r.metrics.serviceLevel} />], ["Fill Rate", <FillBadge rate={r.metrics.fillRate} />], ["Stockouts", r.metrics.stockOuts]].map(([l, v]) => (
825
  <div key={l} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, marginBottom: 6 }}>
826
  <span style={{ color: C.muted }}>{l}</span><span style={{ fontWeight: 600 }}>{v}</span>
827
  </div>