diff --git a/app/src/modules/simulation/analyzer/analyzer.tsx b/app/src/modules/simulation/analyzer/analyzer.tsx index fc0ec7a..dd27dd8 100644 --- a/app/src/modules/simulation/analyzer/analyzer.tsx +++ b/app/src/modules/simulation/analyzer/analyzer.tsx @@ -17,15 +17,192 @@ function Analyzer() { const { setAnalysis, setAnalyzing, analysis } = analysisStore(); + // ============================================================================ + // COMPREHENSIVE TRACKING REFS FOR PERFORMANCE METRICS + // ============================================================================ + + // Historical data tracking const historicalDataRef = useRef>({}); const materialHistoryRef = useRef([]); const queueLengthsRef = useRef>({}); + + // Timing and intervals const analysisIntervalRef = useRef(null); const startTimeRef = useRef(new Date().toISOString()); + + // Error and action tracking const errorCountsRef = useRef>({}); const completedActionsRef = useRef>({}); const stateTransitionsRef = useRef>({}); + // 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; }