/// <summary> /// Calculate the thermocline depth /// </summary> /// <param name="dataset">The dataset to use</param> /// <param name="mixedTempDifferential">The minimum mixed temp differnetial</param> /// <param name="preCalculatedDensities">The set of precalculated densities</param> /// <param name="seasonal">Whether or not to look check that values aren't seasonal</param> /// <param name="minimumMetalimionSlope">The minimum metalimion slope</param> /// <returns></returns> public static Dictionary<DateTime, ThermoclineDepthDetails> CalculateThermoclineDepth(Dataset dataset, double mixedTempDifferential = 0, IEnumerable<DensitySeries> preCalculatedDensities = null, bool seasonal = false, float minimumMetalimionSlope = 0.1f) { var thermocline = new Dictionary<DateTime, ThermoclineDepthDetails>(); var densities = (preCalculatedDensities == null) ? CalculateDensity(dataset).OrderBy(x => x.Depth).ToArray() : preCalculatedDensities.OrderBy(x => x.Depth).ToArray(); var densityColumns = GenerateDensityColumns(densities); var timeStamps = densityColumns.Keys.ToArray(); foreach (var t in timeStamps) { var thermoclineDetails = new ThermoclineDepthDetails(); var depths = densityColumns[t].Keys.OrderBy(x => x).ToArray(); if (depths.Length < 3) //We need at least 3 depths to calculate continue; if (mixedTempDifferential > 0) { var orderedSensors = dataset.Sensors.Where(x => x.SensorType == "Water_Temperature").OrderBy(x => x.Elevation).ToArray(); if (orderedSensors.Any()) { var first = orderedSensors.First(x => x.CurrentState.Values.ContainsKey(t)); var last = orderedSensors.Last(x => x.CurrentState.Values.ContainsKey(t)); if (first != null && last != null) { if (first.CurrentState.Values[t] - last.CurrentState.Values[t] <= mixedTempDifferential) continue; } } } var slopes = new double[depths.Length]; for (var i = 1; i < depths.Length - 1; i++) { slopes[i] = (densityColumns[t][depths[i + 1]] - densityColumns[t][depths[i]]) / (depths[i + 1] - depths[i]); } thermoclineDetails.DrhoDz = slopes; var maxSlope = slopes.Max(); var indexOfMaxium = Array.IndexOf(slopes, maxSlope); thermoclineDetails.ThermoclineIndex = indexOfMaxium; thermoclineDetails.ThermoclineDepth = (depths[indexOfMaxium] + depths[indexOfMaxium + 1]) / 2; if (indexOfMaxium > 1 && indexOfMaxium < depths.Length - 2) { var sdn = -(depths[indexOfMaxium + 1] - depths[indexOfMaxium]) / (slopes[indexOfMaxium + 1] - slopes[indexOfMaxium]); var sup = (depths[indexOfMaxium] - depths[indexOfMaxium - 1]) / (slopes[indexOfMaxium] - slopes[indexOfMaxium - 1]); var upD = depths[indexOfMaxium]; var dnD = depths[indexOfMaxium + 1]; if (!(double.IsInfinity(sdn) || double.IsInfinity(sup) || double.IsNaN(sdn) || double.IsNaN(sup))) { thermoclineDetails.ThermoclineDepth = (float)(dnD * (sdn / (sdn + sup)) + upD * (sup / (sdn + sup))); } } if (seasonal) { const float minPercentageForUniqueTheroclineStep = 0.15f; var minCutPoint = Math.Max(minPercentageForUniqueTheroclineStep * maxSlope, minimumMetalimionSlope); var localPeaks = LocalPeaks(slopes, minCutPoint); if (localPeaks.Any()) { var indexOfSeasonallyAdjustedMaximum = Array.IndexOf(slopes, localPeaks.Last()); if (indexOfSeasonallyAdjustedMaximum > indexOfMaxium + 1) { thermoclineDetails.SeasonallyAdjustedThermoclineIndex = indexOfSeasonallyAdjustedMaximum; thermoclineDetails.SeasonallyAdjustedThermoclineDepth = (depths[indexOfSeasonallyAdjustedMaximum] + depths[indexOfSeasonallyAdjustedMaximum + 1]) / 2; if (indexOfSeasonallyAdjustedMaximum > 1 && indexOfSeasonallyAdjustedMaximum < depths.Length - 2) { var sdn = -(depths[indexOfSeasonallyAdjustedMaximum + 1] - depths[indexOfSeasonallyAdjustedMaximum]) / (slopes[indexOfSeasonallyAdjustedMaximum + 1] - slopes[indexOfSeasonallyAdjustedMaximum]); var sup = (depths[indexOfSeasonallyAdjustedMaximum] - depths[indexOfSeasonallyAdjustedMaximum - 1]) / (slopes[indexOfSeasonallyAdjustedMaximum] - slopes[indexOfSeasonallyAdjustedMaximum - 1]); var upD = depths[indexOfSeasonallyAdjustedMaximum]; var dnD = depths[indexOfSeasonallyAdjustedMaximum + 1]; if (!(double.IsInfinity(sdn) || double.IsInfinity(sup) || double.IsNaN(sdn) || double.IsNaN(sup))) { thermoclineDetails.SeasonallyAdjustedThermoclineDepth = (float)(dnD * (sdn / (sdn + sup)) + upD * (sup / (sdn + sup))); } } } else { thermoclineDetails.NoSeasonalFound(); } } else { thermoclineDetails.NoSeasonalFound(); } } else { thermoclineDetails.NoSeasonalFound(); } thermocline[t] = thermoclineDetails; } return thermocline; }
/// <summary> /// Calculate the thermocline depth /// </summary> /// <param name="dataset">The dataset to use</param> /// <param name="mixedTempDifferential">The minimum mixed temp differnetial</param> /// <param name="preCalculatedDensities">The set of precalculated densities</param> /// <param name="seasonal">Whether or not to look check that values aren't seasonal</param> /// <param name="minimumMetalimionSlope">The minimum metalimion slope</param> /// <returns></returns> public static Dictionary <DateTime, ThermoclineDepthDetails> CalculateThermoclineDepth(Dataset dataset, double mixedTempDifferential = 0, IEnumerable <DensitySeries> preCalculatedDensities = null, bool seasonal = false, float minimumMetalimionSlope = 0.1f) { var thermocline = new Dictionary <DateTime, ThermoclineDepthDetails>(); var densities = (preCalculatedDensities == null) ? CalculateDensity(dataset).OrderBy(x => x.Depth).ToArray() : preCalculatedDensities.OrderBy(x => x.Depth).ToArray(); var densityColumns = GenerateDensityColumns(densities); var timeStamps = densityColumns.Keys.ToArray(); foreach (var t in timeStamps) { var thermoclineDetails = new ThermoclineDepthDetails(); var depths = densityColumns[t].Keys.OrderBy(x => x).ToArray(); if (depths.Length < 3) //We need at least 3 depths to calculate { continue; } if (mixedTempDifferential > 0) { var orderedSensors = dataset.Sensors.Where(x => x.SensorType == "Water_Temperature").OrderBy(x => x.Elevation).ToArray(); if (orderedSensors.Any()) { var first = orderedSensors.First(x => x.CurrentState.Values.ContainsKey(t)); var last = orderedSensors.Last(x => x.CurrentState.Values.ContainsKey(t)); if (first != null && last != null) { if (first.CurrentState.Values[t] - last.CurrentState.Values[t] <= mixedTempDifferential) { continue; } } } } var slopes = new double[depths.Length]; for (var i = 1; i < depths.Length - 1; i++) { slopes[i] = (densityColumns[t][depths[i + 1]] - densityColumns[t][depths[i]]) / (depths[i + 1] - depths[i]); } thermoclineDetails.DrhoDz = slopes; var maxSlope = slopes.Max(); var indexOfMaxium = Array.IndexOf(slopes, maxSlope); thermoclineDetails.ThermoclineIndex = indexOfMaxium; thermoclineDetails.ThermoclineDepth = (depths[indexOfMaxium] + depths[indexOfMaxium + 1]) / 2; if (indexOfMaxium > 1 && indexOfMaxium < depths.Length - 2) { var sdn = -(depths[indexOfMaxium + 1] - depths[indexOfMaxium]) / (slopes[indexOfMaxium + 1] - slopes[indexOfMaxium]); var sup = (depths[indexOfMaxium] - depths[indexOfMaxium - 1]) / (slopes[indexOfMaxium] - slopes[indexOfMaxium - 1]); var upD = depths[indexOfMaxium]; var dnD = depths[indexOfMaxium + 1]; if (!(double.IsInfinity(sdn) || double.IsInfinity(sup) || double.IsNaN(sdn) || double.IsNaN(sup))) { thermoclineDetails.ThermoclineDepth = (float)(dnD * (sdn / (sdn + sup)) + upD * (sup / (sdn + sup))); } } if (seasonal) { const float minPercentageForUniqueTheroclineStep = 0.15f; var minCutPoint = Math.Max(minPercentageForUniqueTheroclineStep * maxSlope, minimumMetalimionSlope); var localPeaks = LocalPeaks(slopes, minCutPoint); if (localPeaks.Any()) { var indexOfSeasonallyAdjustedMaximum = Array.IndexOf(slopes, localPeaks.Last()); if (indexOfSeasonallyAdjustedMaximum > indexOfMaxium + 1) { thermoclineDetails.SeasonallyAdjustedThermoclineIndex = indexOfSeasonallyAdjustedMaximum; thermoclineDetails.SeasonallyAdjustedThermoclineDepth = (depths[indexOfSeasonallyAdjustedMaximum] + depths[indexOfSeasonallyAdjustedMaximum + 1]) / 2; if (indexOfSeasonallyAdjustedMaximum > 1 && indexOfSeasonallyAdjustedMaximum < depths.Length - 2) { var sdn = -(depths[indexOfSeasonallyAdjustedMaximum + 1] - depths[indexOfSeasonallyAdjustedMaximum]) / (slopes[indexOfSeasonallyAdjustedMaximum + 1] - slopes[indexOfSeasonallyAdjustedMaximum]); var sup = (depths[indexOfSeasonallyAdjustedMaximum] - depths[indexOfSeasonallyAdjustedMaximum - 1]) / (slopes[indexOfSeasonallyAdjustedMaximum] - slopes[indexOfSeasonallyAdjustedMaximum - 1]); var upD = depths[indexOfSeasonallyAdjustedMaximum]; var dnD = depths[indexOfSeasonallyAdjustedMaximum + 1]; if (!(double.IsInfinity(sdn) || double.IsInfinity(sup) || double.IsNaN(sdn) || double.IsNaN(sup))) { thermoclineDetails.SeasonallyAdjustedThermoclineDepth = (float)(dnD * (sdn / (sdn + sup)) + upD * (sup / (sdn + sup))); } } } else { thermoclineDetails.NoSeasonalFound(); } } else { thermoclineDetails.NoSeasonalFound(); } } else { thermoclineDetails.NoSeasonalFound(); } thermocline[t] = thermoclineDetails; } return(thermocline); }