Begin statistics creation
This commit is contained in:
200
CHDataManagement/Workouts/Point.swift
Normal file
200
CHDataManagement/Workouts/Point.swift
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
enum SamplingMode {
|
||||
case cumulative // e.g., distance, energy
|
||||
case instantaneous // e.g., heart rate, altitude
|
||||
}
|
||||
|
||||
struct Point: Identifiable, Equatable, Hashable {
|
||||
|
||||
var x, y: Double
|
||||
|
||||
var id: Double { x }
|
||||
|
||||
/**
|
||||
Interpolate the y value at an x coordinate toward another point.
|
||||
*/
|
||||
func interpolate(to other: Point, at location: Double) -> Double {
|
||||
let totalX = other.x - x
|
||||
if totalX == 0 {
|
||||
return (y + other.y) / 2
|
||||
}
|
||||
let diffX = location - x
|
||||
if diffX == 0 {
|
||||
return y
|
||||
}
|
||||
let ratio = diffX / totalX
|
||||
return y + (other.y - y) * ratio
|
||||
}
|
||||
|
||||
static let zero = Point(x: 0, y: 0)
|
||||
}
|
||||
|
||||
extension Array where Element == Point {
|
||||
|
||||
func resample(numberOfSamples: Int, minX: Double? = nil, maxX: Double? = nil, mode: SamplingMode) -> [Point] {
|
||||
guard count >= 2, numberOfSamples > 0 else { return [] }
|
||||
let firstX = minX ?? first!.x
|
||||
let lastX = maxX ?? last!.x
|
||||
let totalDuration = lastX - firstX
|
||||
guard totalDuration > 0 else { return [] }
|
||||
|
||||
let interval = totalDuration / Double(numberOfSamples)
|
||||
var result: [Point] = .init(repeating: .zero, count: numberOfSamples)
|
||||
|
||||
var currentIndex = 0
|
||||
var current = self[0]
|
||||
|
||||
for i in 0..<numberOfSamples {
|
||||
let startOfInterval = Double(i) * interval
|
||||
let endOfInterval = startOfInterval + interval
|
||||
|
||||
var accumulated = 0.0
|
||||
var accumulatedDuration = 0.0
|
||||
|
||||
while currentIndex < count - 1 {
|
||||
let next = self[currentIndex + 1]
|
||||
|
||||
if next.x <= endOfInterval {
|
||||
let dt = next.x - current.x
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
accumulated += next.y - current.y
|
||||
case .instantaneous:
|
||||
accumulated += (current.y + next.y) / 2 * dt
|
||||
accumulatedDuration += dt
|
||||
}
|
||||
currentIndex += 1
|
||||
current = next
|
||||
} else {
|
||||
// partial segment at the interval end
|
||||
let dt = endOfInterval - current.x
|
||||
let slope = (next.y - current.y) / (next.x - current.x)
|
||||
let interpolatedY = current.y + slope * dt
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
accumulated += interpolatedY - current.y
|
||||
case .instantaneous:
|
||||
accumulated += (current.y + interpolatedY) / 2 * dt
|
||||
accumulatedDuration += dt
|
||||
}
|
||||
current = Point(x: endOfInterval, y: interpolatedY)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result[i].x = (startOfInterval + endOfInterval) / 2 // midpoint
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
result[i].y = accumulated / interval // average rate over interval
|
||||
case .instantaneous:
|
||||
result[i].y = accumulatedDuration > 0 ? accumulated / accumulatedDuration : current.y
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func resample(
|
||||
numberOfSamples: Int,
|
||||
minX: Double? = nil,
|
||||
maxX: Double? = nil,
|
||||
mode: SamplingMode
|
||||
) -> [Double] {
|
||||
guard count >= 2, numberOfSamples > 0 else { return [] }
|
||||
let minX = minX ?? first!.x
|
||||
let maxX = maxX ?? last!.x
|
||||
let totalDuration = maxX - minX
|
||||
guard totalDuration > 0 else { return [] }
|
||||
|
||||
let interval = totalDuration / Double(numberOfSamples)
|
||||
var result = [Double](repeating: 0.0, count: numberOfSamples)
|
||||
|
||||
var currentIndex = 0
|
||||
var current = self[0]
|
||||
|
||||
for i in 0..<numberOfSamples {
|
||||
let startOfInterval = minX + Double(i) * interval
|
||||
let endOfInterval = startOfInterval + interval
|
||||
|
||||
var accumulated = 0.0
|
||||
var accumulatedDuration = 0.0
|
||||
|
||||
while currentIndex < count - 1 {
|
||||
let next = self[currentIndex + 1]
|
||||
|
||||
if next.x <= endOfInterval {
|
||||
let dt = next.x - current.x
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
accumulated += next.y - current.y
|
||||
case .instantaneous:
|
||||
accumulated += (current.y + next.y) / 2 * dt
|
||||
accumulatedDuration += dt
|
||||
}
|
||||
currentIndex += 1
|
||||
current = next
|
||||
} else {
|
||||
let dt = endOfInterval - current.x
|
||||
let slope = (next.y - current.y) / (next.x - current.x)
|
||||
let interpolatedY = current.y + slope * dt
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
accumulated += interpolatedY - current.y
|
||||
case .instantaneous:
|
||||
accumulated += (current.y + interpolatedY) / 2 * dt
|
||||
accumulatedDuration += dt
|
||||
}
|
||||
current = Point(x: endOfInterval, y: interpolatedY)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let value: Double
|
||||
switch mode {
|
||||
case .cumulative:
|
||||
value = accumulated / interval
|
||||
case .instantaneous:
|
||||
value = accumulatedDuration > 0 ? accumulated / accumulatedDuration : current.y
|
||||
}
|
||||
result[i] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence {
|
||||
/// Resamples any sequence of elements into evenly spaced intervals.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - numberOfSamples: Number of output points
|
||||
/// - xSelector: Closure to get the independent variable (e.g., time)
|
||||
/// - ySelector: Closure to get the dependent variable (e.g., distance, heart rate)
|
||||
/// - mode: .cumulative or .instantaneous
|
||||
/// - Returns: Array of Points with evenly spaced `x` and averaged `y`
|
||||
func resample(
|
||||
numberOfSamples: Int,
|
||||
x: (Element) -> Double,
|
||||
y: (Element) -> Double,
|
||||
mode: SamplingMode
|
||||
) -> [Point] {
|
||||
let points = self.map { Point(x: x($0), y: y($0)) }
|
||||
return points.resample(numberOfSamples: numberOfSamples, mode: mode)
|
||||
}
|
||||
|
||||
func resample(
|
||||
numberOfSamples: Int,
|
||||
minX: Double? = nil,
|
||||
maxX: Double? = nil,
|
||||
x: (Element) -> Double,
|
||||
y: (Element) -> Double,
|
||||
mode: SamplingMode
|
||||
) -> [Double] {
|
||||
let points = self.map { Point(x: x($0), y: y($0)) }
|
||||
return points.resample(
|
||||
numberOfSamples: numberOfSamples,
|
||||
minX: minX,
|
||||
maxX: maxX,
|
||||
mode: mode)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user