import Foundation extension Array3DFastTime { /// Fill in missing data by interpolating using differnet interpolation types /// /// Important: Backwards sums like precipitation must be deaveraged before AND should already have a corrected sum. The interpolation code will simply copy the array value of the next element WITHOUT dividing by `dt`. Meaning a 6 hour preciptation value should be devided by 2 before, to preserve the rum correctly /// /// interpolate missing steps.. E.g. `DDDDDD-D-D-D-D-D` /// Automatically detects data spacing `--D--D--D` for deaverging or backfilling mutating func interpolateInplace(type: ReaderInterpolation, time: TimerangeDt, grid: Gridable, locationRange: any RandomAccessCollection) { precondition(nTime == time.count) data.interpolateInplace(type: type, time: time, grid: grid, locationRange: locationRange) } } extension Array2DFastTime { /// Fill in missing data by interpolating using differnet interpolation types /// /// Important: Backwards sums like precipitation must be deaveraged before AND should already have a corrected sum. The interpolation code will simply copy the array value of the next element WITHOUT dividing by `dt`. Meaning a 6 hour preciptation value should be devided by 2 before, to preserve the rum correctly /// /// interpolate missing steps.. E.g. `DDDDDD-D-D-D-D-D` /// Automatically detects data spacing `--D--D--D` for deaverging or backfilling mutating func interpolateInplace(type: ReaderInterpolation, time: TimerangeDt, grid: Gridable, locationRange: any RandomAccessCollection) { precondition(nTime == time.count) data.interpolateInplace(type: type, time: time, grid: grid, locationRange: locationRange) } } extension Array where Element == Float { /// Fill in missing data by interpolating using differnet interpolation types /// /// Important: Backwards sums like precipitation must be deaveraged before AND should already have a corrected sum. The interpolation code will simply copy the array value of the next element WITHOUT dividing by `dt`. Meaning a 6 hour preciptation value should be devided by 2 before, to preserve the rum correctly /// /// interpolate missing steps.. E.g. `DDDDDD-D-D-D-D-D` /// Automatically detects data spacing `--D--D--D` for deaverging or backfilling mutating func interpolateInplace(type: ReaderInterpolation, time: TimerangeDt, grid: Gridable, locationRange: any RandomAccessCollection) { switch type { case .linear: interpolateInplaceLinear(nTime: time.count) case .linearDegrees: interpolateInplaceLinearDegrees(nTime: time.count) case .hermite(let bounds): interpolateInplaceHermite(nTime: time.count, bounds: bounds) case .solar_backwards_averaged: interpolateInplaceSolarBackwards(time: time, grid: grid, locationRange: locationRange, missingValuesAreBackwardsAveraged: true) case .solar_backwards_missing_not_averaged: interpolateInplaceSolarBackwards(time: time, grid: grid, locationRange: locationRange, missingValuesAreBackwardsAveraged: false) case .backwards_sum: interpolateInplaceBackwards(nTime: time.count, isSummation: true) case .backwards: interpolateInplaceBackwards(nTime: time.count, isSummation: false) } } /// Interpolate missing values, but taking the next valid value /// Automatically detects data spacing. e.g. `--D--D--D` and correctly backfills mutating func interpolateInplaceBackwards(nTime: Int, isSummation: Bool) { precondition(nTime <= self.count) precondition(self.count % nTime == 0) let nLocations = self.count / nTime for l in 0.. 180 && value < previousValue) ? value + 360 : value let B2 = (abs(previousValue-value) > 180 && value > previousValue) ? previousValue + 360 : previousValue let h = A2 * fraction + B2 * (1 - fraction) self[l * nTime + t] = h.truncatingRemainder(dividingBy: 360) } break } } } } /// Interpolate missing values by seeking for the next valid value and perform a hermite interpolation mutating func interpolateInplaceHermite(nTime: Int, bounds: ClosedRange?) { precondition(nTime <= self.count) precondition(self.count % nTime == 0) let nLocations = self.count / nTime for l in 0..= 0 ? posB - width : posB let B = self[l * nTime + posB] let A = self[l * nTime + posA] let a = -A/2.0 + (3.0*B)/2.0 - (3.0*C)/2.0 + D/2.0 let b = A - (5.0*B)/2.0 + 2.0*C - D / 2.0 let c = -A/2.0 + C/2.0 let d = B // Fill up all missing values until point C for t in t.., missingValuesAreBackwardsAveraged: Bool) { let nTime = time.count precondition(nTime <= self.count) precondition(self.count % nTime == 0) let nLocations = self.count / nTime precondition(locationRange.count <= nLocations) precondition(nLocations % locationRange.count == 0) // If no values are missing, return and do not calculate solar coefficients guard let firstMissing = self[0..= 0 && posA - sLow >= 0 && !self[l * nTime + posA].isNaN let posDValid = posD < nTime && posD - sLow < solarTime.count && !self[l * nTime + posD].isNaN let B = self[l * nTime + posB] let solB = solar2d[sPos, posB - sLow] let solC = solar2d[sPos, posC - sLow] /// solAvgC is an average of the solar factor from posB until posC let solAvgC = missingValuesAreBackwardsAveraged ? solar2d[sPos, posB + 1 - sLow ..< posC + 1 - sLow].mean() : solC /// clearness index at point C. At low radiation levels it is impossible to estimate KT indices, set to NaN var ktC = solAvgC <= radMinium ? .nan : Swift.min(C / solAvgC, radLimit) /// Clearness index at point B, or use `ktC` for low radiation levels. B could be NaN if data is immediately missing in the beginning of a time-series var ktB = solB <= radMinium || B.isNaN ? ktC : Swift.min(B / solB, radLimit) var ktA, ktD: Float if posDValid && posAValid { // 4 point Hermite kt interpolation let A = self[l * nTime + posA] let D = self[l * nTime + posD] let solA = solar2d[sPos, posA - sLow] ktA = solA <= radMinium ? ktB : Swift.min(A / solA, radLimit) /// solD is an average of the solar factor from posC until posD let solAvgD = missingValuesAreBackwardsAveraged ? solar2d[sPos, posC + 1 - sLow ..< posD + 1 - sLow].mean() : solar2d[sPos, posD - sLow] ktD = solAvgD <= radMinium ? ktC : Swift.min(D / solAvgD, radLimit) } else { // 2 point linear kt interpolation ktA = ktB ktD = ktC } if ktC.isNaN && ktB > 0 { ktC = ktB } if ktC.isNaN && ktA > 0 { ktB = ktA ktC = ktA } // Especially for 6h values, aggressively try to find any KT index that works // As a future improvement, the clear-sky radiation could be approximated by cloud cover total as an additional input // This could improve morning/evening kt approximations if ktC.isNaN && ktD > 0 { ktA = ktD ktB = ktD ktC = ktD } let a = -ktA/2.0 + (3.0*ktB)/2.0 - (3.0*ktC)/2.0 + ktD/2.0 let b = ktA - (5.0*ktB)/2.0 + 2.0*ktC - ktD / 2.0 let c = -ktA/2.0 + ktC/2.0 let d = ktB // Fill up all missing values until point C for t in t..