UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Best approach to store 2D data and interpolate between that data

Forums > Swift

Hi,

I am working on an iOS app that needs to do a lot of op data lookup in tables with data, most of the time this data has to be interpolated as well.

Here is an example:

Then when calling a function: GoAroundRefGradient(temp: 28, pressure: 2300) It returns 3.79 from the first table. And GradientAdjustmentWeight(gradient:1.5, weight 64000) returns -0.71 from the second table.

I have around 30 of those tables. What is the best place to store this data, so the function can look up the appropriate entry and do the 3D interpolation? I was thinking of a nested array, but perhaps there is a better option.

Cheers, Bastiaan

4      

Hi Bastiaan!

I am facing a similar issue. Did you come up with a solution yet?

Toine

3      

Hi, I would create static dictionaries and store this that way. For example with the second table, your first key would be the weight and this would give you another dictionary and you would use the "reference go-around gradient" value to get the final number.

It is going to be pain to write, you could possibly check out some tools to generate Swift syntax (https://nshipster.com/swift-gyb/), but it will be quite nice to use, fast and you will see all the values in your source code.

3      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Thanks for the suggestion, but the problem is that these values have to be (linearly) interpolated. I tried to train a ML model, but did not achieve the accuracy that I desired. I think I will stick with dictionaries and interpolate myself!

3      

You are right, dictionaries are probably not the best choice when you need to interpolate across both axis.

3      

hi,

i would suggest thinking of any of these tables as triples of 3D points (x,y,z). given a needed interpolation, locate the four points in the table that surround it and interpolate the four z-coordinates adding up a weighted sum.

first, the arithmetic for your example, using x = 28 and y = 2300: locate the four points in the table that surround it and compute appropriate weights (from 0 to 1) depending on how close the x = 28 and y = 2300 are to the x- and y-coordinates of the points, respectively, like this

  • x = 26, y = 2000, z = 4.19, xWeight = 0.5, yWeight = 0.85, zContribution = 4.19 • 0.5 • 0.85 = 1.78075
  • x = 26, y = 4000, z = 3.15, xWeight = 0.5, yWeight = 0.15, zContribution = 3.15 • 0.5 • 0.15 = 0.23625
  • x = 30, y = 2000, z = 3.69, xWeight = 0.5, yWeight = 0.85, zContribution = 3.69 • 0.5 • 0.85 = 1.56825
  • x = 30, y = 4000, z = 2.68, xWeight = 0.5, yWeight = 0.15, zContribution = 2.68 • 0.5 • 0.15 = 0.201

the sum of the four weighted z-contributions is 3.78625, or the 3.79 you want.

the xWeight contribution is just 1 - abs(28 - x)/(30 - 26), and the yWeight is 1 - abs(2300 - y)/(4000 - 2000).

to turn this into code ... this works in my playground:

typealias DataPoint = (Double,Double,Double)

let dataPoints: [DataPoint] =
    [(30,2000,3.69), (30,4000,2.68),(26,2000,4.19),(26,4000,3.15)]

let x = 28.0
let y = 2300.0

let minX = dataPoints.map({ $0.0 }).min()!
let minY = dataPoints.map({ $0.1 }).min()!
let maxX = dataPoints.map({ $0.0 }).max()!
let maxY = dataPoints.map({ $0.1 }).max()!

var sum = 0.0
for dataPoint in dataPoints {
    let weight = (1 - abs(dataPoint.0 - x) / (maxX - minX)) * (1 - abs(dataPoint.1 - y) / (maxY - minY))
    sum += dataPoint.2 * weight
}
print(sum)

that's one sample computation only; to turn this into a function to support interpolation throughout the table

  • expand the dataPoints array to be the complete data from the table, including z-values of 0 for every blank entry
  • and given an x (28) and y (2300), find the four points in the table that immediately surround it

this second part might sound hard, but i think it will be enough to just find just the four "closest" points that minimize the x and y differences (e.g., the table values x = 26 and x = 30 are the two closest to 28; the table values y = 2000 and y = 4000 are the two closest to y = 2300).

you'll have to handle the boundary cases yourself (e.g., the x and y are already in the table, or the x and y lie outside the range of the table), but that sounds easy.

hope that helps,

DMG

5      

Hi, i have interpolated my data set. I would like to check which method will be best suited for my data. Reference data for cross validation is missing. Is any other method exit with which i can check which interpolation technique will be best suited for my data set?

3      

Would using Core Data, NSSortDescriptors and Predicate be useful here?

Each table would have it own entity type, and have the atttributes xCoord, yCoord, itemValue.

The use Fetch, NSSortDescriptors and NSPredicates to pull out the four corners of the interpolation region, as a set of 4 entities.

Just a thought.

3      

First of all, my apologies for my late response, this part of the project got sidetracked; I picked it up again. Secondly, thank you for you're ideas. Finally, if there are still people who seek a similar solution, I approached it as delawaremathguy suggested.

To show my solution, this is the table used in this example:

First, I put all the data in a struct:

struct BrakeCooling {

    let ReferenceBrakeEnergy700 =
        [[15.3,17.2,19.4,22.9,25.8,29.3,31.7,35.8,40.9,41.5,47.1,54.2,52.2,59.6,69,62.4,71.4,83.3],
            ...
            ...
        [10.9,12.2,13.8,15.4,17.3,19.5,20.4,23,26.1,26,29.4,33.4,32.1,36.4,41.6,38.7,44,50.5]]

    let AdjustedBrakeEnergyNoRev700 =
        [[7.5,15.8,24.6,33.8,43.5,53.5,63.6,73.9,84.2,],
        [7.3,15,23.2,31.9,41.2,51,61.3,72.2,83.7,],
        [7,14.2,21.8,29.7,38.1,47.1,56.7,67.1,78.3,],
        [6.6,13.3,20.2,27.3,34.7,42.6,51,59.9,69.6,],
        [6.3,12.4,18.6,24.9,31.6,38.6,46.2,54.4,63.5,]]

    let AdjustedBrakeEnergyRev700 =
        [[...]]

    let AdjustedBrakeEnergyNoRev800 =
        [[...]]

    let AdjustedBrakeEnergyRev800 =
        [[...]]
}

Then with an if the else I assign the correct table, e.g.:

currentBrake = brakeCooling.ReferenceBrakeEnergy700

Then the tedious job starts to find the range of the columns and rows.

    if groundSpeed < 100 {
      speedIndexLower = 0
      speedIndexUpper = 3
      speedLower = 80
      speedUpper = 100
    } else if groundSpeed > 100 && groundSpeed <= 120 {
      speedIndexLower = 3
      speedIndexUpper = 6
      speedLower = 100
      speedUpper = 120
    } else if groundSpeed > 120 && groundSpeed <= 140 {
      speedIndexLower = 6
      speedIndexUpper = 9
      speedLower = 120
      speedUpper = 140
    } else if groundSpeed > 140 && groundSpeed <= 160 {
      speedIndexLower = 9
      speedIndexUpper = 12
      speedLower = 140
      speedUpper = 160
    } else if groundSpeed > 160 && groundSpeed <= 180 {
      speedIndexLower = 12
      speedIndexUpper = 16
      speedLower = 160
      speedUpper = 180
    }

    if airplane.weight <= 80000 && airplane.weight > 70000 {
      weightIndexUpper = 0
      weightIndexLower = 7
      weightLower = 70000
      weightUpper = 80000

    } else if airplane.weight <= 70000 && airplane.weight > 60000 {
      weightIndexUpper = 7
      weightIndexLower = 14
      weightLower = 70000
      weightUpper = 60000

    } else if airplane.weight <= 60000 && airplane.weight > 50000
    {
      weightIndexUpper = 14
      weightIndexLower = 21
      weightLower = 60000
      weightUpper = 50000

    } else if airplane.weight <= 50000 && airplane.weight > 40000 {
      weightIndexUpper = 21
      weightIndexLower = 28
      weightLower = 50000
      weightUpper = 40000

    }

A simple 2D interpolation is used to find the correct value:

func interpolate2D(x1: Double, x2: Double, y1: Double, y2: Double, x: Double) -> Double {

  let y = y1 + ((y2 - y1) / (x2 - x1)) * (x - x1)
  return y

}

For example:

    let upperSpeed = interpolate2D(x1: speedUpper, x2: speedLower, y1: currentBrake[weightIndexUpper+2][speedIndexUpper], y2: currentBrake[weightIndexUpper+2][speedIndexLower], x: groundSpeed)
    let lowerSpeed = interpolate2D(x1: speedUpper, x2: speedLower, y1: currentBrake[weightIndexLower+2][speedIndexUpper], y2: currentBrake[weightIndexLower+2][speedIndexLower], x: groundSpeed)
    let brakeEnergy = interpolate2D(x1: weightLower, x2: weightUpper, y1: upperSpeed, y2: lowerSpeed, x: Double(airplane.weight))

And so on. Still, I hope there is a more straightforward solution. This operation is, for example in Numbers, or Excel so much easier.

Cheers, Bastiaan

3      

hi Bastiaan,

i am not sure why i have a fascination with this question ... although i do know some math (!) ... and then you threw a monkey wrench into your solution for a different table that requires (apparently) four input parameters (weight, temperature, wind corrected brakes (?), and pressure/altitude). that's a lot different than the simple 2D lookup in the original question.

anyway, it got me to thinking, and i have something for your consideration: a general scheme for looking up values (Doubles) in a table based on a number of input parameters (doubles) that have a fixed range, and for which values are known at certain points of the input range (e.g., there are known values for weight = 40, 50, 60, 70, 80, and for pressure/altitude = 0, 5, 10).

so i suggest a recursive computation, recursing on the number of inputs. that is, you tell me that you need to input temperature and pressure/altitude, and i will compare the temperature against reference temperature points that you have defined (2, 6, 10, 14 , ... 54) and known pressure/altitude points that you have defined (0, 2000, 4000, ... , 10000) and i will ratio out the known values one at a time:

  • first, by an appropriate percentage of where the first input parameter lies within the array of reference temperature points
  • second, by an appropriate percentage of where the second input parameter lies within the array of reference pressure/altitude points
  • etc, even as more parameters are required.

and to make this work, i require you to store the known values for each combination of known reference inputs in a dictionary, keyed by the inputs.

example:

var dictionary = [[Double] : Double]()
dictionary[ [34, 6000] ] = 1.28 // temperature = 34, altitude = 6000

another example:

var dictionary = [[Double] : Double]()
dictionary[ [70, 15, 120, 5] ] = 33.6 // weight = 70, temperature = 15, wind corrected brakes = 120, pressure = 5

so, let's define a protocol for how to do a table lookup of a value based on some number of inputs:

protocol TableLookup {
    var knownInputValues: [[Double]] { get }
    var tableData: [[Double] : Double] { get }
    func interpolatedValue(partialKey: [Double], remainingKeys: [Double]) -> Double
}

the interpolatedValue function, as you will see later, implements recursion on the number of input keys. in fact, it always works the same way, once you get it started.

so for your brake cooling schedule, you define this:

struct BrakeCoolingSchedule: TableLookup {

    var knownInputValues: [[Double]] = [
        [40, 50, 60, 70, 80], // weights
        [0, 10, 15, 20, 30, 40, 50],  // temperatures
        [80, 100, 120, 140, 160, 180],  // wind corrected brakes on speed
        [0, 5, 10] // pressure altitude
    ]

    // all values in the table are defined in a lookup dictionary based on a given
    // weight, temperature, wind-corrected brake measurement, and pressure altitude.
    // for demonstration, these are the sixteen table entries that surround incoming values
    // of weight = 73.5, temperature = 17.9, wcBrakes = 128.7, pressure = 8.3
    var tableData: [[Double] : Double] = [
        [70, 15, 120, 5] : 33.6,
        [70, 15, 140, 5] : 44.1,
        [70, 15, 120, 10] : 38.3,
        [70, 15, 140, 10] : 50.5,
        [70, 20, 120, 5] : 34.2,
        [70, 20, 140, 5] : 44.7,
        [70, 20, 120, 10] : 38.9,
        [70, 20, 140, 10] : 51.3,

        [80, 15, 120, 5] : 37.6,
        [80, 15, 140, 5] : 49.4,
        [80, 15, 120, 10] : 42.9,
        [80, 15, 140, 10] : 56.8,
        [80, 20, 120, 5] : 38.1,
        [80, 20, 140, 5] : 50.1,
        [80, 20, 120, 10] : 43.5,
        [80, 20, 140, 10] : 57.6
    ]

    // main entry point to interpolation
    func interpolatedValue(weight: Double, temperature: Double, wcBrakes: Double, pressure: Double) -> Double {
        // this is a place where you might check whether the incoming values are in range,
        // and if not, force them into range.  then call the recursive function for the
        // computation in the tableData, starting with the partial key of the empty array
        interpolatedValue(partialKey: [], remainingKeys: [weight, temperature, wcBrakes, pressure])
    }

}

and when you want an interpolated value, you do this:

let schedule = BrakeCoolingSchedule()
print(schedule.interpolatedValue(weight: 73.5, temperature: 17.9, wcBrakes: 128.7, pressure: 8.3))

now, interpolatedValue just takes one key at a time (e.g., first the weight key) and does a weighted interpolation of the recursively interpolated values for the keys not yet weighted. e.g.,

interpolatedValue(partialKey: [], remainingKeys: [73.5, 17.9, 128.7, 8.3])

will compute 65% of interpolatedValue(partialKey: [70], remainingKeys: [17.9, 128.7, 8.3]) and 35% of interpolatedValue(partialKey: [80], remainingKeys: [17.9, 128.7, 8.3]).

once the recursion goes 4 levels deep in this case, you'll be looking at a combination of weighted averages of weighted averages ... of weighted averages such as interpolatedValue(partialKey: [70, 15, 120, 5], remainingKeys: []) which will then do a lookup directly.

here's what that looks like:

extension TableLookup {

    // a helper function for implementing the recursive weighting, one parameter at a time
    func interpolatedValue(partialKey: [Double], remainingKeys: [Double]) -> Double {

        // trivial case: the partial key array is now a full key
        if remainingKeys.count == 0 {
            // return value in table for the initial 4-tuple of coordinates (which are Ints)
            //let lookupKey = partialKey.map({ Int($0) })
            if let dataPoint = tableData[partialKey] {
                return dataPoint
            }
            // but not found?  really?
            return 0.0
        }

        // identify bounds on first value in remainingKeys.  you can tell which
        // set of cutoffs we should use based on the length of the partialKey
        let x = remainingKeys[0]
        let cutoffs = knownInputValues[partialKey.count]
        if let higherIndex = cutoffs.firstIndex(where: { $0 > x }) {
            // interpolate between cutoffs[higherIndex - 1] and cutoffs[higherIndex]
            // percentage is the weight given to the lower-side measurement (e.g., a weight
            // measurement of 73.5 is "35% of the way from 70 to 80," so it should be
            // weighted 65% of the lower-indexed value and 35% of the higher0indexed value.
            let percentage = 1.0 - (x - cutoffs[higherIndex - 1]) / (cutoffs[higherIndex] - cutoffs[higherIndex - 1])

            let lowerIndexedInterpolation =
                interpolatedValue(partialKey: partialKey + [cutoffs[higherIndex - 1]],
                                                    remainingKeys: remainingKeys.suffix(remainingKeys.count - 1))
            let upperIndexedInterpolation =
                interpolatedValue(partialKey: partialKey + [cutoffs[higherIndex]],
                                                    remainingKeys: remainingKeys.suffix(remainingKeys.count - 1))
            return percentage * lowerIndexedInterpolation + (1 - percentage) * upperIndexedInterpolation
        }
        // if we fall through to here, then we ran off the table on the current coordinate, so just
        // cap the coordinate
        return interpolatedValue(partialKey: partialKey + [cutoffs[cutoffs.count - 1]],
                                                         remainingKeys: remainingKeys.suffix(remainingKeys.count - 1))

    }
}

i think this works, based on my limited testing ... but if i have missed something, i'm not very far off. recursion is a tricky thing and very mathematical ... sometimes the best recursive routines are the ones that don't seem to do very much.

the only requirement to then implement any table lookup is to define a struct that adopts the TableLookup protocol, define its dictionary entries, its known input values on which the data entries are based, and define a beginning interpolatedValue function that's unique to your lookup.

wow ... just could not let this one go ...

hope that helps,

DMG

3      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.