diff --git a/examples/src/main/kotlin/space/kscience/kmath/series/emdDemo.kt b/examples/src/main/kotlin/space/kscience/kmath/series/emdDemo.kt index c69b163cb..a46f3768a 100644 --- a/examples/src/main/kotlin/space/kscience/kmath/series/emdDemo.kt +++ b/examples/src/main/kotlin/space/kscience/kmath/series/emdDemo.kt @@ -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 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, block: Scatter.() -> Unit = {}) { this.scatter { this.name = name - this.x.numbers = buffer.offsetIndices + this.x.numbers = buffer.labels this.y.doubles = buffer.toDoubleArray() block() } diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/EmpiricalModeDecomposition.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/EmpiricalModeDecomposition.kt index f5f77e4d1..700b15757 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/EmpiricalModeDecomposition.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/EmpiricalModeDecomposition.kt @@ -26,13 +26,13 @@ import space.kscience.kmath.structures.last * @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, +public class EmpiricalModeDecomposition, A: Field, BA, L: T> ( + private val seriesAlgebra: SeriesAlgebra, 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, BA: FieldOps> { +) where BA: BufferAlgebra, BA: FieldOps> { /** * Take a signal, construct an upper and a lower envelopes, find the mean value of two, @@ -42,18 +42,18 @@ public class EmpiricalModeDecomposition, BA, L: Number> ( * @return mean [Series] or `null`. `null` is returned in case * the signal does not have enough extrema to construct envelopes. */ - private fun findMean(signal: Series): Series? = (seriesAlgebra) { + private fun findMean(signal: Series): Series? = (seriesAlgebra) { val interpolator = SplineInterpolator(elementAlgebra) val makeBuffer = elementAlgebra.bufferFactory - fun generateEnvelope(extrema: List, paddedExtremeValues: Buffer): Series { + fun generateEnvelope(extrema: List, paddedExtremeValues: Buffer): Series { val envelopeFunction = interpolator.interpolate( - Buffer(extrema.size) { signal.labels[extrema[it]].toDouble() }, + 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? } } @@ -90,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): SiftingResult = siftInner(signal, 1, 0) + private fun sift(signal: Series): SiftingResult = siftInner(signal, 1, 0) /** * Compute a single iteration of the sifting process. */ private tailrec fun siftInner( - prevMode: Series, + prevMode: Series, iterationNumber: Int, sNumber: Int ): SiftingResult = (seriesAlgebra) { @@ -108,7 +108,8 @@ public class EmpiricalModeDecomposition, BA, L: Number> ( 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) } } @@ -121,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. */ - public fun decompose(signal: Series): EMDecompositionResult = (seriesAlgebra) { - val modes = mutableListOf>() + public fun decompose(signal: Series): EMDecompositionResult = (seriesAlgebra) { + val modes = mutableListOf>() var residual = signal repeat(nModes) { val nextMode = when(val r = sift(residual)) { @@ -130,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 } - modes.add(nextMode as Series) // TODO remove unchecked cast + modes.add(nextMode as Series) // 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) } } @@ -155,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 , BA> SeriesAlgebra.empiricalModeDecomposition( +public fun , L: T, A: Field, BA> SeriesAlgebra.empiricalModeDecomposition( sConditionThreshold: Int = 15, maxSiftIterations: Int = 20, - siftingDelta: Double = 1e-2, + siftingDelta: T, nModes: Int = 3 -): EmpiricalModeDecomposition -where BA: BufferAlgebra, BA: FieldOps> = EmpiricalModeDecomposition( +): EmpiricalModeDecomposition +where BA: BufferAlgebra, BA: FieldOps> = EmpiricalModeDecomposition( seriesAlgebra = this, sConditionThreshold = sConditionThreshold, maxSiftIterations = maxSiftIterations, @@ -281,5 +283,6 @@ public enum class EMDTerminationReason { public data class EMDecompositionResult( val terminatedBecause: EMDTerminationReason, - val modes: List> + val modes: List>, + val residual: Series ) \ No newline at end of file