// Copyright (C) Stichting Deltares 2019. 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.Properties; using Deltares.DamEngine.Data.Design; using Deltares.DamEngine.Data.General; using Deltares.DamEngine.Data.General.Results; using Deltares.DamEngine.Data.General.Specifications.Extensions; using Deltares.DamEngine.Data.General.Specifications; using Deltares.DamEngine.Data.General.TimeSeries; using Deltares.DamEngine.Data.General.Sensors; 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 { /// /// 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) { } } /// /// 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 int locationCount = 0; inputTimeSerieCollection = damProjectData.Dike.InputTimeSerieCollection; outputTimeSerieCollection = new TimeSerieCollection(); var locations = damProjectData.Dike.Locations.GetBySpecification(new LocationsWithSensorData()); foreach (var location in locations) { location.ModelParametersForPlLines.PlLineCreationMethod = PlLineCreationMethod.Sensors; PrepareSensorDataLookup(location); InitializeOutputSeries(location); locationCount++; } damProjectData.CalculationMessages.Add(new LogMessage(LogMessageType.Info, null, string.Format("There are {0} locations with sensor data", locationCount))); if (!locations.Any()) { damProjectData.CalculationMessages.Add(new LogMessage(LogMessageType.Error, null, "No location to process.")); return; } // Prepare the designCalculatorTasks var operationalCalculatorTasks = new List(); foreach (var series in outputTimeSerieCollection.Series) { int entryIndex = 0; foreach (var entry in series.Entries) { var location = locations.First(l => series.LocationId == l.Name); DesignScenario designScenario = null; if (location.Scenarios.Count > 0) { designScenario = location.Scenarios[entryIndex]; designScenario.Location = location; } var sensorValues = values[entry.DateTime][location]; if (!ContainsMissingValues(sensorValues, series.MissVal)) { FailureMechanismSystemType soilProbabilityFailureMechanismSystemType = damProjectData.DamProjectCalculationSpecification.CurrentSpecification.FailureMechanismSystemType; var soiProfileProbability = location.Segment.GetMostProbableSoilGeometryProbability( ConversionHelper.ConvertToSegmentFailureMechanismType(soilProbabilityFailureMechanismSystemType)); if (soiProfileProbability != null) { var 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, String.Format("In location '{0}' missing values are found in timestep {1}", location.Name, entry.DateTime))); } entryIndex++; } } if (operationalCalculatorTasks.Count > 0) { // Perform the calculation Parallel.Run(operationalCalculatorTasks, RunOperationalCalculatorTask, null, damProjectData.MaxCalculationCores); foreach (var 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); } } private void RunOperationalCalculatorTask(object operationalCalculatorTask) { OperationalCalculatorTask 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); 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 damFailureMechanismeCalculationSpecification, TimeSerieEntry timeSerieEntry, int timeStepIndex, List calculationMessages) { try { // Prepare input var damKernelInput = new DamKernelInput(); damKernelInput.ProjectDir = projectPath; damKernelInput.CalculationDir = Path.Combine(projectPath, calculationMap); damKernelInput.Location = location; damKernelInput.SubSoilScenario = soiProfileProbability; damKernelInput.TimeStepDateTime = timeSerieEntry.DateTime; damKernelInput.DamFailureMechanismeCalculationSpecification = damFailureMechanismeCalculationSpecification; damKernelInput.RiverLevelHigh = Double.NaN; damKernelInput.RiverLevelLow = null; damKernelInput.FilenamePrefix = String.Format("Dik(dike)_Loc({0})_Stp({1})_Mdl({2})_{3}", location.Name, timeStepIndex, damFailureMechanismeCalculationSpecification.StabilityModelType, DateToTimeStamp(timeSerieEntry.DateTime)); SynchronizeScenarioDataWithLocationData(designScenario, location); IKernelDataInput kernelDataInput; IKernelDataOutput kernelDataOutput; // Create kernelwrapper IKernelWrapper kernelWrapper = KernelWrapperHelper.CreateKernelWrapper(damFailureMechanismeCalculationSpecification); 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) { PerformOperationalCalculation( kernelWrapper, kernelDataInput, kernelDataOutput, damKernelInput, timeStepIndex, timeSerieEntry, out var 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)))); } } } catch (Exception e) { 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(); List 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); StringBuilder sb = new StringBuilder(); foreach (var message in locationCalculationMessages) { sb.Append(message.Message + Environment.NewLine); } string resultMessage = sb.ToString(); calculationResult = CalculationResult.Succeeded; kernelWrapper.PostProcess(damKernelInput, kernelDataOutput, null, resultMessage, out designResults); timeSerieEntry.Value = designResults[0].SafetyFactor.Value; } catch (Exception exception) { string resultMessage = exception.Message; calculationResult = CalculationResult.RunFailed; kernelWrapper.PostProcess(damKernelInput, kernelDataOutput, null, resultMessage, out designResults); } } /// /// Dates to time stamp. /// /// The date time. /// public static 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 string DetermineOutputParameter(DamFailureMechanismeCalculationSpecification currentSpecification) { string parameter = ""; switch (currentSpecification.FailureMechanismSystemType) { case FailureMechanismSystemType.HorizontalBalance: throw new NotImplementedException(); 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.StabilityInsideFactor.ToString(); ##Bka 2020-07-13 // break; case PipingModelType.SellmeijerVnk: case PipingModelType.Sellmeijer4Forces: throw new NotImplementedException(); case PipingModelType.Wti2017: parameter = TimeSerieParameters.StabilityInsideFactor.ToString(); break; } break; } return parameter; } /// /// Initializes the output series. /// private void InitializeOutputSeries(Location location) { var firstTimeSeries = inputTimeSerieCollection.Series.First(); var copyOfSeries = firstTimeSeries.GetShallowCopy(); foreach (var entry in firstTimeSeries.Entries) { // get copy of the input entry var copyOfEntry = entry.GetShallowCopy(); // 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(); bool hasFirstSeriesEntries = false; foreach (var 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 (var timeSeries in series) { ThrowIfTimeEntryCountDontMatch(firstSeriesEntries, timeSeries, hasFirstSeriesEntries); foreach (var entry in timeSeries.Entries) { var 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 = string.Format("Values for sensor with id {0} and name {1} and location {2} and time step {3} already exists. Check the time series data.", sensor.ID, sensor.Name, location.Name, timeStep); 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 static void ThrowIfSensorNotExists(Sensor sensor, bool hasSeriesBySensorID) { if (!hasSeriesBySensorID) { // TODO log info string message = string.Format("Sensor with name '{0}' and parameter id '{1}' not found in the input time series", sensor.Name, TimeSerie.WaterPressureParameterId); 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 static 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) { var 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 string message = string.Format("Sensor with name '{0}' and parameter id '{1}' not found in the input time series", string.Join(", ", sensorsNotFound.ToArray()), TimeSerie.WaterPressureParameterId); throw new OperationalCalculatorException(message); } } private static 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 (var sensorValue in sensorValues) { if (sensorValues[sensorValue.Key].AlmostEquals(missingValue)) { return true; } } return false; } /// /// Synchronizes the scenario data with location data. /// Note that scenario data is leading when available. /// /// The scenario. /// The location. private void SynchronizeScenarioDataWithLocationData(DesignScenario designScenario, Location location) { if (designScenario != null) { location.ModelFactors.RequiredSafetyFactorStabilityInnerSlope = designScenario.RequiredSafetyFactorStabilityInnerSlope; location.ModelFactors.RequiredSafetyFactorStabilityOuterSlope = designScenario.RequiredSafetyFactorStabilityOuterSlope; location.ModelFactors.UpliftCriterionStability = designScenario.UpliftCriterionStability; location.ModelFactors.RequiredSafetyFactorPiping = designScenario.RequiredSafetyFactorPiping; location.ModelFactors.UpliftCriterionPiping = designScenario.UpliftCriterionPiping; if (designScenario.DikeTableHeight.HasValue) { location.DikeTableHeight = designScenario.DikeTableHeight ?? 0; } } } } }