2571 lines
110 KiB
TypeScript
2571 lines
110 KiB
TypeScript
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<Record<string, any[]>>({});
|
|
const materialHistoryRef = useRef<MaterialHistoryEntry[]>([]);
|
|
const queueLengthsRef = useRef<Record<string, { timestamp: number; length: number }[]>>({});
|
|
|
|
// Timing and intervals
|
|
const analysisIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const startTimeRef = useRef<string>(new Date().toISOString());
|
|
|
|
// Error and action tracking
|
|
const errorCountsRef = useRef<Record<string, number>>({});
|
|
const completedActionsRef = useRef<Record<string, number>>({});
|
|
const stateTransitionsRef = useRef<Record<string, any[]>>({});
|
|
|
|
// Material flow tracking - tracks materials added/removed per asset
|
|
const materialAdditionsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
materialId: string;
|
|
materialType: string;
|
|
timestamp: number;
|
|
fromAsset?: string;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
const materialRemovalsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
materialId: string;
|
|
materialType: string;
|
|
timestamp: number;
|
|
toAsset?: string;
|
|
processingTime?: number;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Asset state change tracking with detailed timestamps
|
|
const assetStateChangesRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
fromState: string;
|
|
toState: string;
|
|
timestamp: number;
|
|
duration?: number;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Cycle tracking for each asset
|
|
const assetCyclesRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
cycleId: string;
|
|
startTime: number;
|
|
endTime?: number;
|
|
cycleType: string; // 'processing', 'transport', 'pick-place', etc.
|
|
materialsInvolved: string[];
|
|
success: boolean;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Action completion times for performance analysis
|
|
const actionCompletionTimesRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
actionId: string;
|
|
actionType: string;
|
|
startTime: number;
|
|
endTime: number;
|
|
duration: number;
|
|
success: boolean;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Material processing times per asset
|
|
const materialProcessingTimesRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
materialId: string;
|
|
entryTime: number;
|
|
exitTime?: number;
|
|
processingDuration?: number;
|
|
waitTime?: number;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// WIP (Work In Progress) tracking per asset over time
|
|
const wipSnapshotsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
timestamp: number;
|
|
wipCount: number;
|
|
materialIds: string[];
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Throughput snapshots for trend analysis
|
|
const throughputSnapshotsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
timestamp: number;
|
|
itemsProcessed: number;
|
|
timeWindow: number; // in seconds
|
|
rate: number; // items per hour
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Asset performance snapshots
|
|
const performanceSnapshotsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
timestamp: number;
|
|
utilization: number;
|
|
efficiency: number;
|
|
quality: number;
|
|
oee: number;
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Bottleneck detection tracking
|
|
const bottleneckEventsRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
timestamp: number;
|
|
queueLength: number;
|
|
utilizationRate: number;
|
|
waitingMaterials: string[];
|
|
}[]
|
|
>
|
|
>({});
|
|
|
|
// Previous state tracking for delta calculations
|
|
const previousAssetStatesRef = useRef<
|
|
Record<
|
|
string,
|
|
{
|
|
state: string;
|
|
isActive: boolean;
|
|
materialCount: number;
|
|
timestamp: number;
|
|
}
|
|
>
|
|
>({});
|
|
|
|
// Track previous actions for ArmBots to detect cycle completion
|
|
const previousArmBotActionsRef = useRef<Record<string, string | undefined>>({});
|
|
|
|
// Track previous actions for Machines to detect cycle completion
|
|
const previousMachineActionsRef = useRef<Record<string, string | undefined>>({});
|
|
|
|
// Track previous action counts for Humans to detect completion from EventManager
|
|
const previousHumanCountsRef = useRef<Record<string, Record<string, number>>>({});
|
|
|
|
// Track previous vehicle phases to detect trip completion
|
|
const previousVehiclePhasesRef = useRef<Record<string, string>>({});
|
|
|
|
// Track previous crane phases to detect cycle completion
|
|
const previousCranePhasesRef = useRef<Record<string, string>>({});
|
|
|
|
// 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<string, any>);
|
|
|
|
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<string, AssetAnalysis[]>);
|
|
|
|
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;
|