WIP: feature/emd #521

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

View File

@ -0,0 +1,85 @@
/*
* Copyright 2018-2024 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package space.kscience.kmath.series
import space.kscience.kmath.operations.*
import space.kscience.kmath.structures.*
import space.kscience.plotly.*
import space.kscience.plotly.models.Scatter
import space.kscience.plotly.models.ScatterMode
import kotlin.math.sin
private val customAlgebra = (Double.algebra.bufferAlgebra) { SeriesAlgebra(this) { it * 50.0 / 599.0 } }
fun main(): Unit = (customAlgebra) {
/*
val signal = DoubleArray(600) {
val x = it * 50.0 / 599
(3.0 * sin(x) + 0.5 * cos(7.0 * x)).coerceIn(-3.0 .. 3.0)
}.asBuffer().moveTo(0)
val peaks = signal.peaks()
val troughs = signal.troughs()
println(peaks)
println(troughs)
fun Plot.series(name: String, buffer: Buffer<Double>, block: Scatter.() -> Unit = {}) {
scatter {
this.name = name
this.x.numbers = buffer.labels
this.y.doubles = buffer.toDoubleArray()
block()
}
}
Plotly.plot {
series("Signal", signal)
scatter {
name = "Peaks"
mode = ScatterMode.markers
x.doubles = peaks.map { signal.labels[it] }.toDoubleArray()
y.doubles = peaks.map { signal[it] }.toDoubleArray()
}
scatter {
name = "Troughs"
mode = ScatterMode.markers
x.doubles = troughs.map { signal.labels[it] }.toDoubleArray()
y.doubles = troughs.map { signal[it] }.toDoubleArray()
}
}.makeFile(resourceLocation = ResourceLocation.REMOTE)
*/
val nSamples = 600
val signal = DoubleArray(nSamples) {
val x = it * 12.0 / (nSamples - 1)
(3.5 * sin(x)).coerceIn(-3.0 .. 3.0)
}.asBuffer().moveTo(0)
val peaks = signal.peaks(PlateauEdgePolicy.KEEP_ALL_EDGES)
val troughs = signal.troughs(PlateauEdgePolicy.KEEP_ALL_EDGES)
println(peaks)
println(troughs)
fun Plot.series(name: String, buffer: Buffer<Double>, block: Scatter.() -> Unit = {}) {
scatter {
this.name = name
this.x.numbers = buffer.labels
this.y.doubles = buffer.toDoubleArray()
block()
}
}
Plotly.plot {
series("Signal", signal)
scatter {
name = "Peaks"
mode = ScatterMode.markers
x.doubles = peaks.map { signal.labels[it] }.toDoubleArray()
y.doubles = peaks.map { signal[it] }.toDoubleArray()
}
scatter {
name = "Troughs"
mode = ScatterMode.markers
x.doubles = troughs.map { signal.labels[it] }.toDoubleArray()
y.doubles = troughs.map { signal[it] }.toDoubleArray()
}
}.makeFile(resourceLocation = ResourceLocation.REMOTE)
}

View File

@ -174,7 +174,7 @@ where BA: BufferAlgebra<T, A>, BA: FieldOps<Buffer<T>> = EmpiricalModeDecomposit
/**
* Brute force count all zeros in the series.
*/
private fun <T: Comparable<T>, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.countZeros(
internal 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" }
@ -203,7 +203,7 @@ private fun <T, A: Ring<T>, BA> SeriesAlgebra<T, A, BA, *>.relativeDifference(
/**
* Brute force count all extrema of a series.
*/
private fun <T: Comparable<T>> Series<T>.countExtrema(): Int {
internal 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`

View File

@ -0,0 +1,66 @@
/*
* Copyright 2018-2024 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package space.kscience.kmath.series
import space.kscience.kmath.operations.algebra
import space.kscience.kmath.operations.bufferAlgebra
import space.kscience.kmath.operations.invoke
import space.kscience.kmath.structures.asBuffer
import kotlin.math.sin
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.random.Random
class TestEmd {
companion object{
val testAlgebra = (Double.algebra.bufferAlgebra) { SeriesAlgebra(this) { it.toDouble() } }
}
@Test
fun testBasic() = (testAlgebra) {
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)
assertEquals(emd.modes.size, 3)
emd.modes.forEach { imf ->
assertTrue(imf.peaks().size - imf.troughs().size in -1..1)
}
}
@Test
fun testNoiseFiltering() = (testAlgebra) {
val signal = DoubleArray(800) {
sin(it.toDouble() / 30.0) + 2.0 * (Random.nextDouble() - 0.5)
}.asBuffer().moveTo(0)
val emd = empiricalModeDecomposition(
sConditionThreshold = 10,
maxSiftIterations = 15,
siftingDelta = 1e-2,
nModes = 10
).decompose(signal)
// Check whether the signal with the expected frequency is present
assertEquals(emd.modes.count { it.countExtrema() in 7..9 }, 1)
}
@Test
fun testZeros() = (testAlgebra) {
val nSamples = 200
// sin(10*x) where x in [0, 1)
val signal = DoubleArray(nSamples) {
sin(it * 10.0 / (nSamples - 1))
}.asBuffer().moveTo(0)
assertEquals(countZeros(signal), 4)
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2018-2024 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package space.kscience.kmath.series
import space.kscience.kmath.operations.algebra
import space.kscience.kmath.operations.bufferAlgebra
import space.kscience.kmath.operations.invoke
import space.kscience.kmath.structures.asBuffer
import kotlin.math.sin
import kotlin.test.Test
import kotlin.test.assertEquals
class TestPeakFinding {
companion object {
val testAlgebra = (Double.algebra.bufferAlgebra) { SeriesAlgebra(this) { it.toDouble() } }
}
@Test
fun testPeakFinding() = (testAlgebra) {
val nSamples = 600
val signal = DoubleArray(nSamples) {
val x = it * 12.0 / (nSamples - 1)
(3.5 * sin(x)).coerceIn(-3.0 .. 3.0)
}.asBuffer().moveTo(0)
val peaksAvg = signal.peaks(PlateauEdgePolicy.AVERAGE)
val troughsAvg = signal.troughs(PlateauEdgePolicy.AVERAGE)
assertEquals(peaksAvg.size, 2)
assertEquals(troughsAvg.size, 2)
val peaksBoth = signal.peaks(PlateauEdgePolicy.KEEP_ALL_EDGES)
val troughsBoth = signal.peaks(PlateauEdgePolicy.KEEP_ALL_EDGES)
assertEquals(peaksBoth.size, 4)
assertEquals(troughsBoth.size, 4)
}
}