WIP: feature/emd #521
@ -11,7 +11,7 @@ import space.kscience.kmath.operations.*
|
||||
import space.kscience.kmath.operations.Float64BufferOps.Companion.div
|
||||
import space.kscience.kmath.operations.Float64BufferOps.Companion.pow
|
||||
import space.kscience.kmath.structures.Buffer
|
||||
import space.kscience.kmath.structures.last
|
||||
import space.kscience.kmath.structures.asBuffer
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
@ -47,24 +47,40 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
|
||||
*/
|
||||
private fun findMean(signal: Series<Double>): Series<Double>? = (seriesAlgebra) {
|
||||
|
||||
val interpolator = SplineInterpolator(Float64Field)
|
||||
fun generateEnvelope(extrema: List<Int>): Series<Double> {
|
||||
fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: DoubleArray): Series<Double> {
|
||||
val envelopeFunction = interpolator.interpolate(
|
||||
Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() },
|
||||
Buffer(extrema.size) { signal[extrema[it]] }
|
||||
paddedExtremeValues.asBuffer()
|
||||
)
|
||||
return signal.mapWithLabel { _, label ->
|
||||
// For some reason PolynomialInterpolator is exclusive and the right boundary
|
||||
// TODO Notify interpolator authors
|
||||
envelopeFunction(label.toDouble()) ?: signal.last()
|
||||
envelopeFunction(label.toDouble()) ?: paddedExtremeValues.last()
|
||||
// need to make the interpolator yield values outside boundaries?
|
||||
}
|
||||
}
|
||||
val maxima = signal.paddedMaxima()
|
||||
val minima = signal.paddedMinima()
|
||||
return if (maxima.size < 3 || minima.size < 3) null else {
|
||||
val upperEnvelope = generateEnvelope(maxima)
|
||||
val lowerEnvelope = generateEnvelope(minima)
|
||||
return upperEnvelope.zip(lowerEnvelope) { upper, lower -> upper + lower / 2.0 }
|
||||
// Extrema padding (experimental) TODO padding needs a dedicated function
|
||||
val maxima = listOf(0) + signal.peaks() + (signal.size - 1)
|
||||
val maxValues = DoubleArray(maxima.size) { signal[maxima[it]] }
|
||||
if (maxValues[0] < maxValues[1]) {
|
||||
maxValues[0] = maxValues[1]
|
||||
}
|
||||
lounres
commented
```kotlin
return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 }
```
|
||||
if (maxValues.last() < maxValues[maxValues.lastIndex - 1]) {
|
||||
maxValues[maxValues.lastIndex] = maxValues[maxValues.lastIndex - 1]
|
||||
}
|
||||
|
||||
val minima = listOf(0) + signal.troughs() + (signal.size - 1)
|
||||
val minValues = DoubleArray(minima.size) { signal[minima[it]] }
|
||||
if (minValues[0] > minValues[1]) {
|
||||
minValues[0] = minValues[1]
|
||||
}
|
||||
if (minValues.last() > minValues[minValues.lastIndex - 1]) {
|
||||
minValues[minValues.lastIndex] = minValues[minValues.lastIndex - 1]
|
||||
}
|
||||
lounres
commented
As well as I understand, whole body of the function can be replaced with just
As well as I understand, whole body of the function can be replaced with just
```kotlin
private fun sift(signal: Series<Double>): SiftingResult = siftInner(signal, 1, 0)
```
teldufalsari
commented
Also Also `siftInner` should be marked `tailrec`, shouldn't it?
lounres
commented
Yeah, it is better if the function is marked Yeah, it is better if the function is marked `tailrec`. But I am not sure if compiler understands the case. So I need a bit of time for a small test.
teldufalsari
commented
It works. With It works. With `tailrec` it does not produce stack overflow when I run sifting with 5000 iterations per mode
|
||||
return if (maxima.size < 3 || minima.size < 3) null else { // maybe make an early return?
|
||||
val upperEnvelope = generateEnvelope(maxima, maxValues)
|
||||
val lowerEnvelope = generateEnvelope(minima, minValues)
|
||||
return (upperEnvelope + lowerEnvelope).map { it * 0.5 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,43 +202,12 @@ private fun <T: Comparable<T>> isExtreme(prev: T, elem: T, next: T): Boolean =
|
||||
/**
|
||||
* Brute force count all extrema of a series.
|
||||
*/
|
||||
@Deprecated("Does not match the algorithm currently in use.")
|
||||
private fun Series<Double>.countExtrema(): Int {
|
||||
lounres
commented
```kotlin
private fun <T : Comparable<T>> Series<T>.maxima(): List<Int> {
```
|
||||
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
|
||||
return (1 .. size - 2).count { isExtreme(this[it - 1], this[it], this[it + 1]) }
|
||||
}
|
||||
lounres
commented
I would recommend rewriting it with old good plain loop on indices:
or using
I would recommend rewriting it with old good plain loop on indices:
```kotlin
for (index in 1 .. size - 2) {
val left = this[index-1]
val middle = this[index]
val right = this[index+1]
if (middle > left && middle > right) maxima.add(index)
}
```
or using
```kotlin
return (1 .. size - 2).filter { (this[it] > this[it-1] && this[it] > this[it+1]) || it == 0 || it == size - 1 }
```
lounres
commented
Also, is it intended that the spline will ignore double extrema? I mean for series Also, is it intended that the spline will ignore double extrema?
I mean for series `[0.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 0.0]` there will be no maxima and minima points but the end points.
teldufalsari
commented
No, I'm planning on improving this function and making it > Also, is it intended that the spline will ignore double extrema?
No, I'm planning on improving this function and making it `public` placed in `SeriesExtensions.kt`
|
||||
|
||||
lounres
commented
Could you comment what question Could you comment what question ` // weird offset, is there a way to do it better?` means in more detail?
|
||||
|
||||
/**
|
||||
* Retrieve indices of knot points for spline interpolation matching the predicate.
|
||||
* The first and the last points of a series are always included.
|
||||
*/
|
||||
private fun <T: Comparable<T>> Series<T>.knotPoints(predicate: (T, T, T) -> Boolean): List<Int> {
|
||||
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
|
||||
val points = mutableListOf(0)
|
||||
for (index in 1 .. size - 2) {
|
||||
val left = this[index - 1]
|
||||
val middle = this[index]
|
||||
val right = this[index + 1]
|
||||
if (predicate(left, middle, right)) points.add(index)
|
||||
}
|
||||
points.add(size - 1)
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve indices of knot points used to construct an upper envelope,
|
||||
* namely maxima together with the first last point in a series.
|
||||
*/
|
||||
private fun <T: Comparable<T>> Series<T>.paddedMaxima(): List<Int> =
|
||||
knotPoints { left, middle, right -> (middle > left && middle > right) }
|
||||
|
||||
/**
|
||||
* Retrieve indices of knot points used to construct a lower envelope,
|
||||
* namely minima together with the first last point in a series.
|
||||
*/
|
||||
private fun <T: Comparable<T>> Series<T>.paddedMinima(): List<Int> =
|
||||
knotPoints { left, middle, right -> (middle < left && middle < right) }
|
||||
|
||||
/**
|
||||
* Check whether the numbers of zeroes and extrema of a series differ by no more than 1.
|
||||
* This is a necessary condition of an empirical mode.
|
||||
|
Just this is enough. Because there is
invoke
extension operator implemented that is imported here.