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