WIP: feature/emd #521
@ -5,20 +5,24 @@
|
|||||||
|
|
||||||
package space.kscience.kmath.series
|
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.structures.*
|
||||||
import space.kscience.kmath.operations.invoke
|
import space.kscience.kmath.operations.invoke
|
||||||
import space.kscience.plotly.*
|
import space.kscience.plotly.*
|
||||||
import space.kscience.plotly.models.Scatter
|
import space.kscience.plotly.models.Scatter
|
||||||
|
|||||||
import kotlin.math.sin
|
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) {
|
val signal = DoubleArray(800) {
|
||||||
sin(it.toDouble() / 10.0) + 3.5 * sin(it.toDouble() / 60.0)
|
sin(it.toDouble() / 10.0) + 3.5 * sin(it.toDouble() / 60.0)
|
||||||
}.asBuffer().moveTo(0)
|
}.asBuffer().moveTo(0)
|
||||||
|
|
||||||
val emd = empiricalModeDecomposition(
|
val emd = empiricalModeDecomposition(
|
||||||
sConditionThreshold = 1,
|
sConditionThreshold = 1,
|
||||||
maxSiftIterations = 15,
|
maxSiftIterations = 15,
|
||||||
|
siftingDelta = 1e-2,
|
||||||
nModes = 4
|
nModes = 4
|
||||||
).decompose(signal)
|
).decompose(signal)
|
||||||
println("EMD: ${emd.modes.size} modes extracted, terminated because ${emd.terminatedBecause}")
|
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 = {}) {
|
fun Plot.series(name: String, buffer: Buffer<Double>, block: Scatter.() -> Unit = {}) {
|
||||||
this.scatter {
|
this.scatter {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.x.numbers = buffer.offsetIndices
|
this.x.numbers = buffer.labels
|
||||||
this.y.doubles = buffer.toDoubleArray()
|
this.y.doubles = buffer.toDoubleArray()
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,8 @@ package space.kscience.kmath.series
|
|||||||
import space.kscience.kmath.interpolation.SplineInterpolator
|
import space.kscience.kmath.interpolation.SplineInterpolator
|
||||||
import space.kscience.kmath.interpolation.interpolate
|
import space.kscience.kmath.interpolation.interpolate
|
||||||
import space.kscience.kmath.operations.*
|
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.Buffer
|
||||||
import space.kscience.kmath.structures.asBuffer
|
import space.kscience.kmath.structures.last
|
||||||
import kotlin.math.sign
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empirical mode decomposition of a signal represented as a [Series].
|
* 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
|
* @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.
|
* possible to extract more modes from the signal.
|
||||||
*/
|
*/
|
||||||
public class EmpiricalModeDecomposition<BA, L: Number> (
|
public class EmpiricalModeDecomposition<T: Comparable<T>, A: Field<T>, BA, L: T> (
|
||||||
private val seriesAlgebra: SeriesAlgebra<Double, *, BA, L>,
|
private val seriesAlgebra: SeriesAlgebra<T, A, BA, L>,
|
||||||
private val sConditionThreshold: Int = 15,
|
private val sConditionThreshold: Int = 15,
|
||||||
private val maxSiftIterations: Int = 20,
|
private val maxSiftIterations: Int = 20,
|
||||||
private val siftingDelta: Double = 1e-2,
|
private val siftingDelta: T,
|
||||||
private val nModes: Int = 6
|
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,
|
* 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
|
* @return mean [Series] or `null`. `null` is returned in case
|
||||||
lounres
commented
Possible typos fix suggestion:
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.
|
* the signal does not have enough extrema to construct envelopes.
|
||||||
*/
|
*/
|
||||||
private fun findMean(signal: Series<Double>): Series<Double>? = (seriesAlgebra) {
|
private fun findMean(signal: Series<T>): Series<T>? = (seriesAlgebra) {
|
||||||
val interpolator = SplineInterpolator(Float64Field)
|
val interpolator = SplineInterpolator(elementAlgebra)
|
||||||
lounres
commented
I suggest adding a sentence that describes what is actually returned. And possible typos fix as well.
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.
```
|
|||||||
fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: DoubleArray): Series<Double> {
|
val makeBuffer = elementAlgebra.bufferFactory
|
||||||
|
fun generateEnvelope(extrema: List<Int>, paddedExtremeValues: Buffer<T>): Series<T> {
|
||||||
lounres
commented
Just this is enough. Because there is ```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(
|
val envelopeFunction = interpolator.interpolate(
|
||||||
Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() },
|
makeBuffer(extrema.size) { signal.labels[extrema[it]] },
|
||||||
paddedExtremeValues.asBuffer()
|
paddedExtremeValues
|
||||||
)
|
)
|
||||||
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()) ?: paddedExtremeValues.last()
|
envelopeFunction(label) ?: paddedExtremeValues.last()
|
||||||
// need to make the interpolator yield values outside boundaries?
|
// need to make the interpolator yield values outside boundaries?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Extrema padding (experimental) TODO padding needs a dedicated function
|
// Extrema padding (experimental) TODO padding needs a dedicated function
|
||||||
val maxima = listOf(0) + signal.peaks() + (signal.size - 1)
|
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]) {
|
if (maxValues[0] < maxValues[1]) {
|
||||||
maxValues[0] = maxValues[1]
|
maxValues[0] = maxValues[1]
|
||||||
}
|
}
|
||||||
if (maxValues.last() < maxValues[maxValues.lastIndex - 1]) {
|
if (maxValues.last() < maxValues[maxValues.size - 2]) {
|
||||||
maxValues[maxValues.lastIndex] = maxValues[maxValues.lastIndex - 1]
|
maxValues[maxValues.size - 1] = maxValues[maxValues.size - 2]
|
||||||
lounres
commented
```kotlin
return upperEnvelope.zip(lowerEnvelope) { left, right -> (left + right) / 2 }
```
|
|||||||
}
|
}
|
||||||
|
|
||||||
val minima = listOf(0) + signal.troughs() + (signal.size - 1)
|
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]) {
|
if (minValues[0] > minValues[1]) {
|
||||||
minValues[0] = minValues[1]
|
minValues[0] = minValues[1]
|
||||||
}
|
}
|
||||||
if (minValues.last() > minValues[minValues.lastIndex - 1]) {
|
if (minValues.last() > minValues[minValues.size - 2]) {
|
||||||
minValues[minValues.lastIndex] = minValues[minValues.lastIndex - 1]
|
minValues[minValues.size - 1] = minValues[minValues.size - 2]
|
||||||
}
|
}
|
||||||
return if (maxima.size < 3 || minima.size < 3) null else { // maybe make an early return?
|
return if (maxima.size < 3 || minima.size < 3) null else { // maybe make an early return?
|
||||||
val upperEnvelope = generateEnvelope(maxima, maxValues)
|
val upperEnvelope = generateEnvelope(maxima, maxValues)
|
||||||
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
|
|||||||
@ -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.
|
* @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.
|
* 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.
|
* Compute a single iteration of the sifting process.
|
||||||
*/
|
*/
|
||||||
private tailrec fun siftInner(
|
private tailrec fun siftInner(
|
||||||
prevMode: Series<Double>,
|
prevMode: Series<T>,
|
||||||
iterationNumber: Int,
|
iterationNumber: Int,
|
||||||
sNumber: Int
|
sNumber: Int
|
||||||
lounres
commented
Gosh. Use
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) {
|
): SiftingResult = (seriesAlgebra) {
|
||||||
@ -106,11 +104,12 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
|
|||||||
return if (iterationNumber == 1) SiftingResult.NotEnoughExtrema
|
return if (iterationNumber == 1) SiftingResult.NotEnoughExtrema
|
||||||
else SiftingResult.SignalFlattened(prevMode)
|
else SiftingResult.SignalFlattened(prevMode)
|
||||||
val mode = prevMode.zip(mean) { p, m -> p - m }
|
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 {
|
return when {
|
||||||
iterationNumber >= maxSiftIterations -> SiftingResult.MaxIterationsReached(mode)
|
iterationNumber >= maxSiftIterations -> SiftingResult.MaxIterationsReached(mode)
|
||||||
sNumber >= sConditionThreshold -> SiftingResult.SNumberReached(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)
|
else -> siftInner(mode, iterationNumber + 1, newSNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lounres
commented
The whole function can be rewritten in such way:
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
|
* Modes returned in a list which contains as many modes as it was possible
|
||||||
* to extract before triggering one of the termination conditions.
|
* to extract before triggering one of the termination conditions.
|
||||||
*/
|
*/
|
||||||
lounres
commented
You should use 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) {
|
public fun decompose(signal: Series<T>): EMDecompositionResult<T> = (seriesAlgebra) {
|
||||||
val modes = mutableListOf<Series<Double>>()
|
val modes = mutableListOf<Series<T>>()
|
||||||
var residual = signal
|
var residual = signal
|
||||||
repeat(nModes) {
|
repeat(nModes) {
|
||||||
val nextMode = when(val r = sift(residual)) {
|
val nextMode = when(val r = sift(residual)) {
|
||||||
@ -132,14 +131,15 @@ public class EmpiricalModeDecomposition<BA, L: Number> (
|
|||||||
return EMDecompositionResult(
|
return EMDecompositionResult(
|
||||||
if (it == 0) EMDTerminationReason.SIGNAL_TOO_FLAT
|
if (it == 0) EMDTerminationReason.SIGNAL_TOO_FLAT
|
||||||
else EMDTerminationReason.ALL_POSSIBLE_MODES_EXTRACTED,
|
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 }
|
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
|
* @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.
|
* 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,
|
sConditionThreshold: Int = 15,
|
||||||
maxSiftIterations: Int = 20,
|
maxSiftIterations: Int = 20,
|
||||||
siftingDelta: Double = 1e-2,
|
siftingDelta: T,
|
||||||
nModes: Int = 3
|
nModes: Int = 3
|
||||||
): EmpiricalModeDecomposition<BA, L>
|
): EmpiricalModeDecomposition<T, A, BA, L>
|
||||||
where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> = EmpiricalModeDecomposition(
|
where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> = EmpiricalModeDecomposition(
|
||||||
seriesAlgebra = this,
|
seriesAlgebra = this,
|
||||||
sConditionThreshold = sConditionThreshold,
|
sConditionThreshold = sConditionThreshold,
|
||||||
maxSiftIterations = maxSiftIterations,
|
maxSiftIterations = maxSiftIterations,
|
||||||
@ -174,12 +174,15 @@ where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> = EmpiricalModeD
|
|||||||
/**
|
/**
|
||||||
* Brute force count all zeros in the series.
|
* Brute force count all zeros in the series.
|
||||||
*/
|
*/
|
||||||
private fun Series<Double>.countZeros(): Int {
|
private fun <T: Comparable<T>, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.countZeros(
|
||||||
require(size >= 2) { "Expected series with at least 2 elements, but got $size elements" }
|
signal: Series<T>
|
||||||
data class SignCounter(val prevSign: Double, val zeroCount: Int)
|
): 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
|
||||||
lounres
commented
Use
Also:
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.
teldufalsari
commented
I also wondered why there is no 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
lounres
commented
@altavir There is a need for a function @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 ->
|
return signal.fold(SignCounter(strictSign(signal[0]), 0)) { acc, it ->
|
||||||
val currentSign = sign(it)
|
val currentSign = strictSign(it)
|
||||||
if (acc.prevSign != currentSign) SignCounter(currentSign, acc.zeroCount + 1)
|
if (acc.prevSign != currentSign) SignCounter(currentSign, acc.zeroCount + 1)
|
||||||
else SignCounter(currentSign, acc.zeroCount)
|
else SignCounter(currentSign, acc.zeroCount)
|
||||||
}.zeroCount
|
}.zeroCount
|
||||||
@ -188,18 +191,19 @@ private fun Series<Double>.countZeros(): Int {
|
|||||||
/**
|
/**
|
||||||
* Compute relative difference of two series.
|
* Compute relative difference of two series.
|
||||||
*/
|
*/
|
||||||
private fun <BA> SeriesAlgebra<Double, *, BA, *>.relativeDifference(
|
private fun <T, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.relativeDifference(
|
||||||
current: Series<Double>,
|
current: Series<T>,
|
||||||
previous: Series<Double>
|
previous: Series<T>
|
||||||
lounres
commented
I would recommend writing
I would recommend writing
```kotlin
return (1 .. size - 2).count { isExtreme(this[it-1], this[it], this[it+1]) }
```
|
|||||||
):Double where BA: BufferAlgebra<Double, *>, BA: RingOps<Buffer<Double>> =
|
): T where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> = (bufferAlgebra) {
|
||||||
(current - previous).pow(2)
|
((current - previous) * (current - previous))
|
||||||
.div(previous pow 2)
|
.div(previous * previous)
|
||||||
.fold(0.0) { acc, d -> acc + d } // TODO replace with Series<>.sum() method when it's implemented
|
.fold(elementAlgebra.zero) { acc, it -> acc + it}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brute force count all extrema of a series.
|
* Brute force count all extrema of a series.
|
||||||
*/
|
*/
|
||||||
private fun Series<Double>.countExtrema(): Int {
|
private fun <T: Comparable<T>> Series<T>.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" }
|
require(size >= 3) { "Expected series with at least 3 elements, but got $size elements" }
|
||||||
return peaks().size + troughs().size
|
return peaks().size + troughs().size
|
||||||
}
|
}
|
||||||
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`
|
|||||||
@ -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.
|
* 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.
|
||||||
*/
|
*/
|
||||||
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 {
|
internal sealed interface SiftingResult {
|
||||||
|
|
||||||
@ -216,33 +223,33 @@ internal sealed interface SiftingResult {
|
|||||||
* Represents a condition when a mode has been successfully
|
* Represents a condition when a mode has been successfully
|
||||||
* extracted in a sifting process.
|
* extracted in a sifting process.
|
||||||
*/
|
*/
|
||||||
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, 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
|
* Returned when no termination condition was reached and the proto-mode
|
||||||
* has become too flat (with not enough extrema to build envelopes)
|
* has become too flat (with not enough extrema to build envelopes)
|
||||||
* after several sifting iterations.
|
* 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
|
* Returned when sifting process has been terminated due to the
|
||||||
* S-number condition being reached.
|
* 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
|
* Returned when sifting process has been terminated due to the
|
||||||
* delta condition (Cauchy criterion) being reached.
|
* delta condition (Cauchy criterion) being reached.
|
||||||
*/
|
*/
|
||||||
class DeltaReached(result: Series<Double>) : Success(result)
|
class DeltaReached<T>(result: Series<T>) : Success<T>(result)
|
||||||
|
|
||||||
lounres
commented
I don't understand what the 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
|
* Returned when sifting process has been terminated after
|
||||||
* executing the maximum number of iterations (specified when creating an instance
|
* executing the maximum number of iterations (specified when creating an instance
|
||||||
* of [EmpiricalModeDecomposition]).
|
* 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,
|
* Returned when the submitted signal has not enough extrema to build envelopes,
|
||||||
@ -274,7 +281,8 @@ public enum class EMDTerminationReason {
|
|||||||
ALL_POSSIBLE_MODES_EXTRACTED
|
ALL_POSSIBLE_MODES_EXTRACTED
|
||||||
}
|
}
|
||||||
|
|
||||||
public data class EMDecompositionResult(
|
public data class EMDecompositionResult<T>(
|
||||||
val terminatedBecause: EMDTerminationReason,
|
val terminatedBecause: EMDTerminationReason,
|
||||||
val modes: List<Series<Double>>
|
val modes: List<Series<T>>,
|
||||||
|
val residual: Series<T>
|
||||||
)
|
)
|
is enough if you import
import space.kscience.kmath.operations.invoke
.