import { useEffect, useCallback, useRef } from "react"; import { useSceneContext } from "../../scene/sceneContext"; import { useAnimationPlaySpeed, usePlayButtonStore } from "../../../store/ui/usePlayButtonStore"; function Analyzer() { const { isPlaying } = usePlayButtonStore(); const { conveyorStore, machineStore, armBotStore, humanStore, vehicleStore, craneStore, storageUnitStore, materialStore, analysisStore, humanEventManagerRef } = useSceneContext(); const { conveyors } = conveyorStore(); const { machines } = machineStore(); const { armBots } = armBotStore(); const { humans } = humanStore(); const { vehicles } = vehicleStore(); const { cranes } = craneStore(); const { storageUnits } = storageUnitStore(); const { materials, getMaterialsByModel } = materialStore(); const { speed } = useAnimationPlaySpeed(); 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; } > >({}); // Track previous actions for ArmBots to detect cycle completion const previousArmBotActionsRef = useRef>({}); // Track previous actions for Machines to detect cycle completion const previousMachineActionsRef = useRef>({}); // Track previous action counts for Humans to detect completion from EventManager const previousHumanCountsRef = useRef>>({}); // Track previous vehicle phases to detect trip completion const previousVehiclePhasesRef = useRef>({}); // Track previous crane phases to detect cycle completion const previousCranePhasesRef = useRef>({}); // 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; } > >({}); const resetAllRefs = () => { assetCyclesRef.current = {}; assetStateChangesRef.current = {}; materialAdditionsRef.current = {}; materialRemovalsRef.current = {}; materialProcessingTimesRef.current = {}; wipSnapshotsRef.current = {}; throughputSnapshotsRef.current = {}; performanceSnapshotsRef.current = {}; bottleneckEventsRef.current = {}; previousAssetStatesRef.current = {}; previousArmBotActionsRef.current = {}; previousMachineActionsRef.current = {}; previousHumanCountsRef.current = {}; previousVehiclePhasesRef.current = {}; previousCranePhasesRef.current = {}; materialLifecycleRef.current = {}; setAnalysis(null); setAnalyzing(false); }; useEffect(() => { if (!isPlaying) { resetAllRefs(); } else { // Reset start time when simulation starts startTimeRef.current = new Date().toISOString(); } }, [isPlaying]); // ============================================================================ // ENHANCED UTILITY FUNCTIONS // ============================================================================ const calculateAdvancedTimeMetrics = (idleTime: number, activeTime: number, assetId: string, errorCount: number = 0) => { const totalTime = idleTime + activeTime; const uptime = totalTime > 0 ? (activeTime / totalTime) * 100 : 0; const downtime = idleTime; const utilizationRate = uptime / 100; // Enhanced reliability calculations const mtbf = errorCount > 0 ? totalTime / errorCount : totalTime; const mttr = errorCount > 0 ? downtime / errorCount : 0; const reliability = Math.exp(-errorCount / Math.max(1, totalTime / 3600)) * 100; // Reliability per hour // Calculate schedule adherence if scheduled data exists const scheduledTime = totalTime; // Could be enhanced with actual schedule data const scheduleAdherence = scheduledTime > 0 ? (activeTime / scheduledTime) * 100 : 100; return { uptime, downtime, utilizationRate, mtbf, mttr, reliability, totalTime, scheduleAdherence, }; }; const calculateQualityMetrics = (assetId: string, totalOperations: number, defects: number, historicalData: any[] = []) => { const errorCount = errorCountsRef.current[assetId] || 0; const firstPassYield = totalOperations > 0 ? ((totalOperations - defects) / totalOperations) * 100 : 0; const defectRate = totalOperations > 0 ? (defects / totalOperations) * 100 : 0; const scrapRate = defects > 0 ? defects * 0.1 : 0; // Assuming 10% scrap rate const reworkRate = defects > 0 ? defects * 0.9 : 0; // Assuming 90% rework // Calculate defect trends let defectTrend = "stable"; if (historicalData.length > 5) { const recentDefects = historicalData.slice(-5).filter((d) => d.defects > 0).length; const olderDefects = historicalData.slice(-10, -5).filter((d) => d.defects > 0).length; if (recentDefects > olderDefects * 1.5) defectTrend = "increasing"; else if (recentDefects < olderDefects * 0.5) defectTrend = "decreasing"; } return { errorRate: totalOperations > 0 ? (errorCount / totalOperations) * 100 : 0, errorFrequency: (errorCount / Math.max(1, totalOperations)) * 1000, // Errors per 1000 operations successRate: firstPassYield, firstPassYield, defectRate, scrapRate, reworkRate, defectTrend, defectsPerMillion: totalOperations > 0 ? (defects / totalOperations) * 1000000 : 0, stateTransitions: getStateTransitions(assetId), }; }; const calculateMaterialFlowMetricsForAsset = (assetId: string) => { const materialsOnAsset = getMaterialsByModel(assetId).length; // 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; // 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 (turnover) const velocity = removals.length / Math.max(1, wip); return { wip, throughput, avgLeadTime, totalMaterialsProcessed: removals.length, currentMaterials: materialsOnAsset, avgCycleTime: avgLeadTime / 1000, materialVelocity: velocity, inventoryTurns: throughput > 0 ? throughput / Math.max(1, wip) : 0, }; }; const calculatePerformanceMetrics = (actualOutput: number, idealOutput: number, actualTime: number, idealTime: number, assetType: string) => { if (idealTime === 0 || actualTime === 0) return { performanceRate: 100, timeEfficiency: 100, productivity: 0, outputPerHour: 0, }; const performanceRate = idealOutput > 0 ? Math.min((actualOutput / idealOutput) * 100, 100) : 100; const timeEfficiency = idealTime > 0 ? Math.min((idealTime / actualTime) * 100, 100) : 100; const productivity = actualTime > 0 ? actualOutput / actualTime : 0; const outputPerHour = actualTime > 0 ? (actualOutput / actualTime) * 3600 : 0; // Asset type specific adjustments const efficiencyAdjustment = { conveyor: 1.0, vehicle: 0.95, machine: 0.9, roboticArm: 0.92, human: 0.85, crane: 0.88, storage: 0.98, }[assetType] || 1.0; return { performanceRate: performanceRate * efficiencyAdjustment, timeEfficiency: timeEfficiency * efficiencyAdjustment, productivity, outputPerHour, efficiencyAdjustment, }; }; const calculateCostMetrics = (assetId: string, assetType: EventType, activeTime: number, totalOutput: number) => { // Cost parameters (hourly rates in $) const costParams = { conveyor: { hourlyRate: 50, maintenanceCost: 0.5, energyCost: 5 }, vehicle: { hourlyRate: 75, maintenanceCost: 2, energyCost: 10 }, machine: { hourlyRate: 100, maintenanceCost: 5, energyCost: 15 }, roboticArm: { hourlyRate: 120, maintenanceCost: 8, energyCost: 20 }, human: { hourlyRate: 35, maintenanceCost: 0.1, energyCost: 0 }, crane: { hourlyRate: 150, maintenanceCost: 10, energyCost: 25 }, storage: { hourlyRate: 25, maintenanceCost: 1, energyCost: 2 }, }; const params = costParams[assetType] || costParams.conveyor; const hoursOperated = activeTime / 3600; const errorCount = errorCountsRef.current[assetId] || 0; const operatingCost = hoursOperated * params.hourlyRate; const maintenanceCost = errorCount * params.maintenanceCost; const energyCost = hoursOperated * params.energyCost; const totalCost = operatingCost + maintenanceCost + energyCost; const costPerUnit = totalOutput > 0 ? totalCost / totalOutput : 0; const costPerHour = totalCost / Math.max(1, hoursOperated); const roi = totalOutput > 0 ? (totalOutput * 10) / totalCost : 0; // Assuming $10 value per unit return { operatingCost, maintenanceCost, energyCost, totalCost, costPerUnit, costPerHour, roi, valueAdded: totalOutput * 10 - totalCost, }; }; const calculateEnergyMetrics = (assetType: EventType, activeTime: number, distanceTraveled: number = 0) => { // Energy consumption rates in kW const energyRates = { conveyor: 7.5, vehicle: 15 + distanceTraveled * 0.1, // Base + distance-based machine: 20, roboticArm: 10, human: 0, crane: 25, storage: 2, }; const rate = energyRates[assetType] || 5; const hoursActive = activeTime / 3600; const energyConsumed = hoursActive * rate; // kWh const energyEfficiency = 85 - Math.random() * 10; // Simulated efficiency 75-85% const carbonFootprint = energyConsumed * 0.5; // kg CO2 per kWh (0.5 kg/kWh average) const energyCost = energyConsumed * 0.12; // $0.12 per kWh return { energyConsumed, energyEfficiency, carbonFootprint, powerUsage: rate, energyCost, energyPerUnit: 0, // Will be calculated with output }; }; const calculateOEE = (availability: number, performance: number, quality: number) => { return (availability * performance * quality) / 10000; }; const getStateTransitions = (assetId: string) => { const transitions = stateTransitionsRef.current[assetId] || []; const grouped = transitions.reduce((acc, t) => { const key = `${t.fromState}-${t.toState}`; if (!acc[key]) { acc[key] = { fromState: t.fromState, toState: t.toState, count: 0, totalTime: 0 }; } acc[key].count++; acc[key].totalTime += t.duration; return acc; }, {} as Record); return Object.values(grouped).map((g: any) => ({ fromState: g.fromState, toState: g.toState, count: g.count, averageTime: g.totalTime / g.count, })); }; // ============================================================================ // 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, context?: { actionName?: string }) => { const timestamp = Date.now(); // Increment error count if entering error state if (toState === "error") { if (!errorCountsRef.current[assetId]) { errorCountsRef.current[assetId] = 0; } errorCountsRef.current[assetId]++; // Granular error tracking based on action type if (context?.actionName) { const actionName = context.actionName.toLowerCase(); if (actionName.includes("pick")) { if (!errorCountsRef.current[`${assetId}_pick`]) errorCountsRef.current[`${assetId}_pick`] = 0; errorCountsRef.current[`${assetId}_pick`]++; } else if (actionName.includes("place")) { if (!errorCountsRef.current[`${assetId}_place`]) errorCountsRef.current[`${assetId}_place`] = 0; errorCountsRef.current[`${assetId}_place`]++; } } } 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; // 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, }); // 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 */ 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 // ============================================================================ const analyzeConveyor = useCallback( (conveyor: ConveyorStatus): ConveyorAnalysis => { const errorCount = errorCountsRef.current[conveyor.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(conveyor.idleTime || 0, conveyor.activeTime || 0, conveyor.modelUuid, errorCount); const materialFlow = calculateMaterialFlowMetricsForAsset(conveyor.modelUuid); const materialsProcessed = materialFlow.totalMaterialsProcessed; const defects = errorCountsRef.current[`${conveyor.modelUuid}_defects`] || 0; const qualityMetrics = calculateQualityMetrics(conveyor.modelUuid, materialsProcessed, defects); // Performance calculations const conveyorLength = 10; // meters const idealSpeed = conveyor.speed; const actualSpeed = conveyor.isActive ? conveyor.speed : 0; const idealThroughput = idealSpeed > 0 ? (idealSpeed * 3600) / (conveyorLength / 2) : 0; // items per hour const actualThroughput = materialFlow.throughput * 3600; const performance = calculatePerformanceMetrics( materialsProcessed, idealThroughput * (timeMetrics.totalTime / 3600), timeMetrics.totalTime, materialsProcessed * 2, // 2 seconds per item ideal "conveyor" ); const costMetrics = calculateCostMetrics(conveyor.modelUuid, "conveyor", conveyor.activeTime || 0, materialsProcessed); const energyMetrics = calculateEnergyMetrics("conveyor", conveyor.activeTime || 0); // Update historical data const timestamp = new Date().toISOString(); const newEntry = { timestamp, isActive: !conveyor.isPaused, speed: conveyor.speed, state: conveyor.state, materialsCount: materialFlow.currentMaterials, throughput: actualThroughput, performance: performance.performanceRate, }; const currentData = historicalDataRef.current[conveyor.modelUuid] || []; historicalDataRef.current[conveyor.modelUuid] = [...currentData, newEntry].slice(-100); // Calculate queue if applicable const queueLength = materialFlow.wip; const currentQueueData = queueLengthsRef.current[conveyor.modelUuid] || []; queueLengthsRef.current[conveyor.modelUuid] = [...currentQueueData, { timestamp: Date.now(), length: queueLength }].slice(-100); return { assetId: conveyor.modelUuid, assetName: conveyor.modelName, assetType: "conveyor", currentStatus: { isActive: !conveyor.isPaused, isPaused: conveyor.isPaused, state: conveyor.state, speed: conveyor.speed, currentProduct: conveyor.productUuid, currentMaterials: materialFlow.currentMaterials, queueLength, }, timeMetrics: { ...timeMetrics, idleTime: conveyor.idleTime || 0, activeTime: conveyor.activeTime || 0, totalTime: timeMetrics.totalTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, scheduleAdherence: timeMetrics.scheduleAdherence, }, throughput: { itemsPerHour: actualThroughput, itemsPerDay: actualThroughput * 24, materialFlowRate: conveyor.speed * 60, capacityUtilization: timeMetrics.utilizationRate * 100, materialsProcessed, averageProcessingTime: materialFlow.avgCycleTime, wip: materialFlow.wip, throughputEfficiency: (actualThroughput / Math.max(1, idealThroughput)) * 100, bottleneckIndex: timeMetrics.utilizationRate > 0.9 ? 1 : 0, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performance.performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performance.performanceRate, quality: qualityMetrics.firstPassYield, timeEfficiency: performance.timeEfficiency, energyEfficiency: energyMetrics.energyEfficiency, costEfficiency: 100 - costMetrics.costPerUnit * 10, // Inverse relationship scheduleEfficiency: timeMetrics.scheduleAdherence, }, quality: qualityMetrics, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, costPerUnit: costMetrics.costPerUnit, costPerHour: costMetrics.costPerHour, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerUnit: materialsProcessed > 0 ? energyMetrics.energyConsumed / materialsProcessed : 0, }, // Material flow metrics materialFlow: { wip: materialFlow.wip, throughput: materialFlow.throughput, avgCycleTime: materialFlow.avgCycleTime, materialVelocity: materialFlow.materialVelocity, inventoryTurns: materialFlow.inventoryTurns, leadTimeVariance: 0, // Could be calculated with more data }, historicalData: historicalDataRef.current[conveyor.modelUuid] || [], }; }, [materials, analysis, speed] ); // ============================================================================ // ENHANCED VEHICLE ANALYSIS (apply similar enhancements to other assets) // ============================================================================ const analyzeVehicle = useCallback( (vehicle: VehicleStatus): VehicleAnalysis => { const errorCount = errorCountsRef.current[vehicle.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(vehicle.idleTime || 0, vehicle.activeTime || 0, vehicle.modelUuid, errorCount); const materialFlow = calculateMaterialFlowMetricsForAsset(vehicle.modelUuid); const tripsCompleted = completedActionsRef.current[vehicle.modelUuid] || 0; const totalLoadsDelivered = completedActionsRef.current[`${vehicle.modelUuid}_loads`] || 0; const defects = errorCount; // Use main error count directly const qualityMetrics = calculateQualityMetrics(vehicle.modelUuid, totalLoadsDelivered, defects); // Performance calculations const loadCapacity = vehicle.point?.action?.loadCapacity || 1; const avgLoad = tripsCompleted > 0 ? totalLoadsDelivered / tripsCompleted : 0; const loadUtilization = (avgLoad / loadCapacity) * 100; const idealTripTime = 60; const actualTripTime = tripsCompleted > 0 ? timeMetrics.totalTime / tripsCompleted : 0; const idealThroughput = loadCapacity * (3600 / idealTripTime); const actualThroughput = materialFlow.throughput * 3600; const performance = calculatePerformanceMetrics(totalLoadsDelivered, idealThroughput * (timeMetrics.totalTime / 3600), timeMetrics.totalTime, idealTripTime * tripsCompleted, "vehicle"); // Route efficiency const optimalDistance = 100; const actualDistance = vehicle.distanceTraveled || 0; const routeEfficiency = actualDistance > 0 ? Math.min((optimalDistance / actualDistance) * 100, 100) : 100; const costMetrics = calculateCostMetrics(vehicle.modelUuid, "vehicle", vehicle.activeTime || 0, totalLoadsDelivered); const energyMetrics = calculateEnergyMetrics("vehicle", vehicle.activeTime || 0, actualDistance); // Update historical data const timestamp = new Date().toISOString(); const currentData = historicalDataRef.current[vehicle.modelUuid] || []; historicalDataRef.current[vehicle.modelUuid] = [ ...currentData, { timestamp, phase: vehicle.currentPhase, load: vehicle.currentLoad, distanceTraveled: actualDistance, state: vehicle.state, performance: performance.performanceRate, speed: vehicle.speed, tripsCompleted, }, ].slice(-100); return { assetId: vehicle.modelUuid, assetName: vehicle.modelName, assetType: "vehicle", currentStatus: { isActive: vehicle.isActive, isPicking: vehicle.isPicking, currentPhase: vehicle.currentPhase, state: vehicle.state, speed: vehicle.speed, currentLoad: vehicle.currentLoad, currentMaterials: vehicle.currentMaterials, distanceTraveled: actualDistance, currentRouteEfficiency: routeEfficiency, }, timeMetrics: { ...timeMetrics, idleTime: vehicle.idleTime || 0, activeTime: vehicle.activeTime || 0, totalTime: timeMetrics.totalTime, averageTripTime: actualTripTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, throughput: { itemsPerHour: actualThroughput, itemsPerDay: actualThroughput * 24, materialFlowRate: vehicle.speed, capacityUtilization: timeMetrics.utilizationRate * 100, tripsCompleted, averageLoadsPerTrip: avgLoad, totalLoadsDelivered, throughputEfficiency: (actualThroughput / Math.max(1, idealThroughput)) * 100, wip: materialFlow.wip, bottleneckIndex: timeMetrics.utilizationRate > 0.85 ? 1 : 0, }, movementMetrics: { distanceTraveled: actualDistance, averageSpeedActual: timeMetrics.totalTime > 0 ? actualDistance / timeMetrics.totalTime : 0, fuelEfficiency: actualDistance > 0 ? totalLoadsDelivered / actualDistance : 0, routeEfficiency, idleDistance: vehicle.idleTime || 0, totalTrips: tripsCompleted, averageTripDistance: tripsCompleted > 0 ? actualDistance / tripsCompleted : 0, distanceEfficiency: routeEfficiency, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performance.performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performance.performanceRate, quality: qualityMetrics.firstPassYield, loadUtilization, timeEfficiency: performance.timeEfficiency, energyEfficiency: energyMetrics.energyEfficiency, routeEfficiency, costEfficiency: 100 - costMetrics.costPerUnit * 5, }, quality: { ...qualityMetrics, onTimeDelivery: tripsCompleted > 0 ? ((tripsCompleted - defects) / tripsCompleted) * 100 : 100, damageRate: defects > 0 ? (defects / totalLoadsDelivered) * 100 : 0, accuracyRate: 100 - qualityMetrics.defectRate, }, costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, costPerMile: actualDistance > 0 ? costMetrics.totalCost / (actualDistance / 1609.34) : 0, // Convert meters to miles costPerTrip: tripsCompleted > 0 ? costMetrics.totalCost / tripsCompleted : 0, costPerLoad: totalLoadsDelivered > 0 ? costMetrics.totalCost / totalLoadsDelivered : 0, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, }, energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerMile: actualDistance > 0 ? energyMetrics.energyConsumed / actualDistance : 0, energyPerTrip: tripsCompleted > 0 ? energyMetrics.energyConsumed / tripsCompleted : 0, }, historicalData: historicalDataRef.current[vehicle.modelUuid] || [], }; }, [materials, analysis, speed] ); // ============================================================================ // ENHANCED ROBOTIC ARM ANALYSIS // ============================================================================ const analyzeRoboticArm = useCallback( (armBot: ArmBotStatus): RoboticArmAnalysis => { const errorCount = errorCountsRef.current[armBot.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(armBot.idleTime || 0, armBot.activeTime || 0, armBot.modelUuid, errorCount); const materialFlow = calculateMaterialFlowMetricsForAsset(armBot.modelUuid); const cyclesCompleted = completedActionsRef.current[armBot.modelUuid] || 0; const pickAndPlaceCount = completedActionsRef.current[`${armBot.modelUuid}_pickplace`] || cyclesCompleted; const defects = errorCount; // Use main error count directly const qualityMetrics = calculateQualityMetrics(armBot.modelUuid, pickAndPlaceCount, defects); // Performance calculations const idealCycleTime = 5; // 5 seconds ideal const actualCycleTime = cyclesCompleted > 0 ? timeMetrics.totalTime / cyclesCompleted : 0; const idealThroughput = (3600 / idealCycleTime) * 1; // 1 item per cycle const actualThroughput = materialFlow.throughput * 3600; const performance = calculatePerformanceMetrics(pickAndPlaceCount, idealThroughput * (timeMetrics.totalTime / 3600), timeMetrics.totalTime, idealCycleTime * cyclesCompleted, "roboticArm"); const costMetrics = calculateCostMetrics(armBot.modelUuid, "roboticArm", armBot.activeTime || 0, pickAndPlaceCount); const energyMetrics = calculateEnergyMetrics("roboticArm", armBot.activeTime || 0); // Calculate success rates // Calculate success rates const pickSuccessCount = completedActionsRef.current[`${armBot.modelUuid}_pick`] || pickAndPlaceCount / 2; const placeSuccessCount = completedActionsRef.current[`${armBot.modelUuid}_place`] || pickAndPlaceCount / 2; const pickErrors = errorCountsRef.current[`${armBot.modelUuid}_pick`] || 0; const placeErrors = errorCountsRef.current[`${armBot.modelUuid}_place`] || 0; // If granular errors are 0 but main error count > 0, distribute them (fallback) const remainingErrors = Math.max(0, errorCount - pickErrors - placeErrors); const effectivePickErrors = pickErrors + (remainingErrors > 0 ? Math.ceil(remainingErrors / 2) : 0); const effectivePlaceErrors = placeErrors + (remainingErrors > 0 ? Math.floor(remainingErrors / 2) : 0); const pickAttempts = pickSuccessCount + effectivePickErrors; const placeAttempts = placeSuccessCount + effectivePlaceErrors; const pickSuccessRate = pickAttempts > 0 ? (pickSuccessCount / pickAttempts) * 100 : 100; const placeAccuracy = placeAttempts > 0 ? (placeSuccessCount / placeAttempts) * 100 : 100; // Update historical data const timestamp = new Date().toISOString(); const newEntry = { timestamp, cycleTime: actualCycleTime, actionType: armBot.currentAction?.actionName || "unknown", isActive: armBot.isActive, state: armBot.state, speed: armBot.speed, cyclesCompleted, successRate: pickSuccessRate, performance: performance.performanceRate, }; const currentData = historicalDataRef.current[armBot.modelUuid] || []; historicalDataRef.current[armBot.modelUuid] = [...currentData, newEntry].slice(-100); return { assetId: armBot.modelUuid, assetName: armBot.modelName, assetType: "roboticArm", currentStatus: { isActive: armBot.isActive, state: armBot.state, speed: armBot.speed, currentAction: armBot.currentAction || null, }, timeMetrics: { ...timeMetrics, idleTime: armBot.idleTime || 0, activeTime: armBot.activeTime || 0, totalTime: timeMetrics.totalTime, averageCycleTime: actualCycleTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, throughput: { itemsPerHour: actualThroughput, itemsPerDay: actualThroughput * 24, materialFlowRate: armBot.speed, capacityUtilization: timeMetrics.utilizationRate * 100, cyclesCompleted, pickAndPlaceCount, throughputEfficiency: (actualThroughput / Math.max(1, idealThroughput)) * 100, wip: materialFlow.wip, bottleneckIndex: timeMetrics.utilizationRate > 0.85 ? 1 : 0, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performance.performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performance.performanceRate, quality: qualityMetrics.firstPassYield, cycleTimeEfficiency: performance.timeEfficiency, }, quality: { ...qualityMetrics, pickSuccessRate, placeAccuracy, }, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerUnit: pickAndPlaceCount > 0 ? energyMetrics.energyConsumed / pickAndPlaceCount : 0, }, historicalData: historicalDataRef.current[armBot.modelUuid] || [], }; }, [materials, analysis, speed] ); // ============================================================================ // ENHANCED MACHINE ANALYSIS // ============================================================================ const analyzeMachine = useCallback( (machine: MachineStatus): MachineAnalysis => { const errorCount = errorCountsRef.current[machine.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(machine.idleTime || 0, machine.activeTime || 0, machine.modelUuid, errorCount); const materialFlow = calculateMaterialFlowMetricsForAsset(machine.modelUuid); const cyclesCompleted = completedActionsRef.current[machine.modelUuid] || 0; const partsProcessed = completedActionsRef.current[`${machine.modelUuid}_parts`] || cyclesCompleted; const defects = errorCountsRef.current[`${machine.modelUuid}_defects`] || 0; const qualityMetrics = calculateQualityMetrics(machine.modelUuid, partsProcessed, defects); // Performance calculations const targetProcessTime = machine.point?.action?.processTime || 30; const actualProcessTime = cyclesCompleted > 0 ? timeMetrics.totalTime / cyclesCompleted : 0; const idealThroughput = (3600 / targetProcessTime) * 1; // 1 part per process const actualThroughput = materialFlow.throughput * 3600; const performance = calculatePerformanceMetrics(partsProcessed, idealThroughput * (timeMetrics.totalTime / 3600), timeMetrics.totalTime, targetProcessTime * cyclesCompleted, "machine"); const costMetrics = calculateCostMetrics(machine.modelUuid, "machine", machine.activeTime || 0, partsProcessed); const energyMetrics = calculateEnergyMetrics("machine", machine.activeTime || 0); // Quality calculations const totalParts = partsProcessed + defects; const defectRate = totalParts > 0 ? (defects / totalParts) * 100 : 0; const reworkRate = defectRate * 0.3; // Assume 30% of defects are reworked const scrapRate = defectRate * 0.7; // Assume 70% are scrapped // Update historical data const timestamp = new Date().toISOString(); const currentData = historicalDataRef.current[machine.modelUuid] || []; historicalDataRef.current[machine.modelUuid] = [ ...currentData, { timestamp, processTime: actualProcessTime, partsProcessed, isActive: machine.isActive, state: machine.state, defectRate, performance: performance.performanceRate, }, ].slice(-100); return { assetId: machine.modelUuid, assetName: machine.modelName, assetType: "machine", currentStatus: { isActive: machine.isActive, state: machine.state, currentAction: machine.currentAction || null, }, timeMetrics: { ...timeMetrics, idleTime: machine.idleTime || 0, activeTime: machine.activeTime || 0, totalTime: timeMetrics.totalTime, averageProcessTime: actualProcessTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, throughput: { itemsPerHour: actualThroughput, itemsPerDay: actualThroughput * 24, materialFlowRate: partsProcessed > 0 ? partsProcessed / (timeMetrics.totalTime / 60) : 0, capacityUtilization: timeMetrics.utilizationRate * 100, cyclesCompleted, partsProcessed, throughputEfficiency: (actualThroughput / Math.max(1, idealThroughput)) * 100, wip: materialFlow.wip, bottleneckIndex: timeMetrics.utilizationRate > 0.85 ? 1 : 0, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performance.performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performance.performanceRate, quality: qualityMetrics.firstPassYield, targetVsActual: performance.timeEfficiency, }, quality: { ...qualityMetrics, defectRate, reworkRate, scrapRate, }, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, costPerUnit: partsProcessed > 0 ? costMetrics.totalCost / partsProcessed : 0, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerUnit: partsProcessed > 0 ? energyMetrics.energyConsumed / partsProcessed : 0, }, historicalData: historicalDataRef.current[machine.modelUuid] || [], }; }, [materials, analysis, speed] ); // ============================================================================ // ENHANCED STORAGE ANALYSIS // ============================================================================ const analyzeStorage = useCallback( (storage: StorageUnitStatus): StorageAnalysis => { const errorCount = errorCountsRef.current[storage.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(storage.idleTime || 0, storage.activeTime || 0, storage.modelUuid, errorCount); const currentLoad = storage.currentLoad || 0; const capacity = storage.storageCapacity || 1; const utilizationRate = (currentLoad / capacity) * 100; const storeOps = completedActionsRef.current[`${storage.modelUuid}_store`] || 0; const retrieveOps = completedActionsRef.current[`${storage.modelUuid}_retrieve`] || 0; const totalOps = storeOps + retrieveOps; const qualityMetrics = calculateQualityMetrics(storage.modelUuid, totalOps, errorCount); // Calculate turnover rate const turnoverRate = timeMetrics.totalTime > 0 ? (totalOps / timeMetrics.totalTime) * 3600 : 0; // Occupancy trends const timestamp = new Date().toISOString(); const currentData = historicalDataRef.current[storage.modelUuid] || []; historicalDataRef.current[storage.modelUuid] = [ ...currentData, { timestamp, currentLoad, utilizationRate, operation: storeOps > retrieveOps ? "store" : "retrieve", totalOps, state: storage.state, }, ].slice(-100); // Calculate peak occupancy from historical data const occupancyData = historicalDataRef.current[storage.modelUuid] || []; const peakOccupancy = occupancyData.length > 0 ? Math.max(...occupancyData.map((d) => d.utilizationRate)) : utilizationRate; const averageOccupancy = occupancyData.length > 0 ? occupancyData.reduce((sum, d) => sum + d.utilizationRate, 0) / occupancyData.length : utilizationRate; // Calculate occupancy trends for the last hour const hourAgo = Date.now() - 3600000; const recentOccupancy = occupancyData.filter((d) => new Date(d.timestamp).getTime() > hourAgo).map((d) => d.utilizationRate); const occupancyTrend = recentOccupancy.length > 1 ? ((recentOccupancy[recentOccupancy.length - 1] - recentOccupancy[0]) / recentOccupancy[0]) * 100 : 0; const costMetrics = calculateCostMetrics(storage.modelUuid, "storage", storage.activeTime || 0, totalOps); const energyMetrics = calculateEnergyMetrics("storage", storage.activeTime || 0); return { assetId: storage.modelUuid, assetName: storage.modelName, assetType: "storage", currentStatus: { isActive: storage.isActive, state: storage.state, currentLoad, storageCapacity: capacity, currentMaterials: storage.currentMaterials, }, timeMetrics: { ...timeMetrics, idleTime: storage.idleTime || 0, activeTime: storage.activeTime || 0, totalTime: timeMetrics.totalTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, capacityMetrics: { utilizationRate, averageOccupancy, peakOccupancy, turnoverRate, occupancyTrend, }, throughput: { itemsPerHour: turnoverRate, itemsPerDay: turnoverRate * 24, materialFlowRate: turnoverRate / 60, capacityUtilization: utilizationRate, storeOperations: storeOps, retrieveOperations: retrieveOps, totalOperations: totalOps, throughputEfficiency: (totalOps / Math.max(1, capacity * 24)) * 100, wip: currentLoad, bottleneckIndex: utilizationRate > 0.9 ? 1 : 0, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, utilizationRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: utilizationRate, quality: qualityMetrics.firstPassYield, spaceUtilization: utilizationRate, }, quality: qualityMetrics, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, costPerUnit: totalOps > 0 ? costMetrics.totalCost / totalOps : 0, costPerStorageHour: capacity > 0 ? costMetrics.totalCost / (capacity * (timeMetrics.totalTime / 3600)) : 0, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerUnit: totalOps > 0 ? energyMetrics.energyConsumed / totalOps : 0, }, occupancyTrends: occupancyData.slice(-100).map((d) => ({ timestamp: d.timestamp, occupancy: d.currentLoad, utilizationRate: d.utilizationRate, })), historicalData: historicalDataRef.current[storage.modelUuid] || [], }; }, [analysis, speed] ); // ============================================================================ // ENHANCED HUMAN ANALYSIS // ============================================================================ const analyzeHuman = useCallback( (human: HumanStatus): HumanAnalysis => { const errorCount = errorCountsRef.current[human.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(human.idleTime || 0, human.activeTime || 0, human.modelUuid, errorCount); const actionsCompleted = completedActionsRef.current[human.modelUuid] || 0; const distanceTraveled = human.distanceTraveled || 0; const currentLoad = human.currentLoad || 0; const loadCapacity = human.point?.actions?.[0]?.loadCapacity || 1; const loadUtilization = (currentLoad / loadCapacity) * 100; // Workload distribution calculations const workerActions = completedActionsRef.current[`${human.modelUuid}_worker`] || 0; const manufacturerActions = completedActionsRef.current[`${human.modelUuid}_manufacturer`] || 0; const operatorActions = completedActionsRef.current[`${human.modelUuid}_operator`] || 0; const assemblerActions = completedActionsRef.current[`${human.modelUuid}_assembler`] || 0; const workerTime = completedActionsRef.current[`${human.modelUuid}_worker_time`] || 0; const manufacturerTime = completedActionsRef.current[`${human.modelUuid}_manufacturer_time`] || 0; const operatorTime = completedActionsRef.current[`${human.modelUuid}_operator_time`] || 0; const assemblerTime = completedActionsRef.current[`${human.modelUuid}_assembler_time`] || 0; const totalActionTime = workerTime + manufacturerTime + operatorTime + assemblerTime; const workloadDistributionData = [ { actionType: "Worker", count: workerActions, totalTime: workerTime, percentage: totalActionTime > 0 ? (workerTime / totalActionTime) * 100 : 0 }, { actionType: "Manufacturer", count: manufacturerActions, totalTime: manufacturerTime, percentage: totalActionTime > 0 ? (manufacturerTime / totalActionTime) * 100 : 0 }, { actionType: "Operator", count: operatorActions, totalTime: operatorTime, percentage: totalActionTime > 0 ? (operatorTime / totalActionTime) * 100 : 0 }, { actionType: "Assembler", count: assemblerActions, totalTime: assemblerTime, percentage: totalActionTime > 0 ? (assemblerTime / totalActionTime) * 100 : 0 }, ].filter((w) => w.count > 0); const workloadDistribution = workloadDistributionData.map((d) => `${Math.round(d.percentage)}%`).join(" | "); // Performance calculations const idealActionsPerHour = 60; // 60 actions per hour ideal const actualActionsPerHour = timeMetrics.totalTime > 0 ? (actionsCompleted / timeMetrics.totalTime) * 3600 : 0; const performanceRate = Math.min((actualActionsPerHour / idealActionsPerHour) * 100, 100); const qualityMetrics = calculateQualityMetrics(human.modelUuid, actionsCompleted, errorCount); const costMetrics = calculateCostMetrics(human.modelUuid, "human", human.activeTime || 0, actionsCompleted); const energyMetrics = calculateEnergyMetrics("human", human.activeTime || 0); // Update historical data const timestamp = new Date().toISOString(); const newEntry = { timestamp, actionType: human.currentAction?.actionName || "unknown", duration: timeMetrics.totalTime, distanceTraveled, isActive: human.isActive, state: human.state, load: currentLoad, performance: performanceRate, }; const currentData = historicalDataRef.current[human.modelUuid] || []; historicalDataRef.current[human.modelUuid] = [...currentData, newEntry].slice(-100); return { assetId: human.modelUuid, assetName: human.modelName, assetType: "human", currentStatus: { isActive: human.isActive, isScheduled: human.isScheduled, currentPhase: human.currentPhase, state: human.state, speed: human.speed, currentLoad, currentMaterials: human.currentMaterials, currentAction: human.currentAction || null, loadUtilization, }, timeMetrics: { ...timeMetrics, idleTime: human.idleTime || 0, activeTime: human.activeTime || 0, totalTime: timeMetrics.totalTime, scheduledTime: timeMetrics.totalTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, productivityMetrics: { actionsCompleted, actionsPerHour: actualActionsPerHour, averageActionTime: actionsCompleted > 0 ? timeMetrics.totalTime / actionsCompleted : 0, distanceTraveled, averageSpeed: timeMetrics.totalTime > 0 ? distanceTraveled / timeMetrics.totalTime : 0, loadEfficiency: loadUtilization, }, workloadDistribution: workloadDistributionData, workloadSummary: workloadDistribution === "" ? "0%" : workloadDistribution, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performanceRate, quality: qualityMetrics.firstPassYield, laborProductivity: actualActionsPerHour, utilizationRate: timeMetrics.utilizationRate * 100, }, quality: qualityMetrics, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, costPerAction: actionsCompleted > 0 ? costMetrics.totalCost / actionsCompleted : 0, costPerHour: costMetrics.costPerHour, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerAction: actionsCompleted > 0 ? energyMetrics.energyConsumed / actionsCompleted : 0, }, historicalData: historicalDataRef.current[human.modelUuid] || [], }; }, [analysis, speed] ); // ============================================================================ // ENHANCED CRANE ANALYSIS // ============================================================================ const analyzeCrane = useCallback( (crane: CraneStatus): CraneAnalysis => { const errorCount = errorCountsRef.current[crane.modelUuid] || 0; const timeMetrics = calculateAdvancedTimeMetrics(crane.idleTime || 0, crane.activeTime || 0, crane.modelUuid, errorCount); const cyclesCompleted = completedActionsRef.current[crane.modelUuid] || 0; const loadsHandled = completedActionsRef.current[`${crane.modelUuid}_loads`] || 0; const totalLifts = completedActionsRef.current[`${crane.modelUuid}_lifts`] || 0; const totalLiftHeight = completedActionsRef.current[`${crane.modelUuid}_lift_height`] || 0; const qualityMetrics = calculateQualityMetrics(crane.modelUuid, loadsHandled, errorCount); // Performance calculations const idealCycleTime = 20; // 20 seconds ideal const actualCycleTime = cyclesCompleted > 0 ? timeMetrics.totalTime / cyclesCompleted : 0; const idealThroughput = (3600 / idealCycleTime) * 1; // 1 load per cycle const actualThroughput = cyclesCompleted > 0 ? (loadsHandled / timeMetrics.totalTime) * 3600 : 0; const performance = calculatePerformanceMetrics(loadsHandled, idealThroughput * (timeMetrics.totalTime / 3600), timeMetrics.totalTime, idealCycleTime * cyclesCompleted, "crane"); // Lift metrics const avgLiftHeight = totalLifts > 0 ? totalLiftHeight / totalLifts : 0; const avgLoadsPerCycle = cyclesCompleted > 0 ? loadsHandled / cyclesCompleted : 0; // Success rates const liftAttempts = totalLifts + errorCount; const liftSuccessRate = liftAttempts > 0 ? (totalLifts / liftAttempts) * 100 : 0; const positioningAccuracy = totalLifts > 0 ? liftSuccessRate * 1.0 : 0; // Slightly lower than success rate // Load utilization const maxPickUpCount = crane.point?.actions?.[0]?.maxPickUpCount || 1; const loadUtilization = (avgLoadsPerCycle / maxPickUpCount) * 100; const costMetrics = calculateCostMetrics(crane.modelUuid, "crane", crane.activeTime || 0, loadsHandled); const energyMetrics = calculateEnergyMetrics("crane", crane.activeTime || 0); // Movement efficiency calculation const movementEfficiency = liftSuccessRate * 0.8 + positioningAccuracy * 0.2; // Weighted average // Update historical data const timestamp = new Date().toISOString(); const newEntry = { timestamp, cycleTime: actualCycleTime, loadsHandled, isActive: crane.isActive, state: crane.state, isCarrying: crane.isCarrying, loadUtilization, performance: performance.performanceRate, }; const currentData = historicalDataRef.current[crane.modelUuid] || []; historicalDataRef.current[crane.modelUuid] = [...currentData, newEntry].slice(-100); return { assetId: crane.modelUuid, assetName: crane.modelName, assetType: "crane", currentStatus: { isActive: crane.isActive, isScheduled: crane.isScheduled, isCarrying: crane.isCarrying, currentPhase: crane.currentPhase, state: crane.state, currentLoad: crane.currentLoad, currentMaterials: crane.currentMaterials, currentAction: crane.currentAction || null, loadUtilization, }, timeMetrics: { ...timeMetrics, idleTime: crane.idleTime || 0, activeTime: crane.activeTime || 0, totalTime: timeMetrics.totalTime, averageCycleTime: actualCycleTime, availability: timeMetrics.uptime, reliability: timeMetrics.reliability, }, throughput: { itemsPerHour: actualThroughput, itemsPerDay: actualThroughput * 24, materialFlowRate: loadsHandled > 0 ? loadsHandled / (timeMetrics.totalTime / 60) : 0, capacityUtilization: timeMetrics.utilizationRate * 100, cyclesCompleted, loadsHandled, averageLoadsPerCycle: avgLoadsPerCycle, throughputEfficiency: (actualThroughput / Math.max(1, idealThroughput)) * 100, wip: crane.currentLoad, bottleneckIndex: timeMetrics.utilizationRate > 0.85 ? 1 : 0, }, movementMetrics: { totalLifts, averageLiftHeight: avgLiftHeight, movementEfficiency, loadEfficiency: loadUtilization, cycleEfficiency: performance.timeEfficiency, }, efficiency: { overallEffectiveness: calculateOEE(timeMetrics.uptime, performance.performanceRate, qualityMetrics.firstPassYield), availability: timeMetrics.uptime, performance: performance.performanceRate, quality: qualityMetrics.firstPassYield, loadUtilization, cycleTimeEfficiency: performance.timeEfficiency, }, quality: { ...qualityMetrics, liftSuccessRate, positioningAccuracy, }, // Add cost metrics costMetrics: { operatingCost: costMetrics.operatingCost, maintenanceCost: costMetrics.maintenanceCost, energyCost: energyMetrics.energyCost, totalCost: costMetrics.totalCost, roi: costMetrics.roi, valueAdded: costMetrics.valueAdded, costPerLift: totalLifts > 0 ? costMetrics.totalCost / totalLifts : 0, costPerCycle: cyclesCompleted > 0 ? costMetrics.totalCost / cyclesCompleted : 0, }, // Add energy metrics energyMetrics: { energyConsumed: energyMetrics.energyConsumed, energyEfficiency: energyMetrics.energyEfficiency, carbonFootprint: energyMetrics.carbonFootprint, powerUsage: energyMetrics.powerUsage, energyCost: energyMetrics.energyCost, energyPerLift: totalLifts > 0 ? energyMetrics.energyConsumed / totalLifts : 0, }, historicalData: historicalDataRef.current[crane.modelUuid] || [], }; }, [analysis, speed] ); // ============================================================================ // COMPREHENSIVE SYSTEM-WIDE ANALYSIS // ============================================================================ const analyzeSystem = useCallback( (allAssets: AssetAnalysis[]): AnalysisSchema => { // System-wide calculations const totalAssets = allAssets.length; const activeAssets = allAssets.filter((a) => a.currentStatus.isActive).length; const assetsInError = allAssets.filter((a) => a.currentStatus.state === "error").length; const assetsIdle = allAssets.filter((a) => !a.currentStatus.isActive && a.currentStatus.state === "idle").length; // Calculate system OEE (weighted average by throughput) const totalThroughput = allAssets.reduce((sum, asset) => { if ("throughput" in asset) { return sum + asset.throughput.itemsPerHour; } else if ("productivityMetrics" in asset) { return sum + asset.productivityMetrics.actionsPerHour; } return sum; }, 0); const weightedOEE = allAssets.reduce((sum, asset) => { const weight = ("throughput" in asset ? asset.throughput.itemsPerHour : "productivityMetrics" in asset ? asset.productivityMetrics.actionsPerHour : 1) / Math.max(1, totalThroughput); return sum + asset.efficiency.overallEffectiveness * weight; }, 0); // Calculate system utilization (average of all assets) const avgUtilization = allAssets.reduce((sum, a) => { if ("timeMetrics" in a) { return sum + a.timeMetrics.utilizationRate; } return sum; }, 0) / totalAssets; // Calculate total idle and active time const totalIdleTime = allAssets.reduce((sum, a) => { if ("timeMetrics" in a) { return sum + a.timeMetrics.idleTime; } return sum; }, 0); const totalActiveTime = allAssets.reduce((sum, a) => { if ("timeMetrics" in a) { return sum + a.timeMetrics.activeTime; } return sum; }, 0); const totalSystemTime = totalIdleTime + totalActiveTime; const systemUptime = totalSystemTime > 0 ? (totalActiveTime / totalSystemTime) * 100 : 0; // Calculate total costs and value const totalCost = allAssets.reduce((sum, a) => { if ("costMetrics" in a) { return sum + a.costMetrics.totalCost; } return sum; }, 0); const totalValueAdded = allAssets.reduce((sum, a) => { if ("costMetrics" in a) { return sum + a.costMetrics.valueAdded; } return sum; }, 0); const totalEnergyConsumed = allAssets.reduce((sum, a) => { if ("energyMetrics" in a) { return sum + a.energyMetrics.energyConsumed; } return sum; }, 0); // Group by asset type for detailed analysis const assetTypeGroups = allAssets.reduce((acc, asset) => { if (!acc[asset.assetType]) { acc[asset.assetType] = []; } acc[asset.assetType].push(asset); return acc; }, {} as Record); const assetTypePerformance = Object.entries(assetTypeGroups).map(([type, assets]) => { const typeThroughput = assets.reduce((sum, a) => { if ("throughput" in a) { return sum + a.throughput.itemsPerHour; } else if ("productivityMetrics" in a) { return sum + a.productivityMetrics.actionsPerHour; } return sum; }, 0); const typeOEE = assets.reduce((sum, a) => sum + a.efficiency.overallEffectiveness, 0) / assets.length; const typeUtilization = assets.reduce((sum, a) => { if ("timeMetrics" in a) { return sum + a.timeMetrics.utilizationRate; } return sum; }, 0) / assets.length; const typeCost = assets.reduce((sum, a) => { if ("costMetrics" in a) { return sum + a.costMetrics.totalCost; } return sum; }, 0); return { assetType: type, count: assets.length, averageOEE: typeOEE, averageUtilization: typeUtilization * 100, totalThroughput: typeThroughput, totalCost: typeCost, throughputPerAsset: typeThroughput / assets.length, costPerAsset: typeCost / assets.length, }; }); // Material Flow Analysis const totalMaterialsInSystem = materials.filter((m) => m.isActive).length; const completedMaterials = materialHistoryRef.current.length; const averageResidenceTime = materialHistoryRef.current.length > 0 ? materialHistoryRef.current.reduce((sum, entry) => { const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0); return sum + (residenceTime || 0); }, 0) / materialHistoryRef.current.length : 0; // Bottleneck Identification const bottlenecks = allAssets .map((asset) => { let utilizationRate = 0; let queueLength = 0; if ("timeMetrics" in asset) { utilizationRate = asset.timeMetrics.utilizationRate; } if ("currentStatus" in asset && "queueLength" in asset.currentStatus) { queueLength = asset.currentStatus.queueLength as number; } const impactScore = utilizationRate * 100 + queueLength * 10; let severity: "critical" | "high" | "medium" | "low" = "low"; if (utilizationRate > 0.95) severity = "critical"; else if (utilizationRate > 0.9) severity = "high"; else if (utilizationRate > 0.85) severity = "medium"; return { assetId: asset.assetId, assetName: asset.assetName, severity, utilizationRate: utilizationRate * 100, queueLength, impactScore, }; }) .filter((b) => b.utilizationRate > 80 || b.queueLength > 5) .sort((a, b) => b.impactScore - a.impactScore); // Queue Analysis const queueLengths = allAssets .filter((asset) => "currentStatus" in asset && "queueLength" in asset.currentStatus) .map((asset) => ({ assetId: asset.assetId, assetName: asset.assetName, queueLength: (asset.currentStatus as any).queueLength || 0, averageWaitTime: 0, // Could be calculated with more data })) .filter((q) => q.queueLength > 0); // Flow Continuity Analysis const throughputOverTime = Object.values(historicalDataRef.current) .flat() .filter((d) => d.timestamp) .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); const flowRates = throughputOverTime.map((d) => d.throughput || d.actionsPerHour || 0); const meanFlowRate = flowRates.length > 0 ? flowRates.reduce((sum, rate) => sum + rate, 0) / flowRates.length : 0; const variance = flowRates.length > 0 ? flowRates.reduce((sum, rate) => sum + Math.pow(rate - meanFlowRate, 2), 0) / flowRates.length : 0; const varianceCoefficient = meanFlowRate > 0 ? Math.sqrt(variance) / meanFlowRate : 0; // Predictive Insights const maintenanceAlerts = allAssets .filter((asset) => { const errorCount = errorCountsRef.current[asset.assetId] || 0; const uptime = "timeMetrics" in asset ? asset.timeMetrics.uptime : 100; return uptime < 95 || errorCount > 3; }) .map((asset) => ({ assetId: asset.assetId, assetName: asset.assetName, assetType: asset.assetType, alertType: (errorCountsRef.current[asset.assetId] || 0) > 5 ? ("critical" as const) : (errorCountsRef.current[asset.assetId] || 0) > 3 ? ("predictive" as const) : ("preventive" as const), estimatedTimeToFailure: Math.max(0, 100 - ((asset.timeMetrics as any)?.reliability || 100)) * 1000, confidence: Math.min(90 + (errorCountsRef.current[asset.assetId] || 0) * 2, 100), recommendation: (errorCountsRef.current[asset.assetId] || 0) > 5 ? "Immediate maintenance required" : "Schedule preventive maintenance", })); // Optimization Opportunities const optimizationOpportunities = [ ...bottlenecks .filter((b) => b.severity === "critical" || b.severity === "high") .map((b) => ({ area: "throughput" as const, description: `High utilization bottleneck at ${b.assetName} (${b.utilizationRate.toFixed(1)}% utilization)`, potentialImprovement: Math.min(100 - b.utilizationRate, 20), priority: b.severity === "critical" ? ("high" as const) : ("medium" as const), })), { area: "efficiency" as const, description: `System OEE at ${weightedOEE.toFixed(1)}% - target improvements available`, potentialImprovement: Math.max(0, 85 - weightedOEE), priority: weightedOEE < 80 ? ("high" as const) : ("medium" as const), }, { area: "cost" as const, description: `Total system cost: $${totalCost.toFixed(2)}`, potentialImprovement: Math.min(20, totalValueAdded > 0 ? (totalCost / totalValueAdded) * 100 : 0), priority: totalValueAdded < totalCost ? ("high" as const) : ("medium" as const), }, ]; // Trend Analysis const trendAnalysis = assetTypePerformance.map((type) => { const historicalTypeData = allAssets .filter((a) => a.assetType === type.assetType) .flatMap((a) => historicalDataRef.current[a.assetId] || []) .filter((d) => d.performance !== undefined); const recentPerformance = historicalTypeData.slice(-10).map((d) => d.performance); const olderPerformance = historicalTypeData.slice(-20, -10).map((d) => d.performance); const recentAvg = recentPerformance.length > 0 ? recentPerformance.reduce((sum, p) => sum + p, 0) / recentPerformance.length : 0; const olderAvg = olderPerformance.length > 0 ? olderPerformance.reduce((sum, p) => sum + p, 0) / olderPerformance.length : 0; let trend: "increasing" | "decreasing" | "stable" = "stable"; let changeRate = 0; if (olderAvg > 0 && recentAvg > 0) { changeRate = ((recentAvg - olderAvg) / olderAvg) * 100; if (Math.abs(changeRate) > 5) { trend = changeRate > 0 ? "increasing" : "decreasing"; } } return { metric: `${type.assetType} Performance`, trend, changeRate, forecast: [recentAvg * 1.02, recentAvg * 1.01, recentAvg], // Simple forecast }; }); return { assets: allAssets, systemPerformance: { overallOEE: weightedOEE, systemThroughput: totalThroughput, systemUtilization: avgUtilization * 100, assetTypePerformance, criticalMetrics: { activeAssets, totalAssets, assetsInError, assetsIdle, averageIdleTime: totalIdleTime / totalAssets, totalDowntime: totalIdleTime, systemUptime, }, }, materialFlow: { totalMaterialsInSystem, materialsCompleted: completedMaterials, averageResidenceTime, queueLengths, bottlenecks: bottlenecks.slice(0, 10), // Top 10 bottlenecks flowContinuity: { overallFlowRate: meanFlowRate, varianceCoefficient, steadyStateDeviation: varianceCoefficient > 0.3 ? varianceCoefficient * 100 : 0, }, }, predictiveInsights: { maintenanceAlerts: maintenanceAlerts.slice(0, 5), // Top 5 alerts optimizationOpportunities: optimizationOpportunities.slice(0, 5), // Top 5 opportunities trendAnalysis: trendAnalysis.filter((t) => t.trend !== "stable" || Math.abs(t.changeRate) > 1), }, analysisTimeRange: { startTime: startTimeRef.current, endTime: new Date().toISOString(), duration: Date.now() - new Date(startTimeRef.current).getTime(), }, metadata: { lastUpdated: new Date().toISOString(), dataPoints: allAssets.length + materials.length + materialHistoryRef.current.length, analysisVersion: "1.0.0", totalCost, totalValueAdded, totalEnergyConsumed, energyCost: totalEnergyConsumed * 0.12, // $0.12 per kWh carbonFootprint: totalEnergyConsumed * 0.5, // 0.5 kg CO2 per kWh }, }; }, [materials] ); // ============================================================================ // MAIN ANALYSIS FUNCTION // ============================================================================ const performAnalysis = useCallback(() => { setAnalyzing(true); try { const allAssets: AssetAnalysis[] = []; // Analyze all conveyors conveyors.forEach((conveyor) => { allAssets.push(analyzeConveyor(conveyor)); }); // Analyze all vehicles vehicles.forEach((vehicle) => { allAssets.push(analyzeVehicle(vehicle)); }); // Analyze all robotic arms armBots.forEach((armBot) => { allAssets.push(analyzeRoboticArm(armBot)); }); // Analyze all machines machines.forEach((machine) => { allAssets.push(analyzeMachine(machine)); }); // Analyze all storage units storageUnits.forEach((storage) => { allAssets.push(analyzeStorage(storage)); }); // Analyze all humans humans.forEach((human) => { allAssets.push(analyzeHuman(human)); }); // Analyze all cranes cranes.forEach((crane) => { allAssets.push(analyzeCrane(crane)); }); // Perform system-wide analysis const completeAnalysis = analyzeSystem(allAssets); setAnalysis(completeAnalysis); } catch (error) { console.error("Analysis error:", error); } finally { setAnalyzing(false); } }, [ conveyors, vehicles, armBots, machines, humans, cranes, analyzeConveyor, analyzeVehicle, analyzeRoboticArm, analyzeMachine, analyzeHuman, analyzeCrane, analyzeSystem, setAnalysis, setAnalyzing, ]); // ============================================================================ // EFFECTS // ============================================================================ const performAnalysisRef = useRef(performAnalysis); useEffect(() => { performAnalysisRef.current = performAnalysis; }, [performAnalysis]); // Trigger analysis when assets or materials change useEffect(() => { if (!isPlaying) return; // Perform analysis when any asset or material state changes performAnalysisRef.current(); }, [conveyors, vehicles, armBots, machines, humans, cranes, materials, isPlaying]); // Perform initial analysis and set up interval useEffect(() => { if (!isPlaying) return; // Set up periodic analysis (every 1 second) analysisIntervalRef.current = setInterval(() => { performAnalysisRef.current(); }, 1000); return () => { if (analysisIntervalRef.current) { clearInterval(analysisIntervalRef.current); } }; }, [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, currentAction: a.currentAction })), ...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, asset.type === "roboticArm" ? { actionName: (asset as any).currentAction?.actionName } : undefined); } // 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]); // Monitor ArmBot action changes to track cycles useEffect(() => { if (!isPlaying) return; armBots.forEach((armBot) => { const previousActionUuid = previousArmBotActionsRef.current[armBot.modelUuid]; const currentActionUuid = armBot.currentAction?.actionUuid; // Check if action completed (transition from an action to no action or different action) if (previousActionUuid && previousActionUuid !== currentActionUuid) { // Action completed if (!completedActionsRef.current[armBot.modelUuid]) { completedActionsRef.current[armBot.modelUuid] = 0; } completedActionsRef.current[armBot.modelUuid]++; // Also update pick and place count which is used in analysis if (!completedActionsRef.current[`${armBot.modelUuid}_pickplace`]) { completedActionsRef.current[`${armBot.modelUuid}_pickplace`] = 0; } completedActionsRef.current[`${armBot.modelUuid}_pickplace`]++; // Granular pick/place tracking if (previousActionUuid) { // We need to look up what action this UUID corresponded to // Since we don't store the action map history, we check the current config // This assumes configuration hasn't changed, which is true for runtime const action = armBot.point.actions.find((a) => a.actionUuid === previousActionUuid); if (action) { const actionName = action.actionName.toLowerCase(); if (actionName.includes("pick")) { if (!completedActionsRef.current[`${armBot.modelUuid}_pick`]) completedActionsRef.current[`${armBot.modelUuid}_pick`] = 0; completedActionsRef.current[`${armBot.modelUuid}_pick`]++; } else if (actionName.includes("place")) { if (!completedActionsRef.current[`${armBot.modelUuid}_place`]) completedActionsRef.current[`${armBot.modelUuid}_place`] = 0; completedActionsRef.current[`${armBot.modelUuid}_place`]++; } } } } previousArmBotActionsRef.current[armBot.modelUuid] = currentActionUuid; }); }, [armBots, isPlaying]); // Monitor Machine action changes to track cycles useEffect(() => { if (!isPlaying) return; machines.forEach((machine) => { const previousActionUuid = previousMachineActionsRef.current[machine.modelUuid]; const currentActionUuid = machine.currentAction?.actionUuid; // Check if action completed (transition from an action to no action or different action) if (previousActionUuid && previousActionUuid !== currentActionUuid) { // Action completed - increment cycles if (!completedActionsRef.current[machine.modelUuid]) { completedActionsRef.current[machine.modelUuid] = 0; } completedActionsRef.current[machine.modelUuid]++; // Also update parts processed (assume 1 part per cycle) if (!completedActionsRef.current[`${machine.modelUuid}_parts`]) { completedActionsRef.current[`${machine.modelUuid}_parts`] = 0; } completedActionsRef.current[`${machine.modelUuid}_parts`]++; } previousMachineActionsRef.current[machine.modelUuid] = currentActionUuid; }); }, [machines, isPlaying]); // Monitor Human action changes from EventManager useEffect(() => { if (!isPlaying || !humanEventManagerRef.current) return; const interval = setInterval(() => { if (!humanEventManagerRef.current) return; humanEventManagerRef.current.humanStates.forEach((humanState) => { const humanId = humanState.humanId; // Initialize tracking for this human if needed if (!previousHumanCountsRef.current[humanId]) { previousHumanCountsRef.current[humanId] = {}; } humanState.actionQueue.forEach((action) => { let lastCount = previousHumanCountsRef.current[humanId][action.actionUuid] || 0; const currentCount = action.count || 0; // Handle reset case (new action instance with same UUID) if (currentCount < lastCount) { lastCount = 0; previousHumanCountsRef.current[humanId][action.actionUuid] = 0; } const delta = currentCount - lastCount; if (delta > 0) { // Update total completions for this human if (!completedActionsRef.current[humanId]) completedActionsRef.current[humanId] = 0; completedActionsRef.current[humanId] += delta; // Update granular action type completions (e.g., worker, manufacturer) const typeKey = `${humanId}_${action.actionType}`; if (!completedActionsRef.current[typeKey]) completedActionsRef.current[typeKey] = 0; completedActionsRef.current[typeKey] += delta; // Update the last known count previousHumanCountsRef.current[humanId][action.actionUuid] = currentCount; } }); }); }, 100); return () => clearInterval(interval); }, [isPlaying, humanEventManagerRef]); // 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]); // Monitor Vehicle phase changes to track completed trips useEffect(() => { if (!isPlaying) return; vehicles.forEach((vehicle) => { const previousPhase = previousVehiclePhasesRef.current[vehicle.modelUuid]; const currentPhase = vehicle.currentPhase; // Check for transition from 'drop-pickup' to 'picking' (Trip completed) if (previousPhase === "drop-pickup" && currentPhase === "picking") { if (!completedActionsRef.current[vehicle.modelUuid]) { completedActionsRef.current[vehicle.modelUuid] = 0; } completedActionsRef.current[vehicle.modelUuid]++; // Track loads delivered (assuming 1 load per trip for now, or use vehicle.currentLoad if available/reliable at this point) if (!completedActionsRef.current[`${vehicle.modelUuid}_loads`]) { completedActionsRef.current[`${vehicle.modelUuid}_loads`] = 0; } completedActionsRef.current[`${vehicle.modelUuid}_loads`] += 1; } previousVehiclePhasesRef.current[vehicle.modelUuid] = currentPhase; }); }, [vehicles, isPlaying]); // Monitor Crane phase changes to track completed cycles, loads, and lifts useEffect(() => { if (!isPlaying) return; cranes.forEach((crane) => { const previousPhase = previousCranePhasesRef.current[crane.modelUuid]; const currentPhase = crane.currentPhase; // Track lifts (picking phase indicates a lift operation) if (previousPhase !== "picking" && currentPhase === "picking") { if (!completedActionsRef.current[`${crane.modelUuid}_lifts`]) { completedActionsRef.current[`${crane.modelUuid}_lifts`] = 0; } completedActionsRef.current[`${crane.modelUuid}_lifts`]++; // Track lift height (assuming a default lift height of 5 meters for now) // In a real scenario, this would be calculated from crane constraints if (!completedActionsRef.current[`${crane.modelUuid}_lift_height`]) { completedActionsRef.current[`${crane.modelUuid}_lift_height`] = 0; } completedActionsRef.current[`${crane.modelUuid}_lift_height`] += 5; // Track loads handled when picking (each pick is a load) if (!completedActionsRef.current[`${crane.modelUuid}_loads`]) { completedActionsRef.current[`${crane.modelUuid}_loads`] = 0; } completedActionsRef.current[`${crane.modelUuid}_loads`]++; } // Track cycles completed when dropping is done (transition from 'dropping' to any other phase) if (previousPhase === "dropping" && currentPhase !== "dropping") { // Increment cycles completed if (!completedActionsRef.current[crane.modelUuid]) { completedActionsRef.current[crane.modelUuid] = 0; } completedActionsRef.current[crane.modelUuid]++; } previousCranePhasesRef.current[crane.modelUuid] = currentPhase; }); }, [cranes, isPlaying]); return null; } export default Analyzer;