You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1094 lines
45 KiB
C#
1094 lines
45 KiB
C#
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<ProductionTrendAnalysis> 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<DailyProduction>();
|
|
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<ProductionReport> GenerateProductionReportAsync(ReportFilter filter)
|
|
{
|
|
var summaryItems = new List<ProductionSummaryItem>();
|
|
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<EfficiencyMetrics> CalculateEfficiencyMetricsAsync(EfficiencyFilter filter)
|
|
{
|
|
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
|
|
(await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList();
|
|
|
|
var metrics = new List<EfficiencyMetrics>();
|
|
var hourlyData = new List<HourlyEfficiency>();
|
|
|
|
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<QualityAnalysis> 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<QualityMetric>();
|
|
var defectAnalysis = new List<DefectAnalysis>();
|
|
|
|
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<int> { 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<DashboardSummary> GetDashboardSummaryAsync(DashboardFilter filter)
|
|
{
|
|
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
|
|
(await _deviceRepository.GetAllDevicesAsync()).Select(d => d.Id).ToList();
|
|
|
|
var deviceSummaries = new List<DeviceSummary>();
|
|
var activeAlerts = new List<AlertSummary>();
|
|
|
|
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<int> { 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<OeeMetrics> 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<ProductionForecast> 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<ForecastItem>();
|
|
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<AnomalyAnalysis> DetectProductionAnomaliesAsync(AnomalyFilter filter)
|
|
{
|
|
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
|
|
(await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList();
|
|
|
|
var anomalies = new List<ProductionAnomaly>();
|
|
|
|
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<DailyProduction> 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<decimal> 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<decimal> CalculateQualityRateAsync(int deviceId, string programName, DateTime startDate, DateTime endDate)
|
|
{
|
|
var records = await _productionRepository.GetProductionRecordsByFilterAsync(new ReportFilter
|
|
{
|
|
DeviceIds = new List<int> { deviceId },
|
|
ProgramNames = programName != null ? new List<string> { 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<ProductionRecord> 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<TimeSpan> 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<ProductionSummaryItem> summaryItems, ReportFilter filter)
|
|
{
|
|
return ReportMetadata.GeneratedAt; // Simplified for now
|
|
}
|
|
|
|
private async Task<EfficiencyMetrics> 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<HourlyEfficiency>();
|
|
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<EfficiencyMetrics> 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<List<QualityMetric>> CalculateDeviceQualityMetricsAsync(int deviceId, QualityFilter filter)
|
|
{
|
|
// Simplified quality metrics calculation
|
|
var metrics = new List<QualityMetric>();
|
|
|
|
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<List<DefectAnalysis>> AnalyzeDefectsAsync(int deviceId, QualityFilter filter)
|
|
{
|
|
// Simplified defect analysis
|
|
return new List<DefectAnalysis>();
|
|
}
|
|
|
|
private async Task<DeviceCurrentStatus> 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<decimal> GetDeviceProductionForDateAsync(int deviceId, DateTime date)
|
|
{
|
|
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date);
|
|
return records.Sum(r => r.Quantity);
|
|
}
|
|
|
|
private async Task<decimal> GetDeviceProductionForDateRangeAsync(int deviceId, DateTime startDate, DateTime endDate)
|
|
{
|
|
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(deviceId, startDate, endDate);
|
|
return records.Sum(r => r.Quantity);
|
|
}
|
|
|
|
private async Task<List<AlertSummary>> 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<TimeSpan> 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<ProductionRecord> 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<decimal> 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<decimal> 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<ProductionRecord> 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<decimal> CalculateHourlyAvailabilityAsync(int deviceId, DateTime hour)
|
|
{
|
|
// Simplified availability calculation
|
|
return 85m; // Default 85% availability
|
|
}
|
|
|
|
private async Task<decimal> CalculateHourlyPerformanceAsync(int deviceId, DateTime hour)
|
|
{
|
|
// Simplified performance calculation
|
|
return 90m; // Default 90% performance
|
|
}
|
|
|
|
private List<ForecastItem> GenerateLinearForecast(List<ProductionRecord> historicalData, int daysToForecast)
|
|
{
|
|
var forecasts = new List<ForecastItem>();
|
|
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<ForecastItem> GenerateExponentialSmoothingForecast(List<ProductionRecord> historicalData, int daysToForecast)
|
|
{
|
|
// Simplified exponential smoothing
|
|
var alpha = 0.3m; // Smoothing factor
|
|
var forecasts = new List<ForecastItem>();
|
|
|
|
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<ForecastItem> GenerateMovingAverageForecast(List<ProductionRecord> historicalData, int daysToForecast)
|
|
{
|
|
var forecasts = new List<ForecastItem>();
|
|
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<ForecastItem> 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<ProductionAnomaly> DetectProductionDropAnomalies(List<ProductionRecord> records)
|
|
{
|
|
var anomalies = new List<ProductionAnomaly>();
|
|
|
|
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<ProductionAnomaly> DetectQualitySpikeAnomalies(List<ProductionRecord> records)
|
|
{
|
|
var anomalies = new List<ProductionAnomaly>();
|
|
|
|
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<ProductionAnomaly> DetectDowntimeSpikeAnomalies(int deviceId, List<ProductionRecord> records)
|
|
{
|
|
var anomalies = new List<ProductionAnomaly>();
|
|
|
|
// 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<ProductionAnomaly> DetectEfficiencyDropAnomalies(List<ProductionRecord> records)
|
|
{
|
|
var anomalies = new List<ProductionAnomaly>();
|
|
|
|
// 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<DowntimePeriod> CalculateDowntimePeriods(List<ProductionRecord> records)
|
|
{
|
|
var downtimePeriods = new List<DowntimePeriod>();
|
|
|
|
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<ProductionAnomaly> 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; }
|
|
}
|
|
} |