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