using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Haoliang.Core.Services; using Haoliang.Data.Repositories; using Haoliang.Models.Device; using Haoliang.Models.Production; using Haoliang.Models.System; using Haoliang.Models.DataCollection; namespace Haoliang.Core.Services { public class ProductionStatisticsService : IProductionStatisticsService { private readonly IProductionRepository _productionRepository; private readonly IDeviceRepository _deviceRepository; private readonly ISystemRepository _systemRepository; private readonly IAlarmRepository _alarmRepository; private readonly ICollectionRepository _collectionRepository; public ProductionStatisticsService( IProductionRepository productionRepository, IDeviceRepository deviceRepository, ISystemRepository systemRepository, IAlarmRepository alarmRepository, ICollectionRepository collectionRepository) { _productionRepository = productionRepository; _deviceRepository = deviceRepository; _systemRepository = systemRepository; _alarmRepository = alarmRepository; _collectionRepository = collectionRepository; } public async Task CalculateProductionTrendsAsync(int deviceId, DateTime startDate, DateTime endDate) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) throw new KeyNotFoundException($"Device {deviceId} not found"); var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync( deviceId, startDate, endDate); var dailyData = new List(); decimal totalProduction = 0; decimal sumSquaredDifferences = 0; decimal mean = 0; int dayCount = 0; // Group records by date var groupedByDate = productionRecords.GroupBy(r => r.Created.Date); foreach (var dateGroup in groupedByDate) { var date = dateGroup.Key; var dayRecords = dateGroup.ToList(); var dayProduction = dayRecords.Sum(r => r.Quantity); // Get target for this date (if available) var target = await GetProductionTargetAsync(deviceId, date); dailyData.Add(new DailyProduction { Date = date, Quantity = dayProduction, Target = target, Efficiency = target > 0 ? (dayProduction / target) * 100 : 0, Records = dayRecords }); totalProduction += dayProduction; dayCount++; } if (dayCount > 0) { mean = totalProduction / dayCount; // Calculate variance foreach (var day in dailyData) { sumSquaredDifferences += (decimal)Math.Pow((double)(day.Quantity - mean), 2); } } decimal productionVariance = dayCount > 0 ? (decimal)Math.Sqrt((double)(sumSquaredDifferences / dayCount)) : 0; // Calculate trend using linear regression var trendCoefficient = CalculateLinearRegressionTrend(dailyData); var trendDirection = DetermineTrendDirection(trendCoefficient); return new ProductionTrendAnalysis { DeviceId = deviceId, DeviceName = device.Name, PeriodStart = startDate, PeriodEnd = endDate, TotalProduction = totalProduction, AverageDailyProduction = mean, ProductionVariance = productionVariance, TrendCoefficient = trendCoefficient, TrendDirection = trendDirection, DailyData = dailyData }; } public async Task GenerateProductionReportAsync(ReportFilter filter) { var summaryItems = new List(); var metadata = new ReportMetadata(); // Get production records based on filter var records = await _productionRepository.GetProductionRecordsByFilterAsync(filter); // Group by device and program var groupedData = records.GroupBy(r => new { r.DeviceId, r.ProgramName }); foreach (var group in groupedData) { var deviceId = group.Key.DeviceId; var programName = group.Key.ProgramName; var device = await _deviceRepository.GetByIdAsync(deviceId); var totalQuantity = group.Sum(r => r.Quantity); var totalTarget = group.Sum(r => r.TargetQuantity); // Calculate quality rate var qualityRate = await CalculateQualityRateAsync(deviceId, programName, filter.StartDate, filter.EndDate); // Calculate runtime and downtime var runtime = CalculateRuntime(group.ToList()); var downtime = await CalculateDowntimeAsync(deviceId, filter.StartDate, filter.EndDate); summaryItems.Add(new ProductionSummaryItem { DeviceId = deviceId, DeviceName = device?.Name ?? "Unknown", ProgramName = programName, Quantity = totalQuantity, TargetQuantity = totalTarget, Efficiency = totalTarget > 0 ? (totalQuantity / totalTarget) * 100 : 0, QualityRate = qualityRate, Runtime = runtime, Downtime = downtime }); } metadata = CalculateReportMetadata(summaryItems, filter); return new ProductionReport { ReportDate = DateTime.Now, ReportType = filter.ReportType, SummaryItems = summaryItems, Metadata = metadata }; } public async Task CalculateEfficiencyMetricsAsync(EfficiencyFilter filter) { var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds : (await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList(); var metrics = new List(); var hourlyData = new List(); foreach (var deviceId in deviceIds) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) continue; var efficiencyMetrics = await CalculateDeviceEfficiencyAsync(deviceId, filter); metrics.Add(efficiencyMetrics); // Add hourly data if requested if (filter.GroupBy == GroupBy.Hour) { hourlyData.AddRange(efficiencyMetrics.HourlyData); } } // Calculate aggregate metrics if multiple devices var aggregatedMetrics = AggregateEfficiencyMetrics(metrics); return aggregatedMetrics; } public async Task PerformQualityAnalysisAsync(QualityFilter filter) { var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds : (await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList(); var totalProduced = 0m; var totalGood = 0m; var qualityMetrics = new List(); var defectAnalysis = new List(); foreach (var deviceId in deviceIds) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) continue; // Get production records var records = await _productionRepository.GetProductionRecordsByFilterAsync(new ReportFilter { DeviceIds = new List { deviceId }, StartDate = filter.StartDate, EndDate = filter.EndDate, ProgramNames = filter.ProgramNames }); var deviceTotalProduced = records.Sum(r => r.Quantity); var deviceTotalGood = records.Where(r => r.IsGood).Sum(r => r.Quantity); totalProduced += deviceTotalProduced; totalGood += deviceTotalGood; // Calculate quality metrics for this device var deviceMetrics = await CalculateDeviceQualityMetricsAsync(deviceId, filter); qualityMetrics.AddRange(deviceMetrics); // Perform defect analysis var deviceDefects = await AnalyzeDefectsAsync(deviceId, filter); defectAnalysis.AddRange(deviceDefects); } var qualityRate = totalProduced > 0 ? (totalGood / totalProduced) * 100 : 0; var defectRate = totalProduced > 0 ? 100 - qualityRate : 0; return new QualityAnalysis { DeviceIds = deviceIds, DeviceNames = deviceIds.Select(id => _deviceRepository.GetByIdAsync(id).Result?.Name ?? "Unknown").ToList(), PeriodStart = filter.StartDate, PeriodEnd = filter.EndDate, TotalProduced = totalProduced, TotalGood = totalGood, QualityRate = qualityRate, DefectRate = defectRate, QualityMetrics = qualityMetrics, DefectAnalysis = defectAnalysis }; } public async Task GetDashboardSummaryAsync(DashboardFilter filter) { var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds : (await _deviceRepository.GetAllDevicesAsync()).Select(d => d.Id).ToList(); var deviceSummaries = new List(); var activeAlerts = new List(); int totalDevices = deviceIds.Count; int activeDevices = 0; int offlineDevices = 0; decimal totalProductionToday = 0; decimal totalProductionThisWeek = 0; decimal totalProductionThisMonth = 0; decimal overallEfficiency = 0; decimal qualityRate = 0; int efficiencyCount = 0; foreach (var deviceId in deviceIds) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) continue; // Get device status var deviceStatus = await GetDeviceCurrentStatusAsync(deviceId); // Get today's production var todayProduction = await GetDeviceProductionForDateAsync(deviceId, DateTime.Today); // Get weekly and monthly production var weekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek); var weekProduction = await GetDeviceProductionForDateRangeAsync(deviceId, weekStart, DateTime.Today); var monthStart = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); var monthProduction = await GetDeviceProductionForDateRangeAsync(deviceId, monthStart, DateTime.Today); // Calculate efficiency and quality var efficiency = await CalculateDeviceEfficiencyAsync(deviceId, new EfficiencyFilter { DeviceIds = new List { deviceId }, StartDate = DateTime.Today, EndDate = DateTime.Today }); var quality = await CalculateQualityRateAsync(deviceId, null, DateTime.Today, DateTime.Today); deviceSummaries.Add(new DeviceSummary { DeviceId = deviceId, DeviceName = device.Name, Status = deviceStatus.Status, TodayProduction = todayProduction, Efficiency = efficiency.Oee, QualityRate = quality, Runtime = deviceStatus.Runtime, CurrentProgram = deviceStatus.CurrentProgram }); totalProductionToday += todayProduction; totalProductionThisWeek += weekProduction; totalProductionThisMonth += monthProduction; overallEfficiency += efficiency.Oee; qualityRate += quality; efficiencyCount++; // Count devices by status if (deviceStatus.Status == DeviceStatus.Online) activeDevices++; else offlineDevices++; // Get active alerts var alerts = await GetDeviceAlertsAsync(deviceId); activeAlerts.AddRange(alerts); } // Calculate averages overallEfficiency = efficiencyCount > 0 ? overallEfficiency / efficiencyCount : 0; qualityRate = efficiencyCount > 0 ? qualityRate / efficiencyCount : 0; return new DashboardSummary { GeneratedAt = DateTime.Now, TotalDevices = totalDevices, ActiveDevices = activeDevices, OfflineDevices = offlineDevices, TotalProductionToday = totalProductionToday, TotalProductionThisWeek = totalProductionThisWeek, TotalProductionThisMonth = totalProductionThisMonth, OverallEfficiency = overallEfficiency, QualityRate = qualityRate, DeviceSummaries = deviceSummaries, ActiveAlerts = activeAlerts }; } public async Task CalculateOeeAsync(int deviceId, DateTime date) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) throw new KeyNotFoundException($"Device {deviceId} not found"); // Get planned production time (typically 8 hours or as configured) var plannedProductionTime = await GetPlannedProductionTimeAsync(deviceId, date); // Get actual production time var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date); var actualProductionTime = CalculateProductionTime(productionRecords); // Calculate downtime var downtime = plannedProductionTime - actualProductionTime; // Get performance metrics var totalPieces = productionRecords.Sum(r => r.Quantity); var goodPieces = productionRecords.Where(r => r.IsGood).Sum(r => r.Quantity); // Calculate OEE components var availability = CalculateAvailability(plannedProductionTime, actualProductionTime); var performance = await CalculatePerformanceAsync(deviceId, date); var quality = totalPieces > 0 ? (goodPieces / totalPieces) * 100 : 0; var oee = (availability * performance * quality) / 10000; // Convert percentage back to decimal return new OeeMetrics { DeviceId = deviceId, DeviceName = device.Name, Date = date, Availability = availability, Performance = performance, Quality = quality, Oee = oee, PlannedProductionTime = plannedProductionTime, ActualProductionTime = actualProductionTime, Downtime = downtime, IdealCycleTime = await GetIdealCycleTimeAsync(deviceId), TotalCycleTime = actualProductionTime, TotalPieces = totalPieces, GoodPieces = goodPieces }; } public async Task GenerateProductionForecastAsync(ForecastFilter filter) { var device = await _deviceRepository.GetByIdAsync(filter.DeviceId); if (device == null) throw new KeyNotFoundException($"Device {filter.DeviceId} not found"); var historicalData = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync( filter.DeviceId, filter.HistoricalDataStart, filter.HistoricalDataEnd); var dailyForecasts = new List(); var forecastAccuracy = 0.0m; switch (filter.Model) { case ForecastModel.Linear: dailyForecasts = GenerateLinearForecast(historicalData, filter.DaysToForecast); break; case ForecastModel.ExponentialSmoothing: dailyForecasts = GenerateExponentialSmoothingForecast(historicalData, filter.DaysToForecast); break; case ForecastModel.MovingAverage: dailyForecasts = GenerateMovingAverageForecast(historicalData, filter.DaysToForecast); break; default: dailyForecasts = GenerateLinearForecast(historicalData, filter.DaysToForecast); break; } // Calculate forecast accuracy if we have actual data forecastAccuracy = CalculateForecastAccuracy(dailyForecasts); return new ProductionForecast { DeviceId = filter.DeviceId, DeviceName = device.Name, ForecastStartDate = DateTime.Today, ForecastEndDate = DateTime.Today.AddDays(filter.DaysToForecast - 1), DailyForecasts = dailyForecasts, ForecastAccuracy = forecastAccuracy, ModelUsed = filter.Model }; } public async Task DetectProductionAnomaliesAsync(AnomalyFilter filter) { var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds : (await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList(); var anomalies = new List(); foreach (var deviceId in deviceIds) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) continue; var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync( deviceId, filter.StartDate, filter.EndDate); // Detect production drop anomalies if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.ProductionDrop) { var dropAnomalies = DetectProductionDropAnomalies(productionRecords); anomalies.AddRange(dropAnomalies); } // Detect quality spike anomalies if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.QualitySpike) { var qualityAnomalies = DetectQualitySpikeAnomalies(productionRecords); anomalies.AddRange(qualityAnomalies); } // Detect downtime spike anomalies if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.DowntimeSpike) { var downtimeAnomalies = DetectDowntimeSpikeAnomalies(deviceId, productionRecords); anomalies.AddRange(downtimeAnomalies); } // Detect efficiency drop anomalies if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.EfficiencyDrop) { var efficiencyAnomalies = DetectEfficiencyDropAnomalies(productionRecords); anomalies.AddRange(efficiencyAnomalies); } } // Filter by severity if (filter.MinSeverity.HasValue) { anomalies = anomalies.Where(a => a.Severity >= filter.MinSeverity.Value).ToList(); } // Determine overall severity var overallSeverity = DetermineOverallAnomalySeverity(anomalies); return new AnomalyAnalysis { DeviceIds = deviceIds, DeviceNames = deviceIds.Select(id => _deviceRepository.GetByIdAsync(id).Result?.Name ?? "Unknown").ToList(), AnalysisStartDate = filter.StartDate, AnalysisEndDate = filter.EndDate, Anomalies = anomalies, OverallSeverity = overallSeverity }; } #region Private Methods private decimal CalculateLinearRegressionTrend(List dailyData) { if (dailyData.Count < 2) return 0; int n = dailyData.Count; decimal sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; for (int i = 0; i < n; i++) { sumX += i; sumY += dailyData[i].Quantity; sumXY += i * dailyData[i].Quantity; sumXX += i * i; } // Linear regression: y = mx + b, where m is the slope decimal numerator = n * sumXY - sumX * sumY; decimal denominator = n * sumXX - sumX * sumX; return denominator != 0 ? numerator / denominator : 0; } private ProductionTrendDirection DetermineTrendDirection(decimal trendCoefficient) { if (trendCoefficient > 0.1m) return ProductionTrendDirection.Increasing; else if (trendCoefficient < -0.1m) return ProductionTrendDirection.Decreasing; else if (Math.Abs(trendCoefficient) <= 0.1m && Math.Abs(trendCoefficient) > 0.01m) return ProductionTrendDirection.Stable; else return ProductionTrendDirection.Volatile; } private async Task GetProductionTargetAsync(int deviceId, DateTime date) { // This would typically come from system configuration or production planning var systemConfig = await _systemRepository.GetSystemConfigurationAsync(); return systemConfig?.DailyProductionTarget ?? 100; // Default target } private async Task CalculateQualityRateAsync(int deviceId, string programName, DateTime startDate, DateTime endDate) { var records = await _productionRepository.GetProductionRecordsByFilterAsync(new ReportFilter { DeviceIds = new List { deviceId }, ProgramNames = programName != null ? new List { programName } : null, StartDate = startDate, EndDate = endDate }); var totalQuantity = records.Sum(r => r.Quantity); var goodQuantity = records.Where(r => r.IsGood).Sum(r => r.Quantity); return totalQuantity > 0 ? (goodQuantity / totalQuantity) * 100 : 0; } private TimeSpan CalculateRuntime(List records) { if (records == null || records.Count == 0) return TimeSpan.Zero; var startTime = records.Min(r => r.Created); var endTime = records.Max(r => r.Created); return endTime - startTime; } private async Task CalculateDowntimeAsync(int deviceId, DateTime startDate, DateTime endDate) { // Get device status changes var statusRecords = await _collectionRepository.GetDeviceStatusHistoryAsync(deviceId, startDate, endDate); // Calculate total downtime (time when device was not in production state) var downtime = TimeSpan.Zero; var previousTime = startDate; foreach (var status in statusRecords.OrderBy(s => s.Timestamp)) { if (status.Status != DeviceStatus.Running && status.Status != DeviceStatus.Idle) { downtime += status.Timestamp - previousTime; } previousTime = status.Timestamp; } // Add remaining time downtime += endDate - previousTime; return downtime; } private ReportMetadata CalculateReportMetadata(List summaryItems, ReportFilter filter) { return ReportMetadata.GeneratedAt; // Simplified for now } private async Task CalculateDeviceEfficiencyAsync(int deviceId, EfficiencyFilter filter) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) throw new KeyNotFoundException($"Device {deviceId} not found"); var records = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync( deviceId, filter.StartDate, filter.EndDate); var hourlyData = new List(); var totalAvailability = 0m; var totalPerformance = 0m; var totalQuality = 0m; var hourCount = 0; // Group by hour if requested if (filter.GroupBy == GroupBy.Hour) { var groupedByHour = records.GroupBy(r => r.Created.Date.AddHours(r.Created.Hour)); foreach (var hourGroup in groupedByHour) { var hour = hourGroup.Key; var hourRecords = hourGroup.ToList(); // Calculate efficiency metrics for this hour var availability = await CalculateHourlyAvailabilityAsync(deviceId, hour); var performance = await CalculateHourlyPerformanceAsync(deviceId, hour); var quality = CalculateHourlyQuality(hourRecords); hourlyData.Add(new HourlyEfficiency { Hour = hour, Availability = availability, Performance = performance, Quality = quality, Oee = (availability * performance * quality) / 100 }); totalAvailability += availability; totalPerformance += performance; totalQuality += quality; hourCount++; } } // Calculate overall metrics var availability = hourCount > 0 ? totalAvailability / hourCount : 0; var performance = hourCount > 0 ? totalPerformance / hourCount : 0; var quality = hourCount > 0 ? totalQuality / hourCount : 0; var oee = (availability * performance * quality) / 100; return new EfficiencyMetrics { DeviceId = deviceId, DeviceName = device.Name, PeriodStart = filter.StartDate, PeriodEnd = filter.EndDate, Availability = availability, Performance = performance, Quality = quality, Oee = oee, Utilization = DetermineUtilizationLevel(oee), HourlyData = hourlyData }; } private EfficiencyMetrics AggregateEfficiencyMetrics(List metrics) { if (metrics == null || metrics.Count == 0) return new EfficiencyMetrics(); var aggregated = new EfficiencyMetrics { DeviceIds = metrics.Select(m => m.DeviceId).ToList(), PeriodStart = metrics.Min(m => m.PeriodStart), PeriodEnd = metrics.Max(m => m.PeriodEnd), Availability = metrics.Average(m => m.Availability), Performance = metrics.Average(m => m.Performance), Quality = metrics.Average(m => m.Quality), Oee = metrics.Average(m => m.Oee), HourlyData = metrics.SelectMany(m => m.HourlyData).ToList() }; aggregated.Utilization = DetermineUtilizationLevel(aggregated.Oee); return aggregated; } private EquipmentUtilization DetermineUtilizationLevel(decimal oee) { if (oee >= 85) return EquipmentUtilization.Optimal; if (oee >= 70) return EquipmentUtilization.High; if (oee >= 50) return EquipmentUtilization.Medium; return EquipmentUtilization.Low; } private async Task> CalculateDeviceQualityMetricsAsync(int deviceId, QualityFilter filter) { // Simplified quality metrics calculation var metrics = new List(); var defectRate = await CalculateQualityRateAsync(deviceId, null, filter.StartDate, filter.EndDate); metrics.Add(new QualityMetric { MetricName = "Defect Rate", Value = 100 - defectRate, Unit = "%", Timestamp = DateTime.Now }); return metrics; } private async Task> AnalyzeDefectsAsync(int deviceId, QualityFilter filter) { // Simplified defect analysis return new List(); } private async Task GetDeviceCurrentStatusAsync(int deviceId) { // This would typically get current status from real-time data var records = await _collectionRepository.GetLatestDeviceStatusAsync(deviceId); return records ?? new DeviceCurrentStatus(); } private async Task GetDeviceProductionForDateAsync(int deviceId, DateTime date) { var records = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date); return records.Sum(r => r.Quantity); } private async Task GetDeviceProductionForDateRangeAsync(int deviceId, DateTime startDate, DateTime endDate) { var records = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(deviceId, startDate, endDate); return records.Sum(r => r.Quantity); } private async Task> GetDeviceAlertsAsync(int deviceId) { var alerts = await _alarmRepository.GetActiveAlertsByDeviceAsync(deviceId); return alerts.Select(a => new AlertSummary { AlertId = a.Id, DeviceName = "Device " + deviceId, // Simplified AlertType = (AlertType)a.AlertType, Message = a.Message, CreatedAt = a.CreatedAt, IsActive = a.IsActive }).ToList(); } private async Task GetPlannedProductionTimeAsync(int deviceId, DateTime date) { // This would typically come from system configuration var systemConfig = await _systemRepository.GetSystemConfigurationAsync(); return systemConfig?.DailyWorkingHours ?? TimeSpan.FromHours(8); } private TimeSpan CalculateProductionTime(List records) { if (records == null || records.Count == 0) return TimeSpan.Zero; var startTime = records.Min(r => r.Created); var endTime = records.Max(r => r.Created); return endTime - startTime; } private decimal CalculateAvailability(TimeSpan plannedTime, TimeSpan actualTime) { return plannedTime.TotalMinutes > 0 ? (actualTime.TotalMinutes / plannedTime.TotalMinutes) * 100 : 0; } private async Task CalculatePerformanceAsync(int deviceId, DateTime date) { // Simplified performance calculation var records = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date); var totalPieces = records.Sum(r => r.Quantity); var idealCycleTime = await GetIdealCycleTimeAsync(deviceId); var standardPieces = idealCycleTime > 0 ? (24 * 60) / idealCycleTime : 0; // Assuming 24 hours operation return standardPieces > 0 ? (totalPieces / standardPieces) * 100 : 0; } private async Task GetIdealCycleTimeAsync(int deviceId) { // This would typically come from device configuration var device = await _deviceRepository.GetByIdAsync(deviceId); return device?.IdealCycleTime ?? 1; // Default 1 minute per piece } private decimal CalculateHourlyQuality(List hourRecords) { if (hourRecords == null || hourRecords.Count == 0) return 0; var totalQuantity = hourRecords.Sum(r => r.Quantity); var goodQuantity = hourRecords.Where(r => r.IsGood).Sum(r => r.Quantity); return totalQuantity > 0 ? (goodQuantity / totalQuantity) * 100 : 0; } private async Task CalculateHourlyAvailabilityAsync(int deviceId, DateTime hour) { // Simplified availability calculation return 85m; // Default 85% availability } private async Task CalculateHourlyPerformanceAsync(int deviceId, DateTime hour) { // Simplified performance calculation return 90m; // Default 90% performance } private List GenerateLinearForecast(List historicalData, int daysToForecast) { var forecasts = new List(); var lastDate = historicalData.Max(r => r.Created); // Calculate average daily production var avgDailyProduction = historicalData.Average(r => r.Quantity); for (int i = 1; i <= daysToForecast; i++) { var forecastDate = lastDate.AddDays(i); var forecastQuantity = avgDailyProduction; // Simple average forecasts.Add(new ForecastItem { Date = forecastDate, ForecastedQuantity = forecastQuantity, ConfidenceLower = forecastQuantity * 0.8m, ConfidenceUpper = forecastQuantity * 1.2m, ActualQuantity = 0, // Will be updated when actual data is available Variance = 0 }); } return forecasts; } private List GenerateExponentialSmoothingForecast(List historicalData, int daysToForecast) { // Simplified exponential smoothing var alpha = 0.3m; // Smoothing factor var forecasts = new List(); if (historicalData.Count == 0) return forecasts; var lastSmoothed = historicalData.First().Quantity; var lastDate = historicalData.Max(r => r.Created); for (int i = 1; i <= daysToForecast; i++) { var forecastDate = lastDate.AddDays(i); var forecastQuantity = lastSmoothed; forecasts.Add(new ForecastItem { Date = forecastDate, ForecastedQuantity = forecastQuantity, ConfidenceLower = forecastQuantity * 0.85m, ConfidenceUpper = forecastQuantity * 1.15m, ActualQuantity = 0, Variance = 0 }); lastSmoothed = alpha * forecastQuantity + (1 - alpha) * lastSmoothed; } return forecasts; } private List GenerateMovingAverageForecast(List historicalData, int daysToForecast) { var forecasts = new List(); var windowSize = Math.Min(7, historicalData.Count); // 7-day moving average if (historicalData.Count < windowSize) return forecasts; var lastDate = historicalData.Max(r => r.Created); // Calculate moving average var movingAvg = historicalData.TakeLast(windowSize).Average(r => r.Quantity); for (int i = 1; i <= daysToForecast; i++) { var forecastDate = lastDate.AddDays(i); var forecastQuantity = movingAvg; forecasts.Add(new ForecastItem { Date = forecastDate, ForecastedQuantity = forecastQuantity, ConfidenceLower = forecastQuantity * 0.9m, ConfidenceUpper = forecastQuantity * 1.1m, ActualQuantity = 0, Variance = 0 }); } return forecasts; } private decimal CalculateForecastAccuracy(List forecasts) { // Simplified accuracy calculation based on confidence intervals var accurateForecasts = forecasts.Count(f => f.ForecastedQuantity >= f.ConfidenceLower && f.ForecastedQuantity <= f.ConfidenceUpper); return forecasts.Count > 0 ? (accurateForecasts / forecasts.Count) * 100 : 0; } private List DetectProductionDropAnomalies(List records) { var anomalies = new List(); if (records.Count < 2) return anomalies; // Compare each day with the previous day var groupedByDay = records.GroupBy(r => r.Created.Date).OrderBy(g => g.Key); var previousDay = groupedByDay.FirstOrDefault(); foreach (var currentDay in groupedByDay.Skip(1)) { var previousQuantity = previousDay.Sum(r => r.Quantity); var currentQuantity = currentDay.Sum(r => r.Quantity); if (previousQuantity > 0) { var dropPercentage = (decimal)((previousQuantity - currentQuantity) / (double)previousQuantity * 100); if (dropPercentage > 30) // 30% drop threshold { anomalies.Add(new ProductionAnomaly { Timestamp = currentDay.Key, Type = AnomalyType.ProductionDrop, Severity = dropPercentage > 50 ? AnomalySeverity.Critical : AnomalySeverity.High, Deviation = dropPercentage, Description = $"Production dropped by {dropPercentage:F1}% from previous day", RecommendedAction = dropPercentage > 50 ? AnomalyAction.Shutdown : AnomalyAction.Investigate }); } } previousDay = currentDay; } return anomalies; } private List DetectQualitySpikeAnomalies(List records) { var anomalies = new List(); if (records.Count == 0) return anomalies; var qualityRates = records.GroupBy(r => r.Created.Date) .Select(g => new { Date = g.Key, QualityRate = g.Where(r => r.IsGood).Average(r => (decimal)r.Quantity / (decimal)g.Sum(r => r.Quantity)) * 100 }); var avgQuality = qualityRates.Average(q => q.QualityRate); foreach (var day in qualityRates) { var deviation = Math.Abs((decimal)(day.QualityRate - avgQuality)); if (deviation > 20 && day.QualityRate > avgQuality) // Significant quality improvement { anomalies.Add(new ProductionAnomaly { Timestamp = day.Date, Type = AnomalyType.QualitySpike, Severity = deviation > 30 ? AnomalySeverity.High : AnomalySeverity.Medium, Deviation = deviation, Description = $"Quality rate spiked to {day.QualityRate:F1}%", RecommendedAction = AnomalyAction.Monitor }); } } return anomalies; } private List DetectDowntimeSpikeAnomalies(int deviceId, List records) { var anomalies = new List(); // Simplified downtime spike detection var downtimePeriods = CalculateDowntimePeriods(records); var avgDowntime = downtimePeriods.Average(d => d.Duration.TotalMinutes); foreach (var period in downtimePeriods) { if (period.Duration.TotalMinutes > avgDowntime * 2) // Double the average downtime { anomalies.Add(new ProductionAnomaly { Timestamp = period.Start, Type = AnomalyType.DowntimeSpike, Severity = period.Duration.TotalMinutes > 120 ? AnomalySeverity.Critical : AnomalySeverity.High, Deviation = (decimal)(period.Duration.TotalMinutes / avgDowntime), Description = $"Extended downtime period: {period.Duration.TotalMinutes:F0} minutes", RecommendedAction = period.Duration.TotalMinutes > 120 ? AnomalyAction.Shutdown : AnomalyAction.Alert }); } } return anomalies; } private List DetectEfficiencyDropAnomalies(List records) { var anomalies = new List(); // Simplified efficiency drop detection var efficiencies = records.GroupBy(r => r.Created.Date) .Select(g => new { Date = g.Key, Efficiency = g.Sum(r => r.Quantity) / g.Sum(r => r.TargetQuantity) * 100 }); var avgEfficiency = efficiencies.Average(e => e.Efficiency); foreach (var day in efficiencies) { if (day.Efficiency < avgEfficiency * 0.7) // 30% below average { anomalies.Add(new ProductionAnomaly { Timestamp = day.Date, Type = AnomalyType.EfficiencyDrop, Severity = day.Efficiency < avgEfficiency * 0.5 ? AnomalySeverity.Critical : AnomalySeverity.High, Deviation = avgEfficiency - day.Efficiency, Description = $"Efficiency dropped to {day.Efficiency:F1}%", RecommendedAction = AnomalyAction.Investigate }); } } return anomalies; } private List CalculateDowntimePeriods(List records) { var downtimePeriods = new List(); if (records.Count == 0) return downtimePeriods; var sortedRecords = records.OrderBy(r => r.Created).ToList(); var downtimeStart = sortedRecords.First().Created; foreach (var record in sortedRecords) { // Assume there's downtime if there's a gap of more than 5 minutes between records if (record.Created - downtimeStart > TimeSpan.FromMinutes(5)) { downtimePeriods.Add(new DowntimePeriod { Start = downtimeStart, End = record.Created, Duration = record.Created - downtimeStart }); downtimeStart = record.Created; } } return downtimePeriods; } private AnomalySeverity DetermineOverallAnomalySeverity(List anomalies) { if (anomalies.Count == 0) return AnomalySeverity.Low; var maxSeverity = anomalies.Max(a => a.Severity); var criticalCount = anomalies.Count(a => a.Severity == AnomalySeverity.Critical); if (criticalCount > 0) return AnomalySeverity.Critical; if (maxSeverity >= AnomalySeverity.High) return AnomalySeverity.High; if (maxSeverity >= AnomalySeverity.Medium) return AnomalySeverity.Medium; return AnomalySeverity.Low; } #endregion } // Helper class for downtime periods internal class DowntimePeriod { public DateTime Start { get; set; } public DateTime End { get; set; } public TimeSpan Duration { get; set; } } }