WIP: feature/emd #521

Draft
teldufalsari wants to merge 11 commits from teldufalsari/kmath:feature/emd into dev
Owner

Empirical Mode Decomposition

Implement the empirical mode decomposition algorithm. Supports several criteria for process termination, more may be added in future. See emdDemo.kt file (examples/series) for an example on how to use the algorithm.

Note: this merge request is WIP and is not intended to be merged immediately.
There are planned changes and any suggestions for further improvements and corrections are appreciated.

For now the algorithm work with Double series only, making it generic over a series algebra is the top priority.

Description

The main algorithm is implemented in the EmpiricalModeDEcomposition class, which can be treated as a factory that produces decompositions. Parameters of a decomposition, such as termination criteria parameters and the number of modes to extract, are passed as parameters when creating an instance of the class. The user can then call the decompose method to produce a decomposition result, which as of now consists of a decomposition itself (as List<Series<T>>) and a reason for termination (as an enum element).

Each empirical mode is extracted in an iterative process called sifting. The algorithm allows to determine the reason for sifting to terminate using a sealed interface (see SiftingResult interface documentation). There is no use for this other than determining when it is impossible to extract another mode from the signal (the only case when sifting fails completely with no valid value (mode) to be returned), so that place can ultimately be simplified to a simple null check. I decided to leave it hoping that use cases for this may arise, e.g. returning verbose statistics together with the decomposition itself.

Issues encountered

  • It seems there was a bug in SeriesAlgebra: mapWithLabel function was failing on series with a non-zero offset.
  • PolynomialInterpolator treats the right boundary exclusively. For that reason, SplineInterpolator does not work as its counterpart from Apache Commons, which returns the right boundary value properly. I don not know if this is intended behaviour. Also, many works that explain the empirical decomposition do not use the leftmost and the rightmost points of the series as knot point for constructing the envelopes. Instead, they use points that are extrapolated using the first or the last polynomials of the spline interpolation. This version seems to work fine too, but probably it could be beneficial to modify the SplineInterpolator class to yield the points outside its boundaries if a special parameter is passed when creating an interpolator. Actually, an intricate extrema padding algorithm is usually involved, so SplineInterpolator should only properly return the value at its right boundary.

Further work

  • Making the decomposition generic over data type (not only Double);
  • Working on terminating conditions;
  • Writing tests and further testing in comparison with existing algorithms;
  • Implementing extrema padding algorithm;
  • Implementing Hilbert decomposition; it does not make much sense to have only EMD but not the complete Huang-Hilbert transform;
  • Implementing some other feature extraction functions as Series extensions: envelopes, extrema, median values; nonparametric
    statistical tests like Lepage test or Cucconi test.

Acknowledgements

Most of the algorithm details are taken from the Python EMD library reference and the sources cited there.

