using AsbCloudApp.Data; using AsbCloudApp.Services; using AsbCloudDb.Model; using Mapster; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using AsbCloudApp.Data.SAUB; using AsbCloudInfrastructure.Services.SAUB; namespace AsbCloudInfrastructure.Services.WellOperationService; public class OperationsStatService : IOperationsStatService { private readonly IAsbCloudDbContext db; private readonly IMemoryCache memoryCache; private readonly IWellService wellService; private readonly TelemetryDataCache telemetryDataCache; public OperationsStatService(IAsbCloudDbContext db, IMemoryCache memoryCache, IWellService wellService, TelemetryDataCache telemetryDataCache) { this.db = db; this.memoryCache = memoryCache; this.wellService = wellService; this.telemetryDataCache = telemetryDataCache; } public async Task GetOrDefaultStatClusterAsync(int idCluster, int idCompany, CancellationToken token) { var cluster = (await memoryCache .GetOrCreateBasicAsync(db.Set(), token)) .FirstOrDefault(c => c.Id == idCluster); if (cluster is null) return null; var allWellsByCompany = await wellService.GetAsync(new() { IdCompany = idCompany }, token).ConfigureAwait(false); var idWellsByCompany = allWellsByCompany.Select(w => w.Id).Distinct(); var wells = await db.Wells .Include(w => w.WellOperations) .Where(o => o.IdCluster == idCluster) .Where(w => idWellsByCompany.Contains(w.Id)) .Select(w => w.Id) .ToListAsync(token); var statsWells = await GetWellsStatAsync(wells, token).ConfigureAwait(false); var statClusterDto = new StatClusterDto { Id = idCluster, Caption = cluster.Caption, StatsWells = statsWells, }; return statClusterDto; } public async Task> GetWellsStatAsync(IEnumerable idWells, CancellationToken token) { var wells = await db.Wells .Include(w => w.WellOperations) .Where(w => idWells.Contains(w.Id)) .AsNoTracking() .ToListAsync(token); var statsWells = new List(wells.Count); foreach (var well in wells) { var statWellDto = await CalcWellStatAsync(well, token); statsWells.Add(statWellDto); } return statsWells; } public async Task GetOrDefaultWellStatAsync(int idWell, CancellationToken token = default) { var well = await db.Wells .Include(w => w.WellOperations) .FirstOrDefaultAsync(w => w.Id == idWell, token) .ConfigureAwait(false); if(well is null) return null; var statWellDto = await CalcWellStatAsync(well, token); return statWellDto; } public async Task GetOrDefaultRopStatAsync(int idWell, CancellationToken token) { var clusterWellsIds = await wellService.GetClusterWellsIdsAsync(idWell, token) .ConfigureAwait(false); if (clusterWellsIds is null) return null; var idLastSectionType = await (from o in db.WellOperations where o.IdWell == idWell && o.IdType == 1 orderby o.DepthStart select o.IdWellSectionType) .LastOrDefaultAsync(token) .ConfigureAwait(false); if (idLastSectionType == default) return null; var operations = await (from o in db.WellOperations where clusterWellsIds.Contains(o.IdWell) && o.IdType == 1 && o.IdWellSectionType == idLastSectionType select o) .ToListAsync(token) .ConfigureAwait(false); var statsList = new List(clusterWellsIds.Count()); foreach (var clusterWellId in clusterWellsIds) { var currentWellOps = operations.Where(o => o.IdWell == clusterWellId); var timezoneOffsetHours = wellService.GetTimezone(clusterWellId).Hours; var stat = CalcStat(currentWellOps, timezoneOffsetHours); if(stat is not null) statsList.Add(stat); }; if (!statsList.Any()) return null; var clusterRops = new ClusterRopStatDto() { RopMax = statsList.Max(s => s.Rop), RopAverage = statsList.Average(s => s.Rop) }; return clusterRops; } private async Task CalcWellStatAsync(Well well, CancellationToken token) { var wellType = (await memoryCache .GetOrCreateBasicAsync(db.Set(), token)) .FirstOrDefault(t => t.Id == well.IdWellType); var statWellDto = new StatWellDto { Id = well.Id, Caption = well.Caption, WellType = wellType?.Caption ?? "", IdState = well.IdState, State = wellService.GetStateText(well.IdState), LastTelemetryDate = wellService.GetLastTelemetryDate(well.Id), Companies = await wellService.GetCompaniesAsync(well.Id, token) }; if (well.WellOperations is null) return statWellDto; var wellOperations = well.WellOperations .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthEnd); if (!wellOperations.Any()) return statWellDto; var timezoneOffsetH = wellService.GetTimezone(well.Id).Hours; statWellDto.Sections = CalcSectionsStats(wellOperations, timezoneOffsetH); statWellDto.Total = GetStatTotal(wellOperations, well.IdState, timezoneOffsetH); statWellDto.TvdLagDays = CalcTvdLagDays(wellOperations); statWellDto.TvdDrillingDays = CalcDrillingDays(wellOperations); return statWellDto; } private static double? CalcDrillingDays(IEnumerable wellOperations) { var operationsOrdered = wellOperations .OrderBy(o => o.DateStart); var factOperations = operationsOrdered .Where(o => o.IdType == WellOperation.IdOperationTypeFact); if (!factOperations.Any()) return null; var operationFrom = factOperations.First(); var operationTo = factOperations.Last(); return (operationTo.DateStart.AddHours(operationFrom.DurationHours) - operationFrom.DateStart).TotalDays; } private static double? CalcTvdLagDays(IEnumerable wellOperations) { var operationsOrdered = wellOperations .OrderBy(o => o.DateStart); var factOperations = operationsOrdered .Where(o => o.IdType == WellOperation.IdOperationTypeFact); var lastCorrespondingFactOperation = factOperations .LastOrDefault(o => o.IdPlan is not null); if (lastCorrespondingFactOperation is null) return null; var lastCorrespondingPlanOperation = wellOperations .FirstOrDefault(o => o.Id == lastCorrespondingFactOperation.IdPlan); if (lastCorrespondingPlanOperation is null) return null; var lastFactOperation = factOperations.Last(); var remainingPlanOperations = operationsOrdered .Where(o => o.IdType == WellOperation.IdOperationTypePlan) .Where(o => o.DateStart > lastCorrespondingPlanOperation.DateStart); var durationRemain = remainingPlanOperations.Sum(o => o.DurationHours); var factEnd = lastFactOperation.DateStart.AddHours(durationRemain + lastFactOperation.DurationHours); var planEnd = lastCorrespondingFactOperation.DateStart.AddHours(durationRemain + lastCorrespondingFactOperation.DurationHours); var lagDays = (planEnd - factEnd).TotalDays; return lagDays; } private IEnumerable CalcSectionsStats(IEnumerable operations, double timezoneOffsetH) { var sectionTypeIds = operations .Select(o => o.IdWellSectionType) .Distinct(); var sectionTypes = memoryCache .GetOrCreateBasic(db.Set()) .Where(s => sectionTypeIds.Contains(s.Id)) .ToDictionary(s => s.Id); var sections = new List(sectionTypes.Count); var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan); var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact); foreach ((var id, var sectionType) in sectionTypes) { var section = new StatSectionDto { Id = id, Caption = sectionType.Caption, Plan = CalcSectionStat(operationsPlan, id, timezoneOffsetH), Fact = CalcSectionStat(operationsFact, id, timezoneOffsetH), }; sections.Add(section); } return sections; } private static PlanFactDto GetStatTotal(IEnumerable operations, int idWellState, double timezoneOffsetH) { var operationsPlan = operations.Where(o => o.IdType == WellOperation.IdOperationTypePlan); var operationsFact = operations.Where(o => o.IdType == WellOperation.IdOperationTypeFact); var factEnd = CalcStat(operationsFact, timezoneOffsetH); if (factEnd is not null && idWellState != 2) factEnd.End = null; var section = new PlanFactDto { Plan = CalcStat(operationsPlan, timezoneOffsetH), Fact = factEnd, }; return section; } private static StatOperationsDto? CalcSectionStat(IEnumerable operations, int idSectionType, double timezoneOffsetHours) { var sectionOperations = operations .Where(o => o.IdWellSectionType == idSectionType) .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthStart); return CalcStat(sectionOperations, timezoneOffsetHours); } private static StatOperationsDto? CalcStat(IEnumerable operations, double timezoneOffsetHours) { if (!operations.Any()) return null; var races = GetCompleteRaces(operations, timezoneOffsetHours); var section = new StatOperationsDto { Start = operations.FirstOrDefault()?.DateStart.ToRemoteDateTime(timezoneOffsetHours), End = operations.Max(o => o.DateStart.ToRemoteDateTime(timezoneOffsetHours).AddHours(o.DurationHours)), WellDepthStart = operations.Min(o => o.DepthStart), WellDepthEnd = operations.Max(o => o.DepthStart), Rop = CalcROP(operations), RouteSpeed = CalcAvgRaceSpeed(races), BhaDownSpeed = CalcBhaDownSpeed(races), BhaUpSpeed = CalcBhaUpSpeed(races), CasingDownSpeed = CalcCasingDownSpeed(operations), NonProductiveHours = operations .Where(o => WellOperationCategory.NonProductiveTimeSubIds.Contains(o.IdCategory)) .Sum(o => o.DurationHours), }; return section; } private static double CalcROP(IEnumerable operationsProps) { var drillingOperations = operationsProps.Where(o => WellOperationCategory.MechanicalDrillingSubIds.Contains(o.IdCategory)); var dDepth = 0d; var dHours = 0d; foreach (var operation in drillingOperations) { var deltaDepth = operation.DepthEnd - operation.DepthStart; dDepth += deltaDepth; dHours += operation.DurationHours; } return dDepth / (dHours + double.Epsilon); } private static double CalcCasingDownSpeed(IEnumerable operationsProps) { var ops = operationsProps.Where(o => o.IdCategory == WellOperationCategory.IdCasingDown); var depth = 0d; var dHours = 0d; foreach (var operation in ops) { depth += operation.DepthStart; dHours += operation.DurationHours; } return depth / (dHours + double.Epsilon); } private static IEnumerable GetCompleteRaces(IEnumerable operations, double timezoneOffsetH) { var races = new List(); var iterator = operations .OrderBy(o => o.DateStart) .GetEnumerator(); while (iterator.MoveNext()) { if (iterator.Current.IdCategory == WellOperationCategory.IdBhaAssembly) { var race = new Race { StartDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH).AddHours(iterator.Current.DurationHours), StartWellDepth = iterator.Current.DepthStart, Operations = new List(10), }; while (iterator.MoveNext()) { if (iterator.Current.IdCategory == WellOperationCategory.IdEquipmentRepair) race.RepairHours += iterator.Current.DurationHours; if (WellOperationCategory.NonProductiveTimeSubIds.Contains(iterator.Current.IdCategory)) race.NonProductiveHours += iterator.Current.DurationHours; if (iterator.Current.IdCategory == WellOperationCategory.IdBhaDisassembly) { race.EndDate = iterator.Current.DateStart.ToRemoteDateTime(timezoneOffsetH); race.EndWellDepth = iterator.Current.DepthStart; races.Add(race); break; } race.Operations.Add(iterator.Current); } } } return races; } private static double CalcAvgRaceSpeed(IEnumerable races) { var dDepth = 0d; var dHours = 0d; foreach (var race in races) { dHours += race.DeltaHours - race.NonProductiveHours - race.RepairHours; dDepth += race.DeltaDepth; } return dDepth / (dHours + double.Epsilon); } private static double CalcBhaDownSpeed(IEnumerable races) { var dDepth = 0d; var dHours = 0d; foreach (Race race in races) { dDepth += race.StartWellDepth; for (var i = 0; i < race.Operations.Count; i++) { if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaDown) dHours += race.Operations[i].DurationHours; if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory)) break; } } return dDepth / (dHours + double.Epsilon); } private static double CalcBhaUpSpeed(IEnumerable races) { var dDepth = 0d; var dHours = 0d; foreach (var race in races) { dDepth += race.EndWellDepth; for (var i = race.Operations.Count - 1; i > 0; i--) { if (race.Operations[i].IdCategory == WellOperationCategory.IdBhaUp) dHours += race.Operations[i].DurationHours; if (WellOperationCategory.MechanicalDrillingSubIds.Contains(race.Operations[i].IdCategory)) break; } } return dDepth / (dHours + double.Epsilon); } public async Task>> GetTvdAsync(int idWell, CancellationToken token) { var wellOperations = await db.WellOperations .Include(o => o.OperationCategory) .Include(o => o.WellSectionType) .Include(o => o.OperationPlan) .Where(o => o.IdWell == idWell) .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthEnd) .AsNoTracking() .ToListAsync(token) .ConfigureAwait(false); var wellOperationsPlan = wellOperations .Where(o => o.IdType == WellOperation.IdOperationTypePlan) .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthEnd); var wellOperationsFact = wellOperations .Where(o => o.IdType == WellOperation.IdOperationTypeFact) .OrderBy(o => o.DateStart) .ThenBy(o => o.DepthEnd); var sectionsIds = wellOperations .Select(o => o.IdWellSectionType) .Distinct(); var tzOffsetHours = wellService.GetTimezone(idWell).Hours; var merged = MergeArraysBySections(sectionsIds, wellOperationsPlan, wellOperationsFact).ToList(); if (merged.Count ==0) return Enumerable.Empty>(); var tvd = new List>(merged.Count); var (Plan, Fact) = merged.FirstOrDefault(); var dateStart = Plan?.DateStart ?? Fact!.DateStart; int? iLastMatch = null; int iLastFact = 0; var nptHours = 0d; for (int i = 0; i < merged.Count; i++) { var item = merged[i]; var plan = item.Plan; var fact = item.Fact; var planFactPredict = new PlanFactPredictBase(); if (plan is not null) { planFactPredict.Plan = Convert(plan, tzOffsetHours); planFactPredict.Plan.Day = (planFactPredict.Plan.DateStart - dateStart).TotalDays; if (fact is not null) iLastMatch = i; } if (fact is not null) { if(WellOperationCategory.NonProductiveTimeSubIds.Contains(fact.IdCategory)) nptHours += fact.DurationHours; planFactPredict.Fact = Convert(fact, tzOffsetHours); planFactPredict.Fact.Day = (planFactPredict.Fact.DateStart - dateStart).TotalDays; planFactPredict.Fact.NptHours = nptHours; iLastFact = i; } tvd.Add(planFactPredict); } if (iLastMatch is null || iLastMatch == merged.Count - 1) return tvd; var lastMatchPlan = merged[iLastMatch.Value].Plan!; var lastMatchPlanOperationEnd = lastMatchPlan.DateStart.AddHours(lastMatchPlan.DurationHours); var lastFact = merged[iLastFact].Fact!; var lastFactDateEnd = lastFact.DateStart.AddHours(lastFact.DurationHours); var startOffset = lastFactDateEnd - lastMatchPlanOperationEnd; for (int i = iLastMatch.Value + 1; i < merged.Count; i++) { if (merged[i].Plan is null) continue; var predict = Convert(merged[i].Plan!, tzOffsetHours); predict.IdType = 2; predict.DateStart = predict.DateStart + startOffset; predict.Day = (predict.DateStart - dateStart).TotalDays; tvd[i].Predict = predict; } return tvd; } private static IEnumerable<(WellOperation? Plan, WellOperation? Fact)> MergeArraysBySections( IEnumerable sectionsIds, IOrderedEnumerable wellOperationsPlan, IOrderedEnumerable wellOperationsFact) { var merged = new List<(WellOperation? Plan, WellOperation? Fact)>(wellOperationsPlan.Count()); foreach (var sectionId in sectionsIds) { var sectionOperationsPlan = wellOperationsPlan .Where(o => o.IdWellSectionType == sectionId); var sectionOperationsFact = wellOperationsFact .Where(o => o.IdWellSectionType == sectionId); var sectionMerged = MergeArrays(sectionOperationsPlan, sectionOperationsFact); merged.AddRange(sectionMerged); } return merged; } private static IEnumerable<(WellOperation? Plan, WellOperation? Fact)> MergeArrays(IEnumerable operationsPlan, IEnumerable operationsFact) { var operationsFactWithNoPlan = operationsFact.Where(x => x.IdPlan == null).ToArray(); var operationsFactWithPlan = operationsFact.Where(x => x.IdPlan != null).ToArray(); var idsPlanWithFact = operationsFact.Where(x => x.IdPlan is not null).Select(x => x.IdPlan).Distinct(); var operationsPlanWithNoFact = operationsPlan.Where(x => !idsPlanWithFact.Contains(x.IdPlan)).ToArray(); var result = new List<(WellOperation? Plan, WellOperation? Fact)>(operationsFactWithNoPlan.Length + operationsFactWithPlan.Length + operationsPlanWithNoFact.Length); foreach (var operation in operationsFactWithPlan) result.Add((operation.OperationPlan, operation)); foreach (var operation in operationsFactWithNoPlan) result.Add((null, operation)); foreach (var operation in operationsPlanWithNoFact) result.Add((operation, null)); return result .OrderBy(x => x.Plan?.DateStart) .ThenBy(x => x.Fact?.DateStart); } private static WellOperationDto Convert(WellOperation source, double tzOffsetHours) { var destination = source.Adapt(); destination.CategoryName = source.OperationCategory?.Name; destination.WellSectionTypeName = source.WellSectionType?.Caption; destination.DateStart = source.DateStart.ToRemoteDateTime(tzOffsetHours); return destination; } }