// Copyright (C) Stichting Deltares 2024. All rights reserved. // // This file is part of the Dam Engine. // // The Dam Engine is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using Deltares.DamEngine.Calculators.General; using Deltares.DamEngine.Calculators.KernelWrappers.Common; using Deltares.DamEngine.Calculators.KernelWrappers.Interfaces; using Deltares.DamEngine.Calculators.KernelWrappers.MacroStabilityInwards; using Deltares.DamEngine.Calculators.Properties; using Deltares.DamEngine.Data.Design; using Deltares.DamEngine.Data.General; using Deltares.DamEngine.Data.General.Results; using Deltares.DamEngine.Data.General.Sensors; using Deltares.DamEngine.Data.General.Specifications; using Deltares.DamEngine.Data.General.Specifications.Extensions; using Deltares.DamEngine.Data.General.TimeSeries; using Deltares.DamEngine.Data.Standard; using Deltares.DamEngine.Data.Standard.Calculation; using Deltares.DamEngine.Data.Standard.Logging; namespace Deltares.DamEngine.Calculators.DikesOperational; /// /// Class for the Operational Calculator /// public class OperationalCalculator { /// /// Holds a reference to a dictionary of output date time entries /// The keys represent the time step and the value part of the dictionary represents /// the argument for the calculation /// /// TimeStep -> Location -> (Sensor, Value) /// /// Cardinality: /// 1 -> N -> M /// private readonly IDictionary>> values = new Dictionary>>(); private TimeSerieCollection inputTimeSerieCollection; private TimeSerieCollection outputTimeSerieCollection; private string outputParameter; /// /// Executes operational calculation. /// /// The dam project data. public void Execute(DamProjectData damProjectData) { if (damProjectData.CalculationMessages == null) { damProjectData.CalculationMessages = new List(); } else { damProjectData.CalculationMessages.Clear(); } outputParameter = DetermineOutputParameter(damProjectData.DamProjectCalculationSpecification.CurrentSpecification); // counter to determine if locations are processed var locationCount = 0; inputTimeSerieCollection = damProjectData.Dike.InputTimeSerieCollection; outputTimeSerieCollection = new TimeSerieCollection(); IEnumerable locations = damProjectData.Dike.Locations.GetBySpecification(new LocationsWithSensorData()); foreach (Location location in locations) { location.ModelParametersForPlLines.PlLineCreationMethod = PlLineCreationMethod.Sensors; PrepareSensorDataLookup(location); InitializeOutputSeries(location); locationCount++; } damProjectData.CalculationMessages.Add(new LogMessage(LogMessageType.Info, null, $"There are {locationCount} locations with sensor data")); if (!locations.Any()) { damProjectData.CalculationMessages.Add(new LogMessage(LogMessageType.Error, null, "No location to process.")); return; } // Prepare the designCalculatorTasks var operationalCalculatorTasks = new List(); foreach (TimeSerie series in outputTimeSerieCollection.Series) { var entryIndex = 0; foreach (TimeSerieEntry entry in series.Entries) { Location location = locations.First(l => series.LocationId == l.Name); DesignScenario designScenario = null; if (location.Scenarios.Count > 0) { // For Operational only ONE scenario is allowed (decided by Irene/Bernard on 30-07-2020). So that's the one that must be used. designScenario = location.Scenarios[0]; } IDictionary sensorValues = values[entry.DateTime][location]; if (!ContainsMissingValues(sensorValues, series.MissVal)) { FailureMechanismSystemType soilProbabilityFailureMechanismSystemType = damProjectData.DamProjectCalculationSpecification.CurrentSpecification.FailureMechanismSystemType; SoilGeometryProbability soiProfileProbability = location.Segment.GetMostProbableSoilGeometryProbability( ConversionHelper.ConvertToSegmentFailureMechanismType(soilProbabilityFailureMechanismSystemType)); if (soiProfileProbability != null) { string projectPath = damProjectData.ProjectPath != "" ? damProjectData.ProjectPath : Directory.GetCurrentDirectory(); var calculationMessages = new List(); operationalCalculatorTasks.Add(new OperationalCalculatorTask { Location = location, SoiProfileProbability = soiProfileProbability, DesignScenario = designScenario, ProjectPath = projectPath, CalculationMap = damProjectData.CalculationMap, FailureMechanismeCalculationSpecification = damProjectData .DamProjectCalculationSpecification.CurrentSpecification, TimeSerieEntry = entry, TimeStepIndex = entryIndex, CalculationMessages = calculationMessages }); } } else { damProjectData.CalculationMessages.Add(new LogMessage(LogMessageType.Warning, null, $"In location '{location.Name}' missing values are found in timestep {entry.DateTime}")); } entryIndex++; } } if (operationalCalculatorTasks.Count > 0) { // Perform the calculation Parallel.Run(operationalCalculatorTasks, RunOperationalCalculatorTask, null, damProjectData.MaxCalculationCores); foreach (OperationalCalculatorTask operationalCalculatorTask in operationalCalculatorTasks) { damProjectData.CalculationMessages.AddRange(operationalCalculatorTask.CalculationMessages); } damProjectData.OutputTimeSerieCollection = outputTimeSerieCollection; } else { var logMessage = new LogMessage { MessageType = LogMessageType.Error, Subject = null, Message = string.Format(Resources.DesignCalculatorNoSegmentsWithFailureMechanismTypePresent, damProjectData.DamProjectCalculationSpecification.CurrentSpecification.FailureMechanismSystemType.ToString()) }; damProjectData.CalculationMessages.Add(logMessage); } } /// /// Dates to time stamp. /// /// The date time. /// public string DateToTimeStamp(DateTime dateTime) { // Following 2 lines is an example how to handle customization of this format. // TNO at the last moment decided they did not need this change so we change it back to // the default format // string customFormat = "yyyy-MM-dd_HH-mm-ss"; // return dateTime.ToString(customFormat); return dateTime.ToString("s", DateTimeFormatInfo.InvariantInfo); } private void RunOperationalCalculatorTask(object operationalCalculatorTask) { var task = (OperationalCalculatorTask) operationalCalculatorTask; Debug.WriteLine("Start calculation Location '{0}', soilprofile '{1}'", task.Location, task.SoiProfileProbability); CalculateOneTimeEntry(task.Location, task.SoiProfileProbability, task.DesignScenario, task.ProjectPath, task.CalculationMap, task.FailureMechanismeCalculationSpecification, task.TimeSerieEntry, task.TimeStepIndex, task.CalculationMessages, out CalculationResult result); task.CalculationResult = result; Debug.WriteLine("End calculation Location '{0}', soilprofile '{1}'", task.Location, task.SoiProfileProbability); } private void CalculateOneTimeEntry(Location location, SoilGeometryProbability soiProfileProbability, DesignScenario designScenario, string projectPath, string calculationMap, DamFailureMechanismeCalculationSpecification damFailureMechanismCalculationSpecification, TimeSerieEntry timeSerieEntry, int timeStepIndex, List calculationMessages, out CalculationResult calculationResult) { try { calculationResult = CalculationResult.NoRun; // Prepare input var damKernelInput = new DamKernelInput(); damKernelInput.ProjectDir = projectPath; damKernelInput.CalculationDir = Path.Combine(projectPath, calculationMap); damKernelInput.Location = location; //.Copy(); #Bka should be a copy but now impossible due to the dictionary used for SensorLocation damKernelInput.SubSoilScenario = soiProfileProbability.Copy(); damKernelInput.TimeStepDateTime = timeSerieEntry.DateTime.Copy(); damKernelInput.DamFailureMechanismeCalculationSpecification = damFailureMechanismCalculationSpecification; damKernelInput.RiverLevelHigh = Double.NaN; damKernelInput.RiverLevelLow = null; damKernelInput.FilenamePrefix = $"Dik(dike)_Loc({location.Name})_Stp({timeStepIndex})_Mdl({damFailureMechanismCalculationSpecification.StabilityModelType})_{DateToTimeStamp(timeSerieEntry.DateTime)}"; damKernelInput.CurrentEmbankmentSoil = location.GetDikeEmbankmentSoil(); SynchronizeLocationDataWithScenarioData(designScenario, location); IKernelDataInput kernelDataInput; IKernelDataOutput kernelDataOutput; // Create kernelwrapper IKernelWrapper kernelWrapper = KernelWrapperHelper.CreateKernelWrapper(damFailureMechanismCalculationSpecification); if (kernelWrapper == null) { throw new NotImplementedException(Resources.DesignCalculatorKernelNotImplemented); } PrepareResult prepareResult = kernelWrapper.Prepare(damKernelInput, 0, out kernelDataInput, out kernelDataOutput); // Sometimes the kernelDataInput is not created (p.e when soilprofileprobablility is meant for // stability where Piping calc is wanted). In that case, do nothing but just skip. if (prepareResult == PrepareResult.Successful) { lock (kernelWrapper) //"this" makes it single core again ;-). { lock (damKernelInput) { PerformOperationalCalculation( kernelWrapper, kernelDataInput, kernelDataOutput, damKernelInput, timeStepIndex, timeSerieEntry, out calculationResult, calculationMessages); } } } else { if (prepareResult == PrepareResult.NotRelevant) { // Do nothing. Calculation will be skipped. } if (prepareResult == PrepareResult.Failed) { calculationMessages.Add(new LogMessage(LogMessageType.Error, null, string.Format(Resources.OperationalCalculatorPrepareError, location.Name, soiProfileProbability, timeStepIndex, DateToTimeStamp(timeSerieEntry.DateTime)))); if (damFailureMechanismCalculationSpecification.FailureMechanismSystemType != FailureMechanismSystemType.StabilityInside) { return; } var mo = (MacroStabilityOutput) kernelDataOutput; if (mo != null) { calculationMessages.Add(mo.Message); } } } } catch (Exception e) { calculationResult = CalculationResult.RunFailed; calculationMessages.Add(new LogMessage(LogMessageType.Error, null, string.Format(Resources.OperationalCalculatorGeneralException, location.Name, soiProfileProbability, timeStepIndex, DateToTimeStamp(timeSerieEntry.DateTime), e.Message))); } } private void PerformOperationalCalculation(IKernelWrapper kernelWrapper, IKernelDataInput kernelDataInput, IKernelDataOutput kernelDataOutput, DamKernelInput damKernelInput, int timeStepIndex, TimeSerieEntry timeSerieEntry, out CalculationResult calculationResult, List calculationMessages) { // Perform validation var designResults = new List(); var locationCalculationMessages = new List(); List validationMessages; calculationResult = CalculationResult.NoRun; try { int errorCount = kernelWrapper.Validate(kernelDataInput, kernelDataOutput, out validationMessages); if (errorCount > 0) { locationCalculationMessages.Add(new LogMessage(LogMessageType.Error, null, string.Format(Resources.OperationalCalculatorValidationFailed, damKernelInput.Location.Name, damKernelInput.SubSoilScenario, timeStepIndex, DateToTimeStamp(timeSerieEntry.DateTime)))); locationCalculationMessages.AddRange(validationMessages); } else { // Perform calculation kernelWrapper.Execute(kernelDataInput, kernelDataOutput, out locationCalculationMessages); } // Process output calculationMessages.AddRange(locationCalculationMessages); var sb = new StringBuilder(); foreach (LogMessage message in locationCalculationMessages) { sb.Append(message.Message + Environment.NewLine); } var resultMessage = sb.ToString(); calculationResult = CalculationResult.Succeeded; DesignScenario designScenario = damKernelInput.Location.CurrentScenario; kernelWrapper.PostProcess(damKernelInput, kernelDataOutput, designScenario, resultMessage, out designResults); if (designResults.Count > 0) { timeSerieEntry.Value = designResults[0].SafetyFactor.Value; if (damKernelInput.DamFailureMechanismeCalculationSpecification.FailureMechanismSystemType is FailureMechanismSystemType.StabilityInside or FailureMechanismSystemType.StabilityOutside && designResults[0].StabilityDesignResults is { StabilityModelType: MStabModelType.Bishop }) { timeSerieEntry.BishopCircleX = designResults[0].StabilityDesignResults.ActiveCenterPoint.X; timeSerieEntry.BishopCircleZ = designResults[0].StabilityDesignResults.ActiveCenterPoint.Z; timeSerieEntry.BishopRadius = designResults[0].StabilityDesignResults.ActiveCenterPointRadius; } } } catch (Exception exception) { string resultMessage = exception.Message; calculationResult = CalculationResult.RunFailed; kernelWrapper.PostProcess(damKernelInput, kernelDataOutput, null, resultMessage, out designResults); } } private string DetermineOutputParameter(DamFailureMechanismeCalculationSpecification currentSpecification) { var parameter = ""; switch (currentSpecification.FailureMechanismSystemType) { case FailureMechanismSystemType.StabilityOutside: if (currentSpecification.StabilityModelType == MStabModelType.Bishop) { parameter = TimeSerieParameters.StabilityOutsideFactor.ToString(); break; } throw new NotImplementedException(); case FailureMechanismSystemType.StabilityInside: switch (currentSpecification.StabilityModelType) { case MStabModelType.Bishop: case MStabModelType.UpliftVan: case MStabModelType.BishopUpliftVan: parameter = TimeSerieParameters.StabilityInsideFactor.ToString(); break; default: throw new NotImplementedException(); } break; case FailureMechanismSystemType.Piping: switch (currentSpecification.PipingModelType) { case PipingModelType.Bligh: parameter = TimeSerieParameters.PipingFactor.ToString(); break; case PipingModelType.Wti2017: parameter = TimeSerieParameters.PipingFactor.ToString(); break; default: throw new NotImplementedException(); } break; } return parameter; } /// /// Initializes the output series. /// private void InitializeOutputSeries(Location location) { TimeSerie firstTimeSeries = inputTimeSerieCollection.Series.First(); TimeSerie copyOfSeries = firstTimeSeries.GetShallowCopy(); foreach (TimeSerieEntry entry in firstTimeSeries.Entries) { // get copy of the input entry TimeSerieEntry copyOfEntry = entry.GetShallowCopy(); copyOfEntry.Value = double.NaN; // set parameter and location id's and add the projected entry to the output copyOfSeries.LocationId = location.Name; copyOfSeries.ParameterId = outputParameter; copyOfSeries.Entries.Add(copyOfEntry); } // add the output series to the output collection outputTimeSerieCollection.Series.Add(copyOfSeries); } /// /// Prepares the output time series and the calculation arguments (sensor data). /// /// The location. private void PrepareSensorDataLookup(Location location) { ThrowIfSensorsAreMissingInInputFile(location); // these variable are used to determine differences in time series entries var firstSeriesEntries = new HashSet(); var hasFirstSeriesEntries = false; foreach (Sensor sensor in location.SensorLocation.Sensors) { IEnumerable series = inputTimeSerieCollection.GetSeriesByLocationId(sensor.Name); ThrowIfSensorNotExists(sensor, series.Any()); // Prepare the output time series and set sensor values foreach (TimeSerie timeSeries in series) { ThrowIfTimeEntryCountDontMatch(firstSeriesEntries, timeSeries, hasFirstSeriesEntries); foreach (TimeSerieEntry entry in timeSeries.Entries) { DateTime key = entry.DateTime; if (hasFirstSeriesEntries) { ThrowIfTimeEntriesKeysDontMatch(key, firstSeriesEntries); } if (!hasFirstSeriesEntries) { firstSeriesEntries.Add(key); } // everything ok set data into internal lookup SetSensorValue(key, entry.Value, sensor, location); } } hasFirstSeriesEntries = true; } location.SensorLocation.SensorValues = values; // Todo #The: only set sensorvalues to values for this location } /// /// Sets the sensor value. /// /// The time step. /// The value. /// The sensor. /// The location. private void SetSensorValue(DateTime timeStep, double value, Sensor sensor, Location location) { if (!values.ContainsKey(timeStep)) { values.Add(timeStep, new Dictionary>()); } if (!values[timeStep].ContainsKey(location)) { values[timeStep].Add(location, new Dictionary()); } if (values[timeStep][location].ContainsKey(sensor)) { var message = $"Values for sensor with id {sensor.ID} and name {sensor.Name} and location {location.Name} and time step {timeStep} already exists. Check the time series data."; throw new OperationalCalculatorException(message); } values[timeStep][location].Add(sensor, value); } /// /// Throws when the sensor not exists. /// /// The sensor. /// if set to true [has series by sensor ID]. private void ThrowIfSensorNotExists(Sensor sensor, bool hasSeriesBySensorID) { if (!hasSeriesBySensorID) { // TODO log info var message = $"Sensor with name '{sensor.Name}' and parameter id '{TimeSerie.WaterPressureParameterId}' not found in the input time series"; throw new OperationalCalculatorException(message); } } /// /// Throws if time entry count dont match. /// /// The first series entries. /// The time series. /// if set to true [has first series entries]. private void ThrowIfTimeEntryCountDontMatch(HashSet firstSeriesEntries, TimeSerie timeSeries, bool hasFirstSeriesEntries) { if (hasFirstSeriesEntries) { // TODO log info if (timeSeries.Entries.Count != firstSeriesEntries.Count) { throw new OperationalCalculatorException("Invalid data in time series entries. Number of entries differ on each sensor"); } } } private void ThrowIfSensorsAreMissingInInputFile(Location location) { List sensorsNotFound = ( from sensor in location.SensorLocation.Sensors let series = inputTimeSerieCollection.GetSeriesByLocationId(sensor.Name) where !series.Any() select sensor.Name).ToList(); if (sensorsNotFound.Any()) { // TODO log info var message = $"Sensor with name '{string.Join(", ", sensorsNotFound.ToArray())}' and parameter id '{TimeSerie.WaterPressureParameterId}' not found in the input time series"; throw new OperationalCalculatorException(message); } } private void ThrowIfTimeEntriesKeysDontMatch(DateTime key, HashSet firstSeriesEntries) { // TODO log info if (!firstSeriesEntries.Contains(key)) { throw new OperationalCalculatorException("Invalid data in time series entries. Time entries (date time values) don't match"); } } /// /// Determines whether any of sensor values contains a missing value. /// /// The sensor values. /// /// private bool ContainsMissingValues(IDictionary sensorValues, double missingValue) { foreach (KeyValuePair sensorValue in sensorValues) { if (sensorValues[sensorValue.Key].AlmostEquals(missingValue)) { return true; } } return false; } /// /// Synchronizes the location data with scenario data. /// Note that scenario data is leading when available. /// /// The scenario. /// The location. private void SynchronizeLocationDataWithScenarioData(DesignScenario designScenario, Location location) { if (designScenario != null) { location.CurrentScenario = designScenario.CloneInput(); if (designScenario.DikeTableHeight.HasValue) { location.DikeTableHeight = designScenario.DikeTableHeight ?? 0; } } } /// /// Specifies a location with valid sensor data. The objects that are satisfied by the /// the specification (predicate) will be used in this calculator. /// See ReadSensorDataAndPrepareOutputSeries and Locations.GetBySpecification(p) /// private class LocationsWithSensorData : PredicateSpecification { public LocationsWithSensorData() : base(l => l.HasSensorLocation && l.SensorLocation.SensorCount > 0) {} } }