Spaces:
Runtime error
Runtime error
ademarteau commited on
Commit Β·
e7f1f53
1
Parent(s): b2065cc
metrics: profit first, then service level, then fill rate (React UI)
Browse files- 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 |
-
{[["
|
| 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 |
-
{[["
|
| 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="
|
|
|
|
|
|
|
| 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 |
-
{[["
|
| 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>
|