# Empirical Mode Decomposition Implement the [empirical mode decomposition](https://en.wikipedia.org/wiki/Hilbert%E2%80%93Huang_transform#Empirical_mode_decomposition_2) algorithm. Supports several criteria for process termination, more may be added in future. See `emdDemo.kt` file (`examples/series`) for an example on how to use the algorithm. > Note: this merge request is WIP and is not intended to be merged immediately. > There are planned changes and any suggestions for further improvements and corrections are appreciated. For now the algorithm work with `Double` series only, making it generic over a series algebra is the top priority. ## Description The main algorithm is implemented in the `EmpiricalModeDEcomposition` class, which can be treated as a factory that produces decompositions. Parameters of a decomposition, such as termination criteria parameters and the number of modes to extract, are passed as parameters when creating an instance of the class. The user can then call the `decompose` method to produce a decomposition result, which as of now consists of a decomposition itself (as `List<Series<T>>`) and a reason for termination (as an `enum` element). Each empirical mode is extracted in an iterative process called sifting. The algorithm allows to determine the reason for sifting to terminate using a sealed interface (see `SiftingResult` interface documentation). There is no use for this other than determining when it is impossible to extract another mode from the signal (the only case when sifting fails completely with no valid value (mode) to be returned), so that place can ultimately be simplified to a simple null check. I decided to leave it hoping that use cases for this may arise, e.g. returning verbose statistics together with the decomposition itself. ## Issues encountered - It seems there was a bug in `SeriesAlgebra`: `mapWithLabel` function was failing on series with a non-zero offset. - `PolynomialInterpolator` treats the right boundary exclusively. For that reason, `SplineInterpolator` does not work as its counterpart from Apache Commons, which returns the right boundary value properly. I don not know if this is intended behaviour. Also, many works that explain the empirical decomposition do not use the leftmost and the rightmost points of the series as knot point for constructing the envelopes. ~~Instead, they use points that are extrapolated using the first or the last polynomials of the spline interpolation. This version seems to work fine too, but probably it could be beneficial to modify the `SplineInterpolator` class to yield the points outside its boundaries if a special parameter is passed when creating an interpolator.~~ Actually, an intricate extrema padding algorithm is usually involved, so `SplineInterpolator` should only properly return the value at its right boundary. ## Further work - Making the decomposition generic over data type (not only `Double`); - Working on terminating conditions; - Writing tests and further testing in comparison with existing algorithms; - Implementing extrema padding algorithm; - Implementing Hilbert decomposition; it does not make much sense to have only EMD but not the complete Huang-Hilbert transform; - Implementing some other feature extraction functions as `Series` extensions: envelopes, extrema, median values; nonparametric statistical tests like Lepage test or Cucconi test. ## Acknowledgements Most of the algorithm details are taken from the [Python EMD library reference](https://emd.readthedocs.io/en/stable/index.html) and the sources cited there.
teldufalsari added 2 commits 2024-04-25 11:59:40 +03:00
teldufalsari changed title from feature/emd to WIP: feature/emd 2024-04-25 12:00:30 +03:00
teldufalsari requested review from altavir 2024-04-25 12:01:18 +03:00
altavir requested review from lounres 2024-04-25 12:11:23 +03:00
lounres reviewed 2024-04-25 18:54:03 +03:00
lounres left a comment
Owner

Good work! But there is a bit of work to do.

Also, I would like to see several (at least, 2 or 3) tests for the feature. And it will be cool to add short description of the features to docs (you can ask help about it here).

