WIP: feature/emd #521

Draft
teldufalsari wants to merge 11 commits from teldufalsari/kmath:feature/emd into dev
2 changed files with 70 additions and 58 deletions
Showing only changes of commit 2e084edc9b - Show all commits

View File

@ -5,20 +5,24 @@
package space.kscience.kmath.series
import space.kscience.kmath.operations.algebra
import space.kscience.kmath.operations.bufferAlgebra
import space.kscience.kmath.structures.*
import space.kscience.kmath.operations.invoke
import space.kscience.plotly.*
import space.kscience.plotly.models.Scatter
Review
fun main(): Unit = (Double.seriesAlgebra()) {

is enough if you import import space.kscience.kmath.operations.invoke.

``` fun main(): Unit = (Double.seriesAlgebra()) { ``` is enough if you import `import space.kscience.kmath.operations.invoke`.
import kotlin.math.sin
fun main(): Unit = (Double.seriesAlgebra()) {
private val customAlgebra = (Double.algebra.bufferAlgebra) { SeriesAlgebra(this) { it.toDouble() } }
fun main(): Unit = (customAlgebra) {
val signal = DoubleArray(800) {
sin(it.toDouble() / 10.0) + 3.5 * sin(it.toDouble() / 60.0)
}.asBuffer().moveTo(0)
val emd = empiricalModeDecomposition(
sConditionThreshold = 1,
maxSiftIterations = 15,
siftingDelta = 1e-2,
nModes = 4
).decompose(signal)
println("EMD: ${emd.modes.size} modes extracted, terminated because ${emd.terminatedBecause}")
@ -26,7 +30,7 @@ fun main(): Unit = (Double.seriesAlgebra()) {
fun Plot.series(name: String, buffer: Buffer<Double>, block: Scatter.() -> Unit = {}) {
this.scatter {
this.name = name
this.x.numbers = buffer.offsetIndices
this.x.numbers = buffer.labels
this.y.doubles = buffer.toDoubleArray()
block()
}

View File

@ -8,11 +8,8 @@ package space.kscience.kmath.series
import space.kscience.kmath.interpolation.SplineInterpolator
import space.kscience.kmath.interpolation.interpolate
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.asBuffer
import kotlin.math.sign
import space.kscience.kmath.structures.last
/**
* Empirical mode decomposition of a signal represented as a [Series].
@ -29,13 +26,13 @@ import kotlin.math.sign
* @param nModes how many modes should be extracted at most. The algorithm may return fewer modes if it was not
* possible to extract more modes from the signal.
*/
public class EmpiricalModeDecomposition<BA, L: Number> (
private val seriesAlgebra: SeriesAlgebra<Double, *, BA, L>,
public class EmpiricalModeDecomposition<T: Comparable<T>, A: Field<T>, BA, L: T> (
private val seriesAlgebra: SeriesAlgebra<T, A, BA, L>,
private val sConditionThreshold: Int = 15,
private val maxSiftIterations: Int = 20,
private val siftingDelta: Double = 1e-2,
private val siftingDelta: T,
private val nModes: Int = 6
) where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> {
) where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> {
/**
* Take a signal, construct an upper and a lower envelopes, find the mean value of two,
@ -45,37 +42,38 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
* @return mean [Series] or `null`. `null` is returned in case

Possible typos fix suggestion:

     * Take a signal, construct an upper and a lower envelopes, find the mean value of the two,
Possible typos fix suggestion: ``` * Take a signal, construct an upper and a lower envelopes, find the mean value of the two, ```
* the signal does not have enough extrema to construct envelopes.
*/
private fun findMean(signal: Series<Double>): Series<Double>? = (seriesAlgebra) {
val interpolator = SplineInterpolator(Float64Field)
fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: DoubleArray): Series<Double> {
private fun findMean(signal: Series<T>): Series<T>? = (seriesAlgebra) {
val interpolator = SplineInterpolator(elementAlgebra)

I suggest adding a sentence that describes what is actually returned. And possible typos fix as well.

     * @return The mean series or `null`. `null` is returned if the signal does not have enough extrema to construct envelopes.
I suggest adding a sentence that describes what is actually returned. And possible typos fix as well. ``` * @return The mean series or `null`. `null` is returned if the signal does not have enough extrema to construct envelopes. ```
val makeBuffer = elementAlgebra.bufferFactory
fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: Buffer<T>): Series<T> {
    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 envelopeFunction = interpolator.interpolate(
Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() },
paddedExtremeValues.asBuffer()
makeBuffer(extrema.size) { signal.labels[extrema[it]] },
paddedExtremeValues
)
return signal.mapWithLabel { _, label ->
// For some reason PolynomialInterpolator is exclusive and the right boundary
// TODO Notify interpolator authors
envelopeFunction(label.toDouble()) ?: paddedExtremeValues.last()
envelopeFunction(label) ?: paddedExtremeValues.last()
// need to make the interpolator yield values outside boundaries?
}
}
// 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]] }
val maxValues = makeBuffer(maxima.size) { signal[maxima[it]] }
if (maxValues[0] < maxValues[1]) {
maxValues[0] = maxValues[1]
}
if (maxValues.last() < maxValues[maxValues.lastIndex - 1]) {
maxValues[maxValues.lastIndex] = maxValues[maxValues.lastIndex - 1]
if (maxValues.last() < maxValues[maxValues.size - 2]) {
maxValues[maxValues.size - 1] = maxValues[maxValues.size - 2]
            return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 }
```kotlin return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 } ```
}
val minima = listOf(0) + signal.troughs() + (signal.size - 1)
val minValues = DoubleArray(minima.size) { signal[minima[it]] }
val minValues = makeBuffer(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]
if (minValues.last() > minValues[minValues.size - 2]) {
minValues[minValues.size - 1] = minValues[minValues.size - 2]
}
return if (maxima.size < 3 || minima.size < 3) null else { // maybe make an early return?
val upperEnvelope = generateEnvelope(maxima, maxValues)

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
@ -92,13 +90,13 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
* @return [SiftingResult.NotEnoughExtrema] is returned if the signal has too few extrema to extract a mode.
* Success of an appropriate type (See [SiftingResult.Success] class) is returned otherwise.
*/
private fun sift(signal: Series<Double>): SiftingResult = siftInner(signal, 1, 0)
private fun sift(signal: Series<T>): SiftingResult = siftInner(signal, 1, 0)
/**
* Compute a single iteration of the sifting process.
*/
private tailrec fun siftInner(
prevMode: Series<Double>,
prevMode: Series<T>,
iterationNumber: Int,
sNumber: Int
Review

Gosh. Use when instead of long if-else sequence, please:

        return when {
            iterationNumber >= maxSiftIterations -> SiftingResult.MaxIterationsReached(mode)
            sNumber >= sConditionThreshold -> SiftingResult.SNumberReached(mode)
            relativeDifference(prevMode, mode) < siftingDelta * mode.size -> SiftingResult.DeltaReached(mode)
            else -> siftInner(mode, iterationNumber + 1, newSNumber)
        }

It's idiom, and it's clearer to read.

Gosh. Use `when` instead of long if-else sequence, please: ```kotlin return when { iterationNumber >= maxSiftIterations -> SiftingResult.MaxIterationsReached(mode) sNumber >= sConditionThreshold -> SiftingResult.SNumberReached(mode) relativeDifference(prevMode, mode) < siftingDelta * mode.size -> SiftingResult.DeltaReached(mode) else -> siftInner(mode, iterationNumber + 1, newSNumber) } ``` It's idiom, and it's clearer to read.
): SiftingResult = (seriesAlgebra) {
@ -106,11 +104,12 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
return if (iterationNumber == 1) SiftingResult.NotEnoughExtrema
else SiftingResult.SignalFlattened(prevMode)
val mode = prevMode.zip(mean) { p, m -> p - m }
val newSNumber = if (mode.sCondition()) sNumber + 1 else sNumber
val newSNumber = if (sCondition(mode)) sNumber + 1 else sNumber
return when {
iterationNumber >= maxSiftIterations -> SiftingResult.MaxIterationsReached(mode)
sNumber >= sConditionThreshold -> SiftingResult.SNumberReached(mode)
relativeDifference(mode, prevMode) < siftingDelta * mode.size -> SiftingResult.DeltaReached(mode)
relativeDifference(mode, prevMode) < (elementAlgebra) { siftingDelta * mode.size } ->
SiftingResult.DeltaReached(mode)
else -> siftInner(mode, iterationNumber + 1, newSNumber)
}
}

The whole function can be rewritten in such way:

    public fun decompose(signal: Series<Double>): EMDecompositionResult = with(seriesAlgebra) {
        val modes = mutableListOf<Series<Double>>()
        var residual = signal
        repeat(nModes) {
            val nextMode = when(val r = sift(residual)) {
                SiftingResult.NotEnoughExtrema ->
                    return EMDecompositionResult(
                        if (it == 0) EMDTerminationReason.SIGNAL_TOO_FLAT
                        else EMDTerminationReason.ALL_POSSIBLE_MODES_EXTRACTED,
                        modes
                    )
                is SiftingResult.Success -> r.result
            }
            modes.add(nextMode)
            residual = residual.zip(nextMode) { l, r -> l - r }
        }
        return EMDecompositionResult(EMDTerminationReason.MAX_MODES_REACHED, modes)
    }

It's shorter but as readable as the previous version.

The whole function can be rewritten in such way: ```kotlin public fun decompose(signal: Series<Double>): EMDecompositionResult = with(seriesAlgebra) { val modes = mutableListOf<Series<Double>>() var residual = signal repeat(nModes) { val nextMode = when(val r = sift(residual)) { SiftingResult.NotEnoughExtrema -> return EMDecompositionResult( if (it == 0) EMDTerminationReason.SIGNAL_TOO_FLAT else EMDTerminationReason.ALL_POSSIBLE_MODES_EXTRACTED, modes ) is SiftingResult.Success -> r.result } modes.add(nextMode) residual = residual.zip(nextMode) { l, r -> l - r } } return EMDecompositionResult(EMDTerminationReason.MAX_MODES_REACHED, modes) } ``` It's shorter but as readable as the previous version.
@ -123,8 +122,8 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
* Modes returned in a list which contains as many modes as it was possible
* to extract before triggering one of the termination conditions.
*/

You should use SeriesAlgebra's elementAlgebra in getting difference of two Double values instead of l - r. And in 8 strings below too.

You should use `SeriesAlgebra`'s `elementAlgebra` in getting difference of two `Double` values instead of `l - r`. And in 8 strings below too.
public fun decompose(signal: Series<Double>): EMDecompositionResult = (seriesAlgebra) {
val modes = mutableListOf<Series<Double>>()
public fun decompose(signal: Series<T>): EMDecompositionResult<T> = (seriesAlgebra) {
val modes = mutableListOf<Series<T>>()
var residual = signal
repeat(nModes) {
val nextMode = when(val r = sift(residual)) {
@ -132,14 +131,15 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
return EMDecompositionResult(
if (it == 0) EMDTerminationReason.SIGNAL_TOO_FLAT
else EMDTerminationReason.ALL_POSSIBLE_MODES_EXTRACTED,
modes
modes,
residual
)
is SiftingResult.Success -> r.result
is SiftingResult.Success<*> -> r.result
}
modes.add(nextMode)
modes.add(nextMode as Series<T>) // TODO remove unchecked cast
residual = residual.zip(nextMode) { l, r -> l - r }
}
return EMDecompositionResult(EMDTerminationReason.MAX_MODES_REACHED, modes)
return EMDecompositionResult(EMDTerminationReason.MAX_MODES_REACHED, modes, residual)
}
}
@ -157,13 +157,13 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
* @param nModes how many modes should be extracted at most. The algorithm may return fewer modes if it was not
* possible to extract more modes from the signal.
*/
public fun <L: Number, BA> SeriesAlgebra<Double, *, BA, L>.empiricalModeDecomposition(
public fun <T: Comparable<T>, L: T, A: Field<T>, BA> SeriesAlgebra<T, A, BA, L>.empiricalModeDecomposition(
sConditionThreshold: Int = 15,
maxSiftIterations: Int = 20,
siftingDelta: Double = 1e-2,
siftingDelta: T,
nModes: Int = 3
): EmpiricalModeDecomposition<BA, L>
where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> = EmpiricalModeDecomposition(
): EmpiricalModeDecomposition<T, A, BA, L>
where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> = EmpiricalModeDecomposition(
seriesAlgebra = this,
sConditionThreshold = sConditionThreshold,
maxSiftIterations = maxSiftIterations,
@ -174,12 +174,15 @@ where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> = EmpiricalModeD
/**
* Brute force count all zeros in the series.
*/
private fun Series<Double>.countZeros(): Int {
require(size >= 2) { "Expected series with at least 2 elements, but got $size elements" }
data class SignCounter(val prevSign: Double, val zeroCount: Int)
private fun <T: Comparable<T>, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.countZeros(
signal: Series<T>
): Int where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> {
require(signal.size >= 2) { "Expected series with at least 2 elements, but got ${signal.size} elements" }
data class SignCounter(val prevSign: Int, val zeroCount: Int)
fun strictSign(arg: T): Int = if (arg > elementAlgebra.zero) 1 else -1

Use = notation for inline bodies, please:

private fun <A: Ring<Double>, BA> SeriesAlgebra<Double, A, BA, *>.relativeDifference(
    current: Series<Double>,
    previous: Series<Double>
):Double where BA: BufferAlgebra<Double, A>, BA: RingOps<Buffer<Double>> =
    (current - previous).pow(2)
        .div(previous pow 2)
        .fold(0.0) { acc, d -> elementAlgebra.add(acc, d) }

Also:

  1. L is not used, so I removed it.
  2. Default algebra used when SeriesAlgebra's elementAlgebra is needed. So I replaced it. It will also help with refactoring from Double "algebras" to general algebras.
  3. No, fold uses Double as a type parameter, so boxing is not avoided. I would replace .fold(0.0) { acc, d -> acc + d } with .sum(elementAlgebra) but there is no such operation for some reason.
Use `=` notation for inline bodies, please: ```kotlin private fun <A: Ring<Double>, BA> SeriesAlgebra<Double, A, BA, *>.relativeDifference( current: Series<Double>, previous: Series<Double> ):Double where BA: BufferAlgebra<Double, A>, BA: RingOps<Buffer<Double>> = (current - previous).pow(2) .div(previous pow 2) .fold(0.0) { acc, d -> elementAlgebra.add(acc, d) } ``` Also: 1. `L` is not used, so I removed it. 2. Default algebra used when `SeriesAlgebra`'s `elementAlgebra` is needed. So I replaced it. It will also help with refactoring from `Double` "algebras" to general algebras. 3. No, `fold` uses `Double` as a type parameter, so boxing is not avoided. I would replace `.fold(0.0) { acc, d -> acc + d }` with `.sum(elementAlgebra)` but there is no such operation for some reason.

I also wondered why there is no .sum() method. I could implement it for a 1-d series, but doing it for a general buffer seems a bit too much if there is need for arbitrary axis like in NumPy

I also wondered why there is no `.sum()` method. I could implement it for a 1-d series, but doing it for a general buffer seems a bit too much if there is need for arbitrary axis like in NumPy

@altavir There is a need for a function Buffer<T>.sum(elementAlgebra: Group<T>). Where should we place it?

@altavir There is a need for a function `Buffer<T>.sum(elementAlgebra: Group<T>)`. Where should we place it?
return fold(SignCounter(sign(get(0)), 0)) { acc: SignCounter, it: Double ->
val currentSign = sign(it)
return signal.fold(SignCounter(strictSign(signal[0]), 0)) { acc, it ->
val currentSign = strictSign(it)
if (acc.prevSign != currentSign) SignCounter(currentSign, acc.zeroCount + 1)
else SignCounter(currentSign, acc.zeroCount)
}.zeroCount
@ -188,18 +191,19 @@ private fun Series<Double>.countZeros(): Int {
/**
* Compute relative difference of two series.
*/
private fun <BA> SeriesAlgebra<Double, *, BA, *>.relativeDifference(
current: Series<Double>,
previous: Series<Double>
):Double where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> =
(current - previous).pow(2)
.div(previous pow 2)
.fold(0.0) { acc, d -> acc + d } // TODO replace with Series<>.sum() method when it's implemented
private fun <T, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.relativeDifference(
current: Series<T>,
previous: Series<T>

I would recommend writing

    return (1 .. size - 2).count { isExtreme(this[it-1], this[it], this[it+1]) }
I would recommend writing ```kotlin return (1 .. size - 2).count { isExtreme(this[it-1], this[it], this[it+1]) } ```
): T where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> = (bufferAlgebra) {
((current - previous) * (current - previous))
.div(previous * previous)
.fold(elementAlgebra.zero) { acc, it -> acc + it}
}
/**
* Brute force count all extrema of a series.
*/
private fun Series<Double>.countExtrema(): Int {
private fun <T: Comparable<T>> Series<T>.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" }
return peaks().size + troughs().size
}

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`
@ -208,7 +212,10 @@ private fun Series<Double>.countExtrema(): Int {
* 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.
*/
private fun Series<Double>.sCondition(): Boolean = (countExtrema() - countZeros()) in -1..1
private fun <T: Comparable<T>, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.sCondition(
signal: Series<T>
): Boolean where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> =
(signal.countExtrema() - countZeros(signal)) in -1..1
internal sealed interface SiftingResult {
@ -216,33 +223,33 @@ internal sealed interface SiftingResult {
* Represents a condition when a mode has been successfully
* extracted in a sifting process.
*/

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, similar question to this one.

Also, similar question to [this one](https://git.sciprog.center/kscience/kmath/pulls/521/files#issuecomment-1868).
open class Success(val result: Series<Double>): SiftingResult
open class Success<T>(val result: Series<T>): SiftingResult
/**
* Returned when no termination condition was reached and the proto-mode
* has become too flat (with not enough extrema to build envelopes)
* after several sifting iterations.
*/
class SignalFlattened(result: Series<Double>) : Success(result)
class SignalFlattened<T>(result: Series<T>) : Success<T>(result)
/**
* Returned when sifting process has been terminated due to the
* S-number condition being reached.
*/
class SNumberReached(result: Series<Double>) : Success(result)
class SNumberReached<T>(result: Series<T>) : Success<T>(result)
/**
* Returned when sifting process has been terminated due to the
* delta condition (Cauchy criterion) being reached.
*/
class DeltaReached(result: Series<Double>) : Success(result)
class DeltaReached<T>(result: Series<T>) : Success<T>(result)
Review

I don't understand what the Success inheritors are for. I mean, they are all instantiated but never distinguished. All of them can be used only internally, but are cast to Success anyway.

I don't understand what the `Success` inheritors are for. I mean, they are all instantiated but never distinguished. All of them can be used only internally, but are cast to `Success` anyway.
/**
* Returned when sifting process has been terminated after
* executing the maximum number of iterations (specified when creating an instance
* of [EmpiricalModeDecomposition]).
*/
class MaxIterationsReached(result: Series<Double>): Success(result)
class MaxIterationsReached<T>(result: Series<T>): Success<T>(result)
/**
* Returned when the submitted signal has not enough extrema to build envelopes,
@ -274,7 +281,8 @@ public enum class EMDTerminationReason {
ALL_POSSIBLE_MODES_EXTRACTED
}
public data class EMDecompositionResult(
public data class EMDecompositionResult<T>(
val terminatedBecause: EMDTerminationReason,
val modes: List<Series<Double>>
val modes: List<Series<T>>,
val residual: Series<T>
)