refactor(buffer): MIN_MAX tier binning, remove MEAN lod mode
- TieredBuffer: replace MEAN binning with MIN_MAX (min+max per bin). Mathematically composable: MIN(bin_mins)=global_MIN, MAX(bin_maxes)=global_MAX. Eliminates tier-boundary visual artifacts. TIER1/TIER2 capacity doubled (2 samples/bin). - LodMode: remove MEAN. Only MIN_MAX and LTTB remain. MEAN introduced values never present in raw signal — disinformation for sensor data. - ChartConfig: default lodMode changed to MIN_MAX. - Version: 0.4.1-SNAPSHOT → 0.5.0-SNAPSHOT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b9ee5ddfa3
commit
554b67e630
5 changed files with 27 additions and 37 deletions
|
|
@ -7,7 +7,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.dtrentin"
|
group = "dev.dtrentin"
|
||||||
version = "0.4.1-SNAPSHOT"
|
version = "0.5.0-SNAPSHOT"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
androidTarget {
|
androidTarget {
|
||||||
|
|
|
||||||
|
|
@ -44,25 +44,11 @@ class LodDecimator(
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
LodMode.MEAN -> decimateMean(n, pixelWidth, outX, outY)
|
|
||||||
LodMode.MIN_MAX -> decimateMinMax(n, pixelWidth, outX, outY)
|
LodMode.MIN_MAX -> decimateMinMax(n, pixelWidth, outX, outY)
|
||||||
LodMode.LTTB -> decimateLttb(n, pixelWidth, outX, outY)
|
LodMode.LTTB -> decimateLttb(n, pixelWidth, outX, outY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decimateMean(n: Int, pixelWidth: Int, outX: FloatArray, outY: FloatArray): Int {
|
|
||||||
for (col in 0 until pixelWidth) { bucketSumY[col] = 0.0; bucketCountArr[col] = 0 }
|
|
||||||
for (i in 0 until n) { bucketSumY[colArr[i]] += valArr[i]; bucketCountArr[colArr[i]]++ }
|
|
||||||
var outIdx = 0
|
|
||||||
for (col in 0 until pixelWidth) {
|
|
||||||
if (bucketCountArr[col] == 0) continue
|
|
||||||
outX[outIdx] = col + 0.5f
|
|
||||||
outY[outIdx] = (bucketSumY[col] / bucketCountArr[col]).toFloat()
|
|
||||||
outIdx++
|
|
||||||
}
|
|
||||||
return outIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decimateMinMax(n: Int, pixelWidth: Int, outX: FloatArray, outY: FloatArray): Int {
|
private fun decimateMinMax(n: Int, pixelWidth: Int, outX: FloatArray, outY: FloatArray): Int {
|
||||||
for (col in 0 until pixelWidth) { colMin[col] = Float.MAX_VALUE; colMax[col] = -Float.MAX_VALUE; colHit[col] = false }
|
for (col in 0 until pixelWidth) { colMin[col] = Float.MAX_VALUE; colMax[col] = -Float.MAX_VALUE; colHit[col] = false }
|
||||||
for (i in 0 until n) {
|
for (i in 0 until n) {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ class TieredBuffer {
|
||||||
private val tier2 = CircularBuffer(TIER2_CAPACITY)
|
private val tier2 = CircularBuffer(TIER2_CAPACITY)
|
||||||
|
|
||||||
private var tier1BinStartMs = -1L
|
private var tier1BinStartMs = -1L
|
||||||
private var tier1BinSum = 0.0
|
private var tier1BinMin = Float.MAX_VALUE
|
||||||
private var tier1BinCount = 0
|
private var tier1BinMax = -Float.MAX_VALUE
|
||||||
|
|
||||||
private var tier2BinStartMs = -1L
|
private var tier2BinStartMs = -1L
|
||||||
private var tier2BinSum = 0.0
|
private var tier2BinMin = Float.MAX_VALUE
|
||||||
private var tier2BinCount = 0
|
private var tier2BinMax = -Float.MAX_VALUE
|
||||||
|
|
||||||
private val t0Ts = LongArray(TIER0_CAPACITY)
|
private val t0Ts = LongArray(TIER0_CAPACITY)
|
||||||
private val t0Vs = FloatArray(TIER0_CAPACITY)
|
private val t0Vs = FloatArray(TIER0_CAPACITY)
|
||||||
|
|
@ -32,15 +32,17 @@ class TieredBuffer {
|
||||||
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
|
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
|
||||||
}
|
}
|
||||||
if (ts >= tier1BinStartMs + TIER1_BIN_MS) {
|
if (ts >= tier1BinStartMs + TIER1_BIN_MS) {
|
||||||
if (tier1BinCount > 0) {
|
if (tier1BinMin != Float.MAX_VALUE) {
|
||||||
tier1.push(tier1BinStartMs + TIER1_BIN_MS / 2L, (tier1BinSum / tier1BinCount).toFloat())
|
val midTs = tier1BinStartMs + TIER1_BIN_MS / 2L
|
||||||
|
tier1.push(midTs, tier1BinMin)
|
||||||
|
tier1.push(midTs, tier1BinMax)
|
||||||
}
|
}
|
||||||
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
|
tier1BinStartMs = (ts / TIER1_BIN_MS) * TIER1_BIN_MS
|
||||||
tier1BinSum = value.toDouble()
|
tier1BinMin = value
|
||||||
tier1BinCount = 1
|
tier1BinMax = value
|
||||||
} else {
|
} else {
|
||||||
tier1BinSum += value
|
if (value < tier1BinMin) tier1BinMin = value
|
||||||
tier1BinCount++
|
if (value > tier1BinMax) tier1BinMax = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,15 +51,17 @@ class TieredBuffer {
|
||||||
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
|
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
|
||||||
}
|
}
|
||||||
if (ts >= tier2BinStartMs + TIER2_BIN_MS) {
|
if (ts >= tier2BinStartMs + TIER2_BIN_MS) {
|
||||||
if (tier2BinCount > 0) {
|
if (tier2BinMin != Float.MAX_VALUE) {
|
||||||
tier2.push(tier2BinStartMs + TIER2_BIN_MS / 2L, (tier2BinSum / tier2BinCount).toFloat())
|
val midTs = tier2BinStartMs + TIER2_BIN_MS / 2L
|
||||||
|
tier2.push(midTs, tier2BinMin)
|
||||||
|
tier2.push(midTs, tier2BinMax)
|
||||||
}
|
}
|
||||||
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
|
tier2BinStartMs = (ts / TIER2_BIN_MS) * TIER2_BIN_MS
|
||||||
tier2BinSum = value.toDouble()
|
tier2BinMin = value
|
||||||
tier2BinCount = 1
|
tier2BinMax = value
|
||||||
} else {
|
} else {
|
||||||
tier2BinSum += value
|
if (value < tier2BinMin) tier2BinMin = value
|
||||||
tier2BinCount++
|
if (value > tier2BinMax) tier2BinMax = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,8 +119,8 @@ class TieredBuffer {
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
tier0.clear(); tier1.clear(); tier2.clear()
|
tier0.clear(); tier1.clear(); tier2.clear()
|
||||||
tier1BinStartMs = -1L; tier1BinSum = 0.0; tier1BinCount = 0
|
tier1BinStartMs = -1L; tier1BinMin = Float.MAX_VALUE; tier1BinMax = -Float.MAX_VALUE
|
||||||
tier2BinStartMs = -1L; tier2BinSum = 0.0; tier2BinCount = 0
|
tier2BinStartMs = -1L; tier2BinMin = Float.MAX_VALUE; tier2BinMax = -Float.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -132,8 +136,8 @@ class TieredBuffer {
|
||||||
const val TIER2_BIN_MS = 1000L / TIER2_HZ
|
const val TIER2_BIN_MS = 1000L / TIER2_HZ
|
||||||
|
|
||||||
val TIER0_CAPACITY = ((TIER0_DURATION_MS / 1000L) * TIER0_MAX_HZ).toInt()
|
val TIER0_CAPACITY = ((TIER0_DURATION_MS / 1000L) * TIER0_MAX_HZ).toInt()
|
||||||
val TIER1_CAPACITY = ((TIER1_DURATION_MS / 1000L) * TIER1_HZ).toInt()
|
val TIER1_CAPACITY = ((TIER1_DURATION_MS / 1000L) * TIER1_HZ).toInt() * 2
|
||||||
val TIER2_CAPACITY = ((TIER2_DURATION_MS / 1000L) * TIER2_HZ).toInt()
|
val TIER2_CAPACITY = ((TIER2_DURATION_MS / 1000L) * TIER2_HZ).toInt() * 2
|
||||||
val TOTAL_CAPACITY = TIER0_CAPACITY + TIER1_CAPACITY + TIER2_CAPACITY
|
val TOTAL_CAPACITY = TIER0_CAPACITY + TIER1_CAPACITY + TIER2_CAPACITY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,5 @@ data class ChartConfig(
|
||||||
val yLabelMode: AxisLabelMode = AxisLabelMode.INSIDE,
|
val yLabelMode: AxisLabelMode = AxisLabelMode.INSIDE,
|
||||||
val yLabelDecimals: Int = 2,
|
val yLabelDecimals: Int = 2,
|
||||||
val xLabelDecimals: Int = 1,
|
val xLabelDecimals: Int = 1,
|
||||||
val lodMode: LodMode = LodMode.LTTB,
|
val lodMode: LodMode = LodMode.MIN_MAX,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
package dev.dtrentin.chart.model
|
package dev.dtrentin.chart.model
|
||||||
|
|
||||||
enum class LodMode { MEAN, MIN_MAX, LTTB }
|
enum class LodMode { MIN_MAX, LTTB }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue