feat: implement simulation analyzer for detailed simulation performance and flow analysis.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useCallback, useRef } from "react";
|
||||
import { useSceneContext } from "../../scene/sceneContext";
|
||||
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
|
||||
import { useAnimationPlaySpeed, usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
|
||||
|
||||
function Analyzer() {
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
@@ -14,6 +14,7 @@ function Analyzer() {
|
||||
const { cranes } = craneStore();
|
||||
const { storageUnits } = storageUnitStore();
|
||||
const { materials, getMaterialsByModel } = materialStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
|
||||
const { setAnalysis, setAnalyzing, analysis } = analysisStore();
|
||||
|
||||
@@ -222,6 +223,9 @@ function Analyzer() {
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
resetAllRefs();
|
||||
} else {
|
||||
// Reset start time when simulation starts
|
||||
startTimeRef.current = new Date().toISOString();
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
@@ -288,32 +292,35 @@ function Analyzer() {
|
||||
|
||||
const calculateMaterialFlowMetricsForAsset = (assetId: string) => {
|
||||
const materialsOnAsset = getMaterialsByModel(assetId).length;
|
||||
const materialHistory = materialHistoryRef.current.filter((m) => m.material.current?.modelUuid === assetId || m.material.previous?.modelUuid === assetId);
|
||||
|
||||
// Use removals as the history of processed items
|
||||
// This fixes the issue where items per hour was 0 because materialHistoryRef was empty
|
||||
const removals = materialRemovalsRef.current[assetId] || [];
|
||||
|
||||
// Calculate flow metrics
|
||||
const wip = materialsOnAsset;
|
||||
const throughput = (materialHistory.length / (Date.now() - new Date(startTimeRef.current).getTime())) * 1000;
|
||||
|
||||
// Calculate lead times
|
||||
const leadTimes = materialHistory
|
||||
.map((m) => {
|
||||
if (m.removedAt && m.material.startTime) {
|
||||
return new Date(m.removedAt).getTime() - m.material.startTime;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.filter((t) => t > 0);
|
||||
// Calculate throughput (items per second)
|
||||
// Ensure we don't divide by zero
|
||||
const durationMs = Date.now() - new Date(startTimeRef.current).getTime();
|
||||
|
||||
// Normalize by simulation speed to get "simulation time" throughput
|
||||
const currentSpeed = Math.max(1, speed);
|
||||
const throughput = durationMs > 1000 ? ((removals.length / durationMs) * 1000) / currentSpeed : 0;
|
||||
|
||||
// Calculate lead times (processing times on this asset)
|
||||
const leadTimes = removals.map((m) => m.processingTime || 0).filter((t) => t > 0);
|
||||
|
||||
const avgLeadTime = leadTimes.length > 0 ? leadTimes.reduce((sum, t) => sum + t, 0) / leadTimes.length : 0;
|
||||
|
||||
// Calculate velocity
|
||||
const velocity = materialHistory.length / Math.max(1, wip);
|
||||
// Calculate velocity (turnover)
|
||||
const velocity = removals.length / Math.max(1, wip);
|
||||
|
||||
return {
|
||||
wip,
|
||||
throughput,
|
||||
avgLeadTime,
|
||||
totalMaterialsProcessed: materialHistory.length,
|
||||
totalMaterialsProcessed: removals.length,
|
||||
currentMaterials: materialsOnAsset,
|
||||
avgCycleTime: avgLeadTime / 1000,
|
||||
materialVelocity: velocity,
|
||||
@@ -670,32 +677,37 @@ function Analyzer() {
|
||||
/**
|
||||
* Update throughput snapshot for an asset
|
||||
*/
|
||||
const updateThroughputSnapshot = useCallback((assetId: string) => {
|
||||
const timestamp = Date.now();
|
||||
const timeWindow = 60; // 60 seconds
|
||||
const updateThroughputSnapshot = useCallback(
|
||||
(assetId: string) => {
|
||||
const timestamp = Date.now();
|
||||
const timeWindow = 60; // 60 seconds
|
||||
|
||||
if (!throughputSnapshotsRef.current[assetId]) {
|
||||
throughputSnapshotsRef.current[assetId] = [];
|
||||
}
|
||||
if (!throughputSnapshotsRef.current[assetId]) {
|
||||
throughputSnapshotsRef.current[assetId] = [];
|
||||
}
|
||||
|
||||
// Count items processed in the last time window
|
||||
const removals = materialRemovalsRef.current[assetId] || [];
|
||||
const recentRemovals = removals.filter((r) => timestamp - r.timestamp <= timeWindow * 1000);
|
||||
const itemsProcessed = recentRemovals.length;
|
||||
const rate = (itemsProcessed / timeWindow) * 3600; // items per hour
|
||||
// Count items processed in the last time window
|
||||
const removals = materialRemovalsRef.current[assetId] || [];
|
||||
const recentRemovals = removals.filter((r) => timestamp - r.timestamp <= timeWindow * 1000);
|
||||
const itemsProcessed = recentRemovals.length;
|
||||
// Normalize by speed
|
||||
const currentSpeed = Math.max(1, speed);
|
||||
const rate = ((itemsProcessed / timeWindow) * 3600) / currentSpeed; // items per hour
|
||||
|
||||
throughputSnapshotsRef.current[assetId].push({
|
||||
timestamp,
|
||||
itemsProcessed,
|
||||
timeWindow,
|
||||
rate,
|
||||
});
|
||||
throughputSnapshotsRef.current[assetId].push({
|
||||
timestamp,
|
||||
itemsProcessed,
|
||||
timeWindow,
|
||||
rate,
|
||||
});
|
||||
|
||||
// Keep only last 100 snapshots
|
||||
if (throughputSnapshotsRef.current[assetId].length > 100) {
|
||||
throughputSnapshotsRef.current[assetId] = throughputSnapshotsRef.current[assetId].slice(-100);
|
||||
}
|
||||
}, []);
|
||||
// Keep only last 100 snapshots
|
||||
if (throughputSnapshotsRef.current[assetId].length > 100) {
|
||||
throughputSnapshotsRef.current[assetId] = throughputSnapshotsRef.current[assetId].slice(-100);
|
||||
}
|
||||
},
|
||||
[speed]
|
||||
);
|
||||
|
||||
/**
|
||||
* Update performance snapshot for an asset
|
||||
@@ -948,7 +960,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[conveyor.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[materials, analysis]
|
||||
[materials, analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1099,7 +1111,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[vehicle.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[materials, analysis]
|
||||
[materials, analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1224,7 +1236,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[armBot.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[materials, analysis]
|
||||
[materials, analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1349,7 +1361,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[machine.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[materials, analysis]
|
||||
[materials, analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1486,7 +1498,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[storage.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[analysis]
|
||||
[analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1624,7 +1636,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[human.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[analysis]
|
||||
[analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
@@ -1774,7 +1786,7 @@ function Analyzer() {
|
||||
historicalData: historicalDataRef.current[crane.modelUuid] || [],
|
||||
};
|
||||
},
|
||||
[analysis]
|
||||
[analysis, speed]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user