Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DTW method realization #517

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions examples/src/main/kotlin/space/kscience/kmath/series/DTWTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2018-2023 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.nd.*
import space.kscience.kmath.operations.algebra
import space.kscience.kmath.operations.bufferAlgebra
import space.kscience.kmath.structures.asBuffer

fun main() = with(Double.algebra.bufferAlgebra.seriesAlgebra()) {
val firstSequence: DoubleArray = doubleArrayOf(0.0, 2.0, 3.0, 1.0, 3.0, 0.1, 0.0, 1.0)
val secondSequence: DoubleArray = doubleArrayOf(1.0, 0.0, 3.0, 0.0, 0.0, 3.0, 2.0, 0.0, 2.0)

val seriesOne = firstSequence.asBuffer()
val seriesTwo = secondSequence.asBuffer()

val result = DoubleFieldOpsND.dynamicTimeWarping(seriesOne, seriesTwo)
println("Total penalty coefficient: ${result.totalCost}")
print("Alignment: ")
println(result.alignMatrix)
for ((i , j) in result.alignMatrix.indices) {
if (result.alignMatrix[i, j] > 0.0) {
print("[$i, $j] ")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2018-2023 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.PerformancePitfall
import space.kscience.kmath.nd.*
import space.kscience.kmath.nd.DoubleBufferND
import space.kscience.kmath.nd.ShapeND
import space.kscience.kmath.structures.DoubleBuffer
import kotlin.math.abs


/**
* Stores a result of [dynamicTimeWarping]. The class contains:
* 1. [Total penalty cost][totalCost] for series alignment.
* 2. [Align matrix][alignMatrix] that describes which point of the first series matches to point of the other series.
*/
public data class DynamicTimeWarpingData(
val totalCost : Double = 0.0,
val alignMatrix : DoubleBufferND = DoubleFieldOpsND.structureND(ShapeND(0, 0)) { (_, _) -> 0.0}
)

/**
* DTW method implementation. Returns alignment matrix for two series comparing and penalty for this alignment.
*/
@OptIn(PerformancePitfall::class)
public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : DynamicTimeWarpingData {
// Create a special matrix of costs alignment for the two series.
val costMatrix = structureND(ShapeND(series1.size, series2.size)) { (row, col) ->
abs(series1[row] - series2[col])
}

lounres marked this conversation as resolved.
Show resolved Hide resolved
// Initialise the cost matrix by formulas
// costMatrix[i, j] = euclideanNorm(series1(i), series2(j)) +
// min(costMatrix[i - 1, j], costMatrix[i, j - 1], costMatrix[i - 1, j - 1]).
for ((row, col) in costMatrix.indices) {
costMatrix[row, col] += when {
row == 0 && col == 0 -> continue
row == 0 -> costMatrix[row, col - 1]
col == 0 -> costMatrix[row - 1, col]
else -> minOf(
costMatrix[row, col - 1],
costMatrix[row - 1, col],
costMatrix[row - 1, col - 1]
)
}
}

// alignMatrix contains non-zero values at position where two points from series matches
// Values are penalty for concatenation of current points.
val alignMatrix = structureND(ShapeND(series1.size, series2.size)) { _ -> 0.0}
var index1 = series1.size - 1
var index2 = series2.size - 1
var cost = 0.0
var pathLength = 0

alignMatrix[index1, index2] = costMatrix[index1, index2]
cost += costMatrix[index1, index2]
pathLength++
while (index1 != 0 || index2 != 0) {
when {
index1 == 0 -> {
index2--
}
index2 == 0 -> {
index1--
}
costMatrix[index1, index2] == costMatrix[index1, index2 - 1] + abs(series1[index1] - series2[index2]) -> {
index2--
}
costMatrix[index1, index2] == costMatrix[index1 - 1, index2] + abs(series1[index1] - series2[index2]) -> {
index1--
}
costMatrix[index1, index2] == costMatrix[index1 - 1, index2 - 1] + abs(series1[index1] - series2[index2]) -> {
index1--
index2--
}
}
alignMatrix[index1, index2] = costMatrix[index1, index2]
cost += costMatrix[index1, index2]
pathLength++
}
cost /= pathLength

return DynamicTimeWarpingData(cost, alignMatrix)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2018-2023 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.nd.*
import space.kscience.kmath.operations.DoubleField
import space.kscience.kmath.operations.algebra
import space.kscience.kmath.operations.bufferAlgebra
import space.kscience.kmath.structures.asBuffer
import space.kscience.kmath.structures.toDoubleBuffer
import kotlin.math.PI
import kotlin.test.Test
import kotlin.test.assertEquals


class DTWTest {

@Test
fun someData() {
val firstSequence: DoubleArray = doubleArrayOf(0.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 1.0)
val secondSequence: DoubleArray = doubleArrayOf(0.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 1.0)

val seriesOne = firstSequence.asBuffer()
val seriesTwo = secondSequence.asBuffer()

val result = DoubleFieldOpsND.dynamicTimeWarping(seriesOne, seriesTwo)
assertEquals(result.totalCost, 0.0)
}

@Test
fun pathTest() = with(Double.algebra.bufferAlgebra.seriesAlgebra()) {
val s1 = series(10) { DoubleField.sin(2 * PI * it / 100)}.toDoubleBuffer()
val s2 = series(20) {sin(PI * it / 100)}.toDoubleBuffer()
val s3 = series(20) {sin(PI * it / 100 + 2 * PI)}.toDoubleBuffer()

val resS1S2 = DoubleFieldOpsND.dynamicTimeWarping(s1, s2).alignMatrix
var pathLengthS1S2 = 0
for ((i,j) in resS1S2.indices) {
if (resS1S2[i, j] > 0.0) {
++pathLengthS1S2
}
}

val resS1S3 = DoubleFieldOpsND.dynamicTimeWarping(s1, s3).alignMatrix
var pathLengthS1S3 = 0
for ((i,j) in resS1S3.indices) {
if (resS1S2[i, j] > 0.0) {
++pathLengthS1S3
}
}
assertEquals(pathLengthS1S3, pathLengthS1S2)
}

}