feat: Add comprehensive simulation analyzer module with extensive tracking and metric calculation capabilities.

This commit is contained in:
2025-12-18 15:25:39 +05:30
parent 5a1d1bdeaf
commit b3adb4ea20

View File

@@ -17,15 +17,192 @@ function Analyzer() {
const { setAnalysis, setAnalyzing, analysis } = analysisStore();
// ============================================================================
// COMPREHENSIVE TRACKING REFS FOR PERFORMANCE METRICS
// ============================================================================
// Historical data tracking
const historicalDataRef = useRef<Record<string, any[]>>({});
const materialHistoryRef = useRef<MaterialHistoryEntry[]>([]);
const queueLengthsRef = useRef<Record<string, { timestamp: number; length: number }[]>>({});
// Timing and intervals
const analysisIntervalRef = useRef<NodeJS.Timeout | null>(null);
const startTimeRef = useRef<string>(new Date().toISOString());
// Error and action tracking
const errorCountsRef = useRef<Record<string, number>>({});
const completedActionsRef = useRef<Record<string, number>>({});
const stateTransitionsRef = useRef<Record<string, any[]>>({});
// Material flow tracking - tracks materials added/removed per asset
const materialAdditionsRef = useRef<
Record<
string,
{
materialId: string;
materialType: string;
timestamp: number;
fromAsset?: string;
}[]
>
>({});
const materialRemovalsRef = useRef<
Record<
string,
{
materialId: string;
materialType: string;
timestamp: number;
toAsset?: string;
processingTime?: number;
}[]
>
>({});
// Asset state change tracking with detailed timestamps
const assetStateChangesRef = useRef<
Record<
string,
{
fromState: string;
toState: string;
timestamp: number;
duration?: number;
}[]
>
>({});
// Cycle tracking for each asset
const assetCyclesRef = useRef<
Record<
string,
{
cycleId: string;
startTime: number;
endTime?: number;
cycleType: string; // 'processing', 'transport', 'pick-place', etc.
materialsInvolved: string[];
success: boolean;
}[]
>
>({});
// Action completion times for performance analysis
const actionCompletionTimesRef = useRef<
Record<
string,
{
actionId: string;
actionType: string;
startTime: number;
endTime: number;
duration: number;
success: boolean;
}[]
>
>({});
// Material processing times per asset
const materialProcessingTimesRef = useRef<
Record<
string,
{
materialId: string;
entryTime: number;
exitTime?: number;
processingDuration?: number;
waitTime?: number;
}[]
>
>({});
// WIP (Work In Progress) tracking per asset over time
const wipSnapshotsRef = useRef<
Record<
string,
{
timestamp: number;
wipCount: number;
materialIds: string[];
}[]
>
>({});
// Throughput snapshots for trend analysis
const throughputSnapshotsRef = useRef<
Record<
string,
{
timestamp: number;
itemsProcessed: number;
timeWindow: number; // in seconds
rate: number; // items per hour
}[]
>
>({});
// Asset performance snapshots
const performanceSnapshotsRef = useRef<
Record<
string,
{
timestamp: number;
utilization: number;
efficiency: number;
quality: number;
oee: number;
}[]
>
>({});
// Bottleneck detection tracking
const bottleneckEventsRef = useRef<
Record<
string,
{
timestamp: number;
queueLength: number;
utilizationRate: number;
waitingMaterials: string[];
}[]
>
>({});
// Previous state tracking for delta calculations
const previousAssetStatesRef = useRef<
Record<
string,
{
state: string;
isActive: boolean;
materialCount: number;
timestamp: number;
}
>
>({});
// Material lifecycle tracking
const materialLifecycleRef = useRef<
Record<
string,
{
materialId: string;
createdAt: number;
completedAt?: number;
path: {
assetId: string;
assetType: string;
entryTime: number;
exitTime?: number;
}[];
totalProcessingTime?: number;
totalWaitTime?: number;
}
>
>({});
// ============================================================================
// ENHANCED UTILITY FUNCTIONS
// ============================================================================
@@ -247,6 +424,370 @@ function Analyzer() {
}));
};
// ============================================================================
// TRACKING HELPER FUNCTIONS
// ============================================================================
/**
* Track material addition to an asset
*/
const trackMaterialAddition = useCallback((assetId: string, materialId: string, materialType: string, fromAsset?: string) => {
const timestamp = Date.now();
if (!materialAdditionsRef.current[assetId]) {
materialAdditionsRef.current[assetId] = [];
}
materialAdditionsRef.current[assetId].push({
materialId,
materialType,
timestamp,
fromAsset,
});
// Track in material processing times
if (!materialProcessingTimesRef.current[assetId]) {
materialProcessingTimesRef.current[assetId] = [];
}
materialProcessingTimesRef.current[assetId].push({
materialId,
entryTime: timestamp,
});
// Update material lifecycle
if (!materialLifecycleRef.current[materialId]) {
materialLifecycleRef.current[materialId] = {
materialId,
createdAt: timestamp,
path: [],
};
}
materialLifecycleRef.current[materialId].path.push({
assetId,
assetType: "", // Will be filled by caller
entryTime: timestamp,
});
// Update WIP snapshot
updateWIPSnapshot(assetId);
}, []);
/**
* Track material removal from an asset
*/
const trackMaterialRemoval = useCallback((assetId: string, materialId: string, materialType: string, toAsset?: string) => {
const timestamp = Date.now();
if (!materialRemovalsRef.current[assetId]) {
materialRemovalsRef.current[assetId] = [];
}
// Calculate processing time
const processingTimes = materialProcessingTimesRef.current[assetId] || [];
const entryRecord = processingTimes.find((p) => p.materialId === materialId && !p.exitTime);
let processingTime: number | undefined;
if (entryRecord) {
processingTime = timestamp - entryRecord.entryTime;
entryRecord.exitTime = timestamp;
entryRecord.processingDuration = processingTime;
}
materialRemovalsRef.current[assetId].push({
materialId,
materialType,
timestamp,
toAsset,
processingTime,
});
// Update material lifecycle
const lifecycle = materialLifecycleRef.current[materialId];
if (lifecycle && lifecycle.path.length > 0) {
const lastPathEntry = lifecycle.path[lifecycle.path.length - 1];
if (lastPathEntry.assetId === assetId) {
lastPathEntry.exitTime = timestamp;
}
}
// Update WIP snapshot
updateWIPSnapshot(assetId);
// Update throughput snapshot
updateThroughputSnapshot(assetId);
}, []);
/**
* Track asset state change
*/
const trackStateChange = useCallback((assetId: string, fromState: string, toState: string) => {
const timestamp = Date.now();
if (!assetStateChangesRef.current[assetId]) {
assetStateChangesRef.current[assetId] = [];
}
// Calculate duration from previous state
const previousChanges = assetStateChangesRef.current[assetId];
let duration: number | undefined;
if (previousChanges.length > 0) {
const lastChange = previousChanges[previousChanges.length - 1];
duration = timestamp - lastChange.timestamp;
lastChange.duration = duration;
}
assetStateChangesRef.current[assetId].push({
fromState,
toState,
timestamp,
});
// Also track in state transitions ref for compatibility
if (!stateTransitionsRef.current[assetId]) {
stateTransitionsRef.current[assetId] = [];
}
stateTransitionsRef.current[assetId].push({
fromState,
toState,
timestamp,
duration: duration || 0,
});
}, []);
/**
* Start a new cycle for an asset
*/
const startAssetCycle = useCallback((assetId: string, cycleType: string, materialsInvolved: string[] = []) => {
const timestamp = Date.now();
const cycleId = `${assetId}-${timestamp}`;
if (!assetCyclesRef.current[assetId]) {
assetCyclesRef.current[assetId] = [];
}
assetCyclesRef.current[assetId].push({
cycleId,
startTime: timestamp,
cycleType,
materialsInvolved,
success: false,
});
return cycleId;
}, []);
/**
* Complete a cycle for an asset
*/
const completeAssetCycle = useCallback((assetId: string, cycleId: string, success: boolean = true) => {
const timestamp = Date.now();
const cycles = assetCyclesRef.current[assetId] || [];
const cycle = cycles.find((c) => c.cycleId === cycleId);
if (cycle) {
cycle.endTime = timestamp;
cycle.success = success;
}
}, []);
/**
* Track action completion
*/
const trackActionCompletion = useCallback((assetId: string, actionId: string, actionType: string, startTime: number, success: boolean = true) => {
const endTime = Date.now();
if (!actionCompletionTimesRef.current[assetId]) {
actionCompletionTimesRef.current[assetId] = [];
}
actionCompletionTimesRef.current[assetId].push({
actionId,
actionType,
startTime,
endTime,
duration: endTime - startTime,
success,
});
// Limit history to last 100 actions
if (actionCompletionTimesRef.current[assetId].length > 100) {
actionCompletionTimesRef.current[assetId] = actionCompletionTimesRef.current[assetId].slice(-100);
}
}, []);
/**
* Update WIP snapshot for an asset
*/
const updateWIPSnapshot = useCallback(
(assetId: string) => {
const timestamp = Date.now();
const currentMaterials = getMaterialsByModel(assetId);
if (!wipSnapshotsRef.current[assetId]) {
wipSnapshotsRef.current[assetId] = [];
}
wipSnapshotsRef.current[assetId].push({
timestamp,
wipCount: currentMaterials.length,
materialIds: currentMaterials.map((m) => m.materialId),
});
// Keep only last 100 snapshots
if (wipSnapshotsRef.current[assetId].length > 100) {
wipSnapshotsRef.current[assetId] = wipSnapshotsRef.current[assetId].slice(-100);
}
},
[getMaterialsByModel]
);
/**
* Update throughput snapshot for an asset
*/
const updateThroughputSnapshot = useCallback((assetId: string) => {
const timestamp = Date.now();
const timeWindow = 60; // 60 seconds
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
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);
}
}, []);
/**
* Update performance snapshot for an asset
*/
const updatePerformanceSnapshot = useCallback((assetId: string, utilization: number, efficiency: number, quality: number, oee: number) => {
const timestamp = Date.now();
if (!performanceSnapshotsRef.current[assetId]) {
performanceSnapshotsRef.current[assetId] = [];
}
performanceSnapshotsRef.current[assetId].push({
timestamp,
utilization,
efficiency,
quality,
oee,
});
// Keep only last 100 snapshots
if (performanceSnapshotsRef.current[assetId].length > 100) {
performanceSnapshotsRef.current[assetId] = performanceSnapshotsRef.current[assetId].slice(-100);
}
}, []);
/**
* Track bottleneck event
*/
const trackBottleneckEvent = useCallback((assetId: string, queueLength: number, utilizationRate: number, waitingMaterials: string[]) => {
const timestamp = Date.now();
if (!bottleneckEventsRef.current[assetId]) {
bottleneckEventsRef.current[assetId] = [];
}
// Only track if it's a significant bottleneck
if (queueLength > 2 || utilizationRate > 0.85) {
bottleneckEventsRef.current[assetId].push({
timestamp,
queueLength,
utilizationRate,
waitingMaterials,
});
// Keep only last 50 events
if (bottleneckEventsRef.current[assetId].length > 50) {
bottleneckEventsRef.current[assetId] = bottleneckEventsRef.current[assetId].slice(-50);
}
}
}, []);
/**
* Update previous asset state for delta tracking
*/
const updatePreviousAssetState = useCallback((assetId: string, state: string, isActive: boolean, materialCount: number) => {
previousAssetStatesRef.current[assetId] = {
state,
isActive,
materialCount,
timestamp: Date.now(),
};
}, []);
/**
* Get enhanced material flow metrics using tracked data
*/
const getEnhancedMaterialFlowMetrics = useCallback(
(assetId: string) => {
const additions = materialAdditionsRef.current[assetId] || [];
const removals = materialRemovalsRef.current[assetId] || [];
const processingTimes = materialProcessingTimesRef.current[assetId] || [];
const wipSnapshots = wipSnapshotsRef.current[assetId] || [];
// Calculate average processing time
const completedProcessing = processingTimes.filter((p) => p.processingDuration);
const avgProcessingTime = completedProcessing.length > 0 ? completedProcessing.reduce((sum, p) => sum + (p.processingDuration || 0), 0) / completedProcessing.length : 0;
// Calculate current WIP
const currentWIP = wipSnapshots.length > 0 ? wipSnapshots[wipSnapshots.length - 1].wipCount : 0;
// Calculate throughput rate
const now = Date.now();
const oneHourAgo = now - 3600000;
const recentRemovals = removals.filter((r) => r.timestamp >= oneHourAgo);
const throughputRate = recentRemovals.length; // items per hour
// Calculate cycle efficiency
const totalAdded = additions.length;
const totalRemoved = removals.length;
const cycleEfficiency = totalAdded > 0 ? (totalRemoved / totalAdded) * 100 : 100;
return {
totalAdded,
totalRemoved,
currentWIP,
avgProcessingTime: avgProcessingTime / 1000, // convert to seconds
throughputRate,
cycleEfficiency,
processingTimeVariance: calculateVariance(completedProcessing.map((p) => p.processingDuration || 0)),
};
},
[getMaterialsByModel]
);
/**
* Calculate variance helper
*/
const calculateVariance = (values: number[]) => {
if (values.length === 0) return 0;
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
const squaredDiffs = values.map((v) => Math.pow(v - mean, 2));
return squaredDiffs.reduce((sum, v) => sum + v, 0) / values.length;
};
// ============================================================================
// ENHANCED CONVEYOR ANALYSIS
// ============================================================================
@@ -1632,6 +2173,110 @@ function Analyzer() {
};
}, [isPlaying]);
// Monitor material changes and track additions/removals
useEffect(() => {
if (!isPlaying) return;
// Track material movements by comparing current materials with previous state
materials.forEach((material) => {
const currentAssetId = material.current?.modelUuid;
const previousAssetId = material.previous?.modelUuid;
if (currentAssetId && previousAssetId && currentAssetId !== previousAssetId) {
// Material moved from one asset to another
trackMaterialRemoval(previousAssetId, material.materialId, material.materialType, currentAssetId);
trackMaterialAddition(currentAssetId, material.materialId, material.materialType, previousAssetId);
} else if (currentAssetId && !previousAssetId) {
// Material newly added to an asset
trackMaterialAddition(currentAssetId, material.materialId, material.materialType);
}
});
// Track material lifecycle completion
materials.forEach((material) => {
if (!material.isActive && material.endTime) {
const lifecycle = materialLifecycleRef.current[material.materialId];
if (lifecycle && !lifecycle.completedAt) {
lifecycle.completedAt = material.endTime;
// Calculate total processing and wait times
let totalProcessing = 0;
let totalWait = 0;
lifecycle.path.forEach((pathEntry) => {
if (pathEntry.exitTime) {
const duration = pathEntry.exitTime - pathEntry.entryTime;
totalProcessing += duration;
}
});
lifecycle.totalProcessingTime = totalProcessing;
lifecycle.totalWaitTime = totalWait;
}
}
});
}, [materials, isPlaying, trackMaterialAddition, trackMaterialRemoval]);
// Monitor asset state changes
useEffect(() => {
if (!isPlaying) return;
const allAssets = [
...conveyors.map((c) => ({ id: c.modelUuid, state: c.state, isActive: !c.isPaused, type: "conveyor" as const })),
...machines.map((m) => ({ id: m.modelUuid, state: m.state, isActive: m.isActive, type: "machine" as const })),
...armBots.map((a) => ({ id: a.modelUuid, state: a.state, isActive: a.isActive, type: "roboticArm" as const })),
...vehicles.map((v) => ({ id: v.modelUuid, state: v.state, isActive: v.isActive, type: "vehicle" as const })),
...humans.map((h) => ({ id: h.modelUuid, state: h.state, isActive: h.isActive, type: "human" as const })),
...cranes.map((c) => ({ id: c.modelUuid, state: c.state, isActive: c.isActive, type: "crane" as const })),
...storageUnits.map((s) => ({ id: s.modelUuid, state: s.state, isActive: s.isActive, type: "storage" as const })),
];
allAssets.forEach((asset) => {
const previousState = previousAssetStatesRef.current[asset.id];
const currentMaterialCount = getMaterialsByModel(asset.id).length;
if (previousState) {
// Check for state change
if (previousState.state !== asset.state) {
trackStateChange(asset.id, previousState.state, asset.state);
}
// Check for material count change (potential bottleneck)
if (currentMaterialCount !== previousState.materialCount) {
const utilizationRate = asset.isActive ? 0.8 : 0.2; // Simplified calculation
const waitingMaterials = getMaterialsByModel(asset.id).map((m) => m.materialId);
trackBottleneckEvent(asset.id, currentMaterialCount, utilizationRate, waitingMaterials);
}
}
// Update previous state
updatePreviousAssetState(asset.id, asset.state, asset.isActive, currentMaterialCount);
});
}, [conveyors, machines, armBots, vehicles, humans, cranes, storageUnits, isPlaying, getMaterialsByModel, trackStateChange, trackBottleneckEvent, updatePreviousAssetState]);
// Periodic WIP and throughput snapshots
useEffect(() => {
if (!isPlaying) return;
const snapshotInterval = setInterval(() => {
const allAssetIds = [
...conveyors.map((c) => c.modelUuid),
...machines.map((m) => m.modelUuid),
...armBots.map((a) => a.modelUuid),
...vehicles.map((v) => v.modelUuid),
...humans.map((h) => h.modelUuid),
...cranes.map((c) => c.modelUuid),
...storageUnits.map((s) => s.modelUuid),
];
allAssetIds.forEach((assetId) => {
updateWIPSnapshot(assetId);
});
}, 1000); // Every 1 seconds
return () => clearInterval(snapshotInterval);
}, [conveyors, machines, armBots, vehicles, humans, cranes, storageUnits, isPlaying, updateWIPSnapshot]);
return null;
}