Good work! But there is a bit of work to do. Also, I would like to see several (at least, 2 or 3) tests for the feature. And it will be cool to add short description of the features to docs (you can ask help about it here).
@ -0,0 +10,4 @@
import space.kscience.plotly.models.Scatter
import kotlin.math.sin
fun main(): Unit = with(Double.seriesAlgebra()) {
Owner
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`.
@ -0,0 +36,4 @@
private val maxSiftIterations: Int = 20,
private val siftingDelta: Double = 1e-2,
private val nModes: Int = 6
) where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>>, L : Number {
Owner

Please, move the only type argument L's bound to L's declaration cite:

public class EmpiricalModeDecomposition<BA, L: Number> (
    private val seriesAlgebra: SeriesAlgebra<Double, *, BA, L>,
    private val sConditionThreshold: Int = 15,
    private val maxSiftIterations: Int = 20,
    private val siftingDelta: Double = 1e-2,
    private val nModes: Int = 6
) where BA: BufferAlgebra<Double, *>,  BA: RingOps<Buffer<Double>> {
Please, move the only type argument `L`'s bound to `L`'s declaration cite: ```kotlin public class EmpiricalModeDecomposition<BA, L: Number> ( private val seriesAlgebra: SeriesAlgebra<Double, *, BA, L>, private val sConditionThreshold: Int = 15, private val maxSiftIterations: Int = 20, private val siftingDelta: Double = 1e-2, private val nModes: Int = 6 ) where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> { ```
Owner

By the way, in general you'll have to use T as L. Otherwise, either interpolate can not be resolved in snippet

interpolator.interpolate(
    Buffer(extrema.size) { signal.labels[extrema[it]] },
    Buffer(extrema.size) { signal[extrema[it]] }
)

or there is no function to map L values to T.

By the way, in general you'll have to use `T` as `L`. Otherwise, either `interpolate` can not be resolved in snippet ```kotlin interpolator.interpolate( Buffer(extrema.size) { signal.labels[extrema[it]] }, Buffer(extrema.size) { signal[extrema[it]] } ) ``` or there is no function to map `L` values to `T`.
@ -0,0 +39,4 @@
) where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>>, L : Number {
/**
* Take a signal, construct an upper and a lower envelope, find the mean value of two,
Owner

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, ```
@ -0,0 +43,4 @@
* represented as a series.
* @param signal Signal to compute on
*
* @return null is returned if the signal has not enough extrema to construct envelopes.
Owner

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. ```
@ -0,0 +45,4 @@
*
* @return null is returned if the signal has not enough extrema to construct envelopes.
*/
private fun findMean(signal: Series<Double>): Series<Double>? = with(seriesAlgebra) {
Owner
    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.
@ -0,0 +64,4 @@
return if (maxima.size < 3 || minima.size < 3) null else {
val upperEnvelope = generateEnvelope(maxima)
val lowerEnvelope = generateEnvelope(minima)
return (upperEnvelope + lowerEnvelope).map { it * 0.5 }
Owner
            return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 }
```kotlin return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 } ```
@ -0,0 +76,4 @@
* @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 = with(seriesAlgebra) {
Owner

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) ```
Author
Owner

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

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

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.
Author
Owner

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
@ -0,0 +98,4 @@
val mean = findMean(prevMode) ?: return SiftingResult.SignalFlattened(prevMode)
val mode = prevMode.zip(mean) { p, m -> p - m }
val newSNumber = if (mode.sCondition()) sNumber + 1 else sNumber
return if (iterationNumber >= maxSiftIterations) SiftingResult.MaxIterationsReached(mode)
Owner

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.
@ -0,0 +100,4 @@
val newSNumber = if (mode.sCondition()) sNumber + 1 else sNumber
return if (iterationNumber >= maxSiftIterations) SiftingResult.MaxIterationsReached(mode)
else if (sNumber >= sConditionThreshold) SiftingResult.SNumberReached(mode)
else if (relativeDifference(prevMode, mode) < siftingDelta * mode.size) SiftingResult.DeltaReached(mode)
Owner

Is it intended that previous mode is assigned to current parameter of relativeDifference and current (new) mode is assigned to previous parameter of relativeDifference? The parameters names say that there is a swap of parameters.

I would use explicit parameters' assignation:

relativeDifference(previous = prevMode, current = mode)
Is it intended that previous mode is assigned to `current` parameter of `relativeDifference` and current (new) mode is assigned to `previous` parameter of `relativeDifference`? The parameters names say that there is a swap of parameters. I would use explicit parameters' assignation: ```kotlin relativeDifference(previous = prevMode, current = mode) ```
@ -0,0 +112,4 @@
* Modes returned in a list which contains as many modes as it was possible
* to extract before triggering one of the termination conditions.
*/
public fun decompose(signal: Series<Double>): EMDecompositionResult = with(seriesAlgebra) {
Owner

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.
@ -0,0 +121,4 @@
}
modes.add(mode)
var residual = signal.zip(mode) { l, r -> l - r }
Owner

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.
@ -0,0 +168,4 @@
*/
private fun Series<Double>.countZeros(): Int {
require(size >= 2) { "Expected series with at least 2 elements, but got $size elements" }
return fold(Pair(get(0), 0)) { acc: Pair<Double, Int>, it: Double ->
Owner

Do not use Pair! It's non-idiomatic in Kotlin. It is really hard to always keep in mind what first and second fields hold. Instead, define and use custom data class with understandable name and understandable parameters' names.

Do not use `Pair`! It's non-idiomatic in Kotlin. It is really hard to always keep in mind what `first` and `second` fields hold. Instead, define and use custom data class with understandable name and understandable parameters' names.
Author
Owner

Is making a one-line data class declaration above considered idiomatic?

    data class SignCounter(val prevSign: Double, val zeroCount: Int)
    return fold(SignCounter(sign(get(0)), 0)) { acc: SignCounter, it: Double ->
Is making a one-line data class declaration above considered idiomatic? ```kotlin data class SignCounter(val prevSign: Double, val zeroCount: Int) return fold(SignCounter(sign(get(0)), 0)) { acc: SignCounter, it: Double -> ```
Owner

Yeah, that's the idiomatic way. But place it outside the function. Otherwise, you won't able to access it :)

Yeah, that's the idiomatic way. But place it outside the function. Otherwise, you won't able to access it :)
@ -0,0 +169,4 @@
private fun Series<Double>.countZeros(): Int {
require(size >= 2) { "Expected series with at least 2 elements, but got $size elements" }
return fold(Pair(get(0), 0)) { acc: Pair<Double, Int>, it: Double ->
if (sign(acc.first) != sign(it)) Pair(it, acc.second + 1) else Pair(it, acc.second)
Owner

It's better to store sign instead of the value which sign is compared, to not compute it each iteration.

It's better to store sign instead of the value which sign is compared, to not compute it each iteration.
@ -0,0 +179,4 @@
private fun <L, BA> SeriesAlgebra<Double, *, BA, L>.relativeDifference(
current: Series<Double>,
previous: Series<Double>
):Double where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>>, L: Number {
Owner

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.
Author
Owner

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
Owner

@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?
@ -0,0 +193,4 @@
*/
private fun Series<Double>.countExtrema(): Int {
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
return slice(1..< size - 1).asIterable().foldIndexed(0) { index, acc, it ->
Owner

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]) } ```
@ -0,0 +203,4 @@
* 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> Series<T>.maxima(): List<Int> where T: Comparable<T> {
Owner
private fun <T : Comparable<T>> Series<T>.maxima(): List<Int> {
```kotlin private fun <T : Comparable<T>> Series<T>.maxima(): List<Int> { ```
@ -0,0 +206,4 @@
private fun<T> Series<T>.maxima(): List<Int> where T: Comparable<T> {
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
val maxima = mutableListOf(0)
slice(1..< size - 1).asIterable().forEachIndexed { index, it ->
Owner

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

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.
Author
Owner

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`
@ -0,0 +207,4 @@
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
val maxima = mutableListOf(0)
slice(1..< size - 1).asIterable().forEachIndexed { index, it ->
if (it > get(index) && it > get(index + 2)) { // weird offset, is there a way to do it better?
Owner

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?
@ -0,0 +219,4 @@
* 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> Series<T>.minima(): List<Int> where T: Comparable<T> {
Owner
private fun <T : Comparable<T>> Series<T>.minima(): List<Int> {
```kotlin private fun <T : Comparable<T>> Series<T>.minima(): List<Int> { ```
@ -0,0 +222,4 @@
private fun<T> Series<T>.minima(): List<Int> where T: Comparable<T> {
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
val minima = mutableListOf(0)
slice(1..< size - 1).asIterable().forEachIndexed { index, it ->
Owner

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

Also, similar question to this one.

Also, similar question to [this one](https://git.sciprog.center/kscience/kmath/pulls/521/files#issuecomment-1868).
@ -0,0 +243,4 @@
* Represents a condition when a mode has been successfully
* extracted in a sifting process.
*/
open class Success(val result: Series<Double>): SiftingResult
Owner

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.
teldufalsari added 1 commit 2024-04-25 21:02:31 +03:00
Author
Owner

Thank you kindly for the review. I have submitted corrections for the majority of issues. I'll notify you when I make significant improvements to the algorithm or the code in general.

Thank you kindly for the review. I have submitted corrections for the majority of issues. I'll notify you when I make significant improvements to the algorithm or the code in general.
teldufalsari added 5 commits 2024-05-03 23:37:55 +03:00
altavir added 1 commit 2024-05-09 09:27:48 +03:00
teldufalsari added 2 commits 2024-05-20 17:39:30 +03:00
This pull request has changes conflicting with the target branch.
  • kmath-stat/build.gradle.kts

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u feature/emd:teldufalsari-feature/emd
git checkout teldufalsari-feature/emd
Sign in to join this conversation.
No description provided.