WIP: feature/emd #521

Draft
teldufalsari wants to merge 11 commits from teldufalsari/kmath:feature/emd into dev
Showing only changes of commit b8bf56bc42 - Show all commits

View File

@ -11,7 +11,7 @@ import space.kscience.kmath.operations.*
import space.kscience.kmath.operations.Float64BufferOps.Companion.div import space.kscience.kmath.operations.Float64BufferOps.Companion.div
import space.kscience.kmath.operations.Float64BufferOps.Companion.pow import space.kscience.kmath.operations.Float64BufferOps.Companion.pow
import space.kscience.kmath.structures.Buffer import space.kscience.kmath.structures.Buffer
import space.kscience.kmath.structures.last import space.kscience.kmath.structures.asBuffer
import kotlin.math.sign import kotlin.math.sign
/** /**
@ -47,24 +47,40 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
*/ */
private fun findMean(signal: Series<Double>): Series<Double>? = (seriesAlgebra) { private fun findMean(signal: Series<Double>): Series<Double>? = (seriesAlgebra) {
    private fun findMean(signal: Series<Double>): Series<Double>? = seriesAlgebra {

Just this is enough. Because there is invoke extension operator implemented that is imported here.

```kotlin private fun findMean(signal: Series<Double>): Series<Double>? = seriesAlgebra { ``` Just this is enough. Because there is `invoke` extension operator implemented that is imported here.
val interpolator = SplineInterpolator(Float64Field) val interpolator = SplineInterpolator(Float64Field)
fun generateEnvelope(extrema: List<Int>): Series<Double> { fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: DoubleArray): Series<Double> {
val envelopeFunction = interpolator.interpolate( val envelopeFunction = interpolator.interpolate(
Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() }, Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() },
Buffer(extrema.size) { signal[extrema[it]] } paddedExtremeValues.asBuffer()
) )
return signal.mapWithLabel { _, label -> return signal.mapWithLabel { _, label ->
// For some reason PolynomialInterpolator is exclusive and the right boundary // For some reason PolynomialInterpolator is exclusive and the right boundary
// TODO Notify interpolator authors // TODO Notify interpolator authors
envelopeFunction(label.toDouble()) ?: signal.last() envelopeFunction(label.toDouble()) ?: paddedExtremeValues.last()
// need to make the interpolator yield values outside boundaries? // need to make the interpolator yield values outside boundaries?
} }
} }
val maxima = signal.paddedMaxima() // Extrema padding (experimental) TODO padding needs a dedicated function
val minima = signal.paddedMinima() val maxima = listOf(0) + signal.peaks() + (signal.size - 1)
return if (maxima.size < 3 || minima.size < 3) null else { val maxValues = DoubleArray(maxima.size) { signal[maxima[it]] }
val upperEnvelope = generateEnvelope(maxima) if (maxValues[0] < maxValues[1]) {
val lowerEnvelope = generateEnvelope(minima) maxValues[0] = maxValues[1]
return upperEnvelope.zip(lowerEnvelope) { upper, lower -> upper + lower / 2.0 } }
            return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 }
```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]
}

As well as I understand, whole body of the function can be replaced with just

    private fun sift(signal: Series<Double>): SiftingResult = siftInner(signal, 1, 0)
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) ```

Also siftInner should be marked tailrec, shouldn't it?

Also `siftInner` should be marked `tailrec`, shouldn't it?

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.

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.

It works. With tailrec it does not produce stack overflow when I run sifting with 5000 iterations per mode

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. * Brute force count all extrema of a series.
*/ */
@Deprecated("Does not match the algorithm currently in use.")
private fun Series<Double>.countExtrema(): Int { private fun Series<Double>.countExtrema(): Int {
private fun <T : Comparable<T>> Series<T>.maxima(): List<Int> {
```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" } 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]) } return (1 .. size - 2).count { isExtreme(this[it - 1], this[it], this[it + 1]) }
} }

I would recommend rewriting it with old good plain loop on indices:

    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

    return (1 .. size - 2).filter { (this[it] > this[it-1] && this[it] > this[it+1]) || it == 0 || it == size - 1 }
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 } ```

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.

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.

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

> 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`

Could you comment what question // weird offset, is there a way to do it better? means in more detail?

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. * 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. * This is a necessary condition of an empirical mode.