From b6b8ac7de53332ebaa2823b5ebd18450a7ce3192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Tue, 4 Apr 2023 19:58:32 +0300 Subject: [PATCH 01/12] Hello world! --- .../kotlin/space/kscience/kmath/series/SomeTests.kt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt new file mode 100644 index 000000000..d7b06978b --- /dev/null +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt @@ -0,0 +1,5 @@ +package space.kscience.kmath.series + +fun main() { + println ("Hello world!") +} \ No newline at end of file From e7b56e49720e04075448dcf407a023ec5db6b6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Wed, 5 Apr 2023 01:25:58 +0300 Subject: [PATCH 02/12] New file in example dir --- .../kscience/kmath/structures/MyTests.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt diff --git a/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt b/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt new file mode 100644 index 000000000..8b4410db4 --- /dev/null +++ b/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt @@ -0,0 +1,23 @@ +/* + * 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.structures + +import space.kscience.kmath.operations.DoubleField +import space.kscience.kmath.operations.buffer +import space.kscience.kmath.operations.bufferAlgebra +import space.kscience.kmath.operations.withSize + +//inline fun MutableBuffer.Companion.same( +// n: Int, +// value: R +//): MutableBuffer = auto(n) { value } + + +fun main() { + with(DoubleField.bufferAlgebra.withSize(5)) { + println(number(2.0) + buffer(1, 2, 10, 4, 9)) + } +} \ No newline at end of file From b8809d1c2120bb6e2d4241dab34469568f5ea003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Sat, 22 Apr 2023 09:52:14 +0300 Subject: [PATCH 03/12] first DTW method realization --- .../kmath/series/DynamicTimeWarping.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt new file mode 100644 index 000000000..840348bbb --- /dev/null +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -0,0 +1,125 @@ +/* + * 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.structures.DoubleBuffer +import kotlin.math.abs + +public const val LEFT_OFFSET : Int = -1 +public const val BOTTOM_OFFSET : Int = 1 +public const val DIAGONAL_OFFSET : Int = 0 + +// TODO: Change container for alignMatrix to kmath special ND structure +public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, + val alignMatrix : Array = Array(0) {BooleanArray(0)}) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as DynamicTimeWarpingData + + if (totalCost != other.totalCost) return false + if (!alignMatrix.contentDeepEquals(other.alignMatrix)) return false + + return true + } + + override fun hashCode(): Int { + var result = totalCost.hashCode() + result = 31 * result + alignMatrix.contentDeepHashCode() + return result + } +} + +/** + * costMatrix calculates special matrix of costs alignment for two series. + * Formula: costMatrix[i, j] = euqlidNorm(series1(i), series2(j)) + min(costMatrix[i - 1, j], + * costMatrix[i, j - 1], + * costMatrix[i - 1, j - 1]). + * There is special cases for i = 0 or j = 0. + */ + +public fun costMatrix(series1 : DoubleBuffer, series2 : DoubleBuffer) : Array { + val dtwMatrix: Array = Array(series1.size){ row -> + DoubleArray(series2.size) { col -> + abs(series1[row] - series2[col]) + } + } + + for (i in dtwMatrix.indices) { + for (j in dtwMatrix[i].indices) { + dtwMatrix[i][j] += when { + i == 0 && j == 0 -> 0.0 + i == 0 -> dtwMatrix[i][j-1] + j == 0 -> dtwMatrix[i-1][j] + else -> minOf( + dtwMatrix[i][j-1], + dtwMatrix[i-1][j], + dtwMatrix[i-1][j-1] + ) + } + } + } + return dtwMatrix +} + +/** + * PathIndices class for better code perceptibility. + * Special fun moveOption represent offset for indices. Arguments of this function + * is flags for bottom, diagonal or left offsets respectively. + */ + +public data class PathIndices (var id_x: Int, var id_y: Int) { + public fun moveOption (direction: Int) { + when(direction) { + BOTTOM_OFFSET -> id_x-- + DIAGONAL_OFFSET -> { + id_x-- + id_y-- + } + LEFT_OFFSET -> id_y-- + else -> throw Exception("There is no such offset flag!") + } + } +} + +/** + * Final DTW method realization. Returns alignment matrix + * for two series comparing and penalty for this alignment. + */ + +public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : DynamicTimeWarpingData { + var cost = 0.0 + var pathLength = 0 + val costMatrix = costMatrix(series1, series2) + val alignMatrix : Array = Array(costMatrix.size) { BooleanArray(costMatrix.first().size) } + val indexes = PathIndices(alignMatrix.lastIndex, alignMatrix.last().lastIndex) + + with(indexes) { + alignMatrix[id_x][id_y] = true + cost += costMatrix[id_x][id_y] + pathLength++ + while (id_x != 0 || id_y != 0) { + when { + id_x == 0 || costMatrix[id_x][id_y] == costMatrix[id_x][id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { + moveOption(LEFT_OFFSET) + } + id_y == 0 || costMatrix[id_x][id_y] == costMatrix[id_x - 1][id_y] + abs(series1[id_x] - series2[id_y]) -> { + moveOption(BOTTOM_OFFSET) + } + costMatrix[id_x][id_y] == costMatrix[id_x - 1][id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { + moveOption(DIAGONAL_OFFSET) + } + } + alignMatrix[id_x][id_y] = true + cost += costMatrix[id_x][id_y] + pathLength++ + } + cost /= pathLength + } + return DynamicTimeWarpingData(cost, alignMatrix) +} + From 55bcd202bf74b8fdd8a920efd4a708a15f09053a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Sun, 11 Jun 2023 05:31:09 +0300 Subject: [PATCH 04/12] first DTW method realization --- .../space/kscience/kmath/ejml/_generated.kt | 2 +- .../kmath/series/DynamicTimeWarping.kt | 65 +++++++++---------- .../space/kscience/kmath/series/DTWTest.kt | 41 ++++++++++++ 3 files changed, 72 insertions(+), 36 deletions(-) create mode 100644 kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt diff --git a/kmath-ejml/src/main/kotlin/space/kscience/kmath/ejml/_generated.kt b/kmath-ejml/src/main/kotlin/space/kscience/kmath/ejml/_generated.kt index c56583fa8..8ad7f7293 100644 --- a/kmath-ejml/src/main/kotlin/space/kscience/kmath/ejml/_generated.kt +++ b/kmath-ejml/src/main/kotlin/space/kscience/kmath/ejml/_generated.kt @@ -19,9 +19,9 @@ import org.ejml.sparse.csc.factory.DecompositionFactory_DSCC import org.ejml.sparse.csc.factory.DecompositionFactory_FSCC import org.ejml.sparse.csc.factory.LinearSolverFactory_DSCC import org.ejml.sparse.csc.factory.LinearSolverFactory_FSCC -import space.kscience.kmath.UnstableKMathAPI import space.kscience.kmath.linear.* import space.kscience.kmath.linear.Matrix +import space.kscience.kmath.UnstableKMathAPI import space.kscience.kmath.nd.StructureFeature import space.kscience.kmath.operations.DoubleField import space.kscience.kmath.operations.FloatField diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 840348bbb..9f6bb528e 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -5,6 +5,11 @@ package space.kscience.kmath.series +import space.kscience.kmath.nd.* +import space.kscience.kmath.nd.DoubleBufferND +import space.kscience.kmath.nd.ShapeND +import space.kscience.kmath.nd.ndAlgebra +import space.kscience.kmath.operations.DoubleField import space.kscience.kmath.structures.DoubleBuffer import kotlin.math.abs @@ -12,9 +17,11 @@ public const val LEFT_OFFSET : Int = -1 public const val BOTTOM_OFFSET : Int = 1 public const val DIAGONAL_OFFSET : Int = 0 + + // TODO: Change container for alignMatrix to kmath special ND structure public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, - val alignMatrix : Array = Array(0) {BooleanArray(0)}) { + val alignMatrix : IntBufferND = IntRingOpsND.structureND(ShapeND(0, 0)) { (i, j) -> 0}) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false @@ -22,16 +29,9 @@ public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, other as DynamicTimeWarpingData if (totalCost != other.totalCost) return false - if (!alignMatrix.contentDeepEquals(other.alignMatrix)) return false return true } - - override fun hashCode(): Int { - var result = totalCost.hashCode() - result = 31 * result + alignMatrix.contentDeepHashCode() - return result - } } /** @@ -42,25 +42,20 @@ public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, * There is special cases for i = 0 or j = 0. */ -public fun costMatrix(series1 : DoubleBuffer, series2 : DoubleBuffer) : Array { - val dtwMatrix: Array = Array(series1.size){ row -> - DoubleArray(series2.size) { col -> - abs(series1[row] - series2[col]) - } +public fun costMatrix(series1 : DoubleBuffer, series2 : DoubleBuffer) : DoubleBufferND { + val dtwMatrix = DoubleField.ndAlgebra.structureND(ShapeND(series1.size, series2.size)) { + (row, col) -> abs(series1[row] - series2[col]) } - - for (i in dtwMatrix.indices) { - for (j in dtwMatrix[i].indices) { - dtwMatrix[i][j] += when { - i == 0 && j == 0 -> 0.0 - i == 0 -> dtwMatrix[i][j-1] - j == 0 -> dtwMatrix[i-1][j] - else -> minOf( - dtwMatrix[i][j-1], - dtwMatrix[i-1][j], - dtwMatrix[i-1][j-1] - ) - } + for ( (row, col) in dtwMatrix.indices) { + dtwMatrix[row, col] += when { + row == 0 && col == 0 -> 0.0 + row == 0 -> dtwMatrix[row, col - 1] + col == 0 -> dtwMatrix[row - 1, col] + else -> minOf( + dtwMatrix[row, col - 1], + dtwMatrix[row - 1, col], + dtwMatrix[row - 1, col - 1] + ) } } return dtwMatrix @@ -95,27 +90,27 @@ public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : var cost = 0.0 var pathLength = 0 val costMatrix = costMatrix(series1, series2) - val alignMatrix : Array = Array(costMatrix.size) { BooleanArray(costMatrix.first().size) } - val indexes = PathIndices(alignMatrix.lastIndex, alignMatrix.last().lastIndex) + val alignMatrix: IntBufferND = IntRingOpsND.structureND(ShapeND(series1.size, series2.size)) {(row, col) -> 0} + val indexes = PathIndices(alignMatrix.indices.shape.first() - 1, alignMatrix.indices.shape.last() - 1) with(indexes) { - alignMatrix[id_x][id_y] = true - cost += costMatrix[id_x][id_y] + alignMatrix[id_x, id_y] = 1 + cost += costMatrix[id_x, id_y] pathLength++ while (id_x != 0 || id_y != 0) { when { - id_x == 0 || costMatrix[id_x][id_y] == costMatrix[id_x][id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { + id_x == 0 || costMatrix[id_x, id_y] == costMatrix[id_x, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { moveOption(LEFT_OFFSET) } - id_y == 0 || costMatrix[id_x][id_y] == costMatrix[id_x - 1][id_y] + abs(series1[id_x] - series2[id_y]) -> { + id_y == 0 || costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y] + abs(series1[id_x] - series2[id_y]) -> { moveOption(BOTTOM_OFFSET) } - costMatrix[id_x][id_y] == costMatrix[id_x - 1][id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { + costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { moveOption(DIAGONAL_OFFSET) } } - alignMatrix[id_x][id_y] = true - cost += costMatrix[id_x][id_y] + alignMatrix[id_x, id_y] = 1 + cost += costMatrix[id_x, id_y] pathLength++ } cost /= pathLength diff --git a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt new file mode 100644 index 000000000..649899fdb --- /dev/null +++ b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -0,0 +1,41 @@ +/* + * 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.DoubleBuffer +import space.kscience.kmath.structures.asBuffer +import kotlin.test.Test +import kotlin.test.assertEquals + +class DTWTest { + + @Test + fun someData() : Unit { + with(Double.algebra.bufferAlgebra) { + 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: DoubleBuffer = firstSequence.asBuffer() + val seriesTwo: DoubleBuffer = secondSequence.asBuffer() + + val result = 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] == 1) { + print("[$i, $j] ") + } + } + } + } +} + + From 455c9d188de1685b683d41135fcc319a400a3e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Tue, 13 Jun 2023 03:37:35 +0300 Subject: [PATCH 05/12] Changed according to the notes --- .../kmath/series/DynamicTimeWarping.kt | 93 ++++++++----------- 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 9f6bb528e..387ad942b 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -5,70 +5,35 @@ 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.nd.ndAlgebra -import space.kscience.kmath.operations.DoubleField -import space.kscience.kmath.structures.DoubleBuffer import kotlin.math.abs -public const val LEFT_OFFSET : Int = -1 -public const val BOTTOM_OFFSET : Int = 1 -public const val DIAGONAL_OFFSET : Int = 0 +/** + * Offset constants which will be used later. Added them for avoiding "magical numbers" problem. + */ +internal const val LEFT_OFFSET : Int = -1 +internal const val BOTTOM_OFFSET : Int = 1 +internal const val DIAGONAL_OFFSET : Int = 0 -// TODO: Change container for alignMatrix to kmath special ND structure -public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, - val alignMatrix : IntBufferND = IntRingOpsND.structureND(ShapeND(0, 0)) { (i, j) -> 0}) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as DynamicTimeWarpingData - - if (totalCost != other.totalCost) return false - - return true - } -} - /** - * costMatrix calculates special matrix of costs alignment for two series. - * Formula: costMatrix[i, j] = euqlidNorm(series1(i), series2(j)) + min(costMatrix[i - 1, j], - * costMatrix[i, j - 1], - * costMatrix[i - 1, j - 1]). - * There is special cases for i = 0 or j = 0. + * Public class to store result of method. Class contains total penalty cost for series alignment. + * Also, this class contains align matrix (which point of the first series matches to point of the other series). */ - -public fun costMatrix(series1 : DoubleBuffer, series2 : DoubleBuffer) : DoubleBufferND { - val dtwMatrix = DoubleField.ndAlgebra.structureND(ShapeND(series1.size, series2.size)) { - (row, col) -> abs(series1[row] - series2[col]) - } - for ( (row, col) in dtwMatrix.indices) { - dtwMatrix[row, col] += when { - row == 0 && col == 0 -> 0.0 - row == 0 -> dtwMatrix[row, col - 1] - col == 0 -> dtwMatrix[row - 1, col] - else -> minOf( - dtwMatrix[row, col - 1], - dtwMatrix[row - 1, col], - dtwMatrix[row - 1, col - 1] - ) - } - } - return dtwMatrix -} +public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, + val alignMatrix : DoubleBufferND = DoubleFieldOpsND.structureND(ShapeND(0, 0)) { (_, _) -> 0.0}) /** * PathIndices class for better code perceptibility. * Special fun moveOption represent offset for indices. Arguments of this function * is flags for bottom, diagonal or left offsets respectively. */ - -public data class PathIndices (var id_x: Int, var id_y: Int) { - public fun moveOption (direction: Int) { +internal data class PathIndices (var id_x: Int, var id_y: Int) { + fun moveOption (direction: Int) { when(direction) { BOTTOM_OFFSET -> id_x-- DIAGONAL_OFFSET -> { @@ -85,16 +50,36 @@ public data class PathIndices (var id_x: Int, var id_y: Int) { * Final DTW method realization. Returns alignment matrix * for two series comparing and penalty for this alignment. */ - -public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : DynamicTimeWarpingData { +@OptIn(PerformancePitfall::class) +public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series, series2 : Series) : DynamicTimeWarpingData { var cost = 0.0 var pathLength = 0 - val costMatrix = costMatrix(series1, series2) - val alignMatrix: IntBufferND = IntRingOpsND.structureND(ShapeND(series1.size, series2.size)) {(row, col) -> 0} + // Special matrix of costs alignment for two series. + val costMatrix = structureND(ShapeND(series1.size, series2.size)) { + (row, col) -> abs(series1[row] - series2[col]) + } + // Formula: costMatrix[i, j] = euqlidNorm(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 { + // There is special cases for i = 0 or j = 0. + row == 0 && col == 0 -> 0.0 + 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} val indexes = PathIndices(alignMatrix.indices.shape.first() - 1, alignMatrix.indices.shape.last() - 1) with(indexes) { - alignMatrix[id_x, id_y] = 1 + alignMatrix[id_x, id_y] = costMatrix[id_x, id_y] cost += costMatrix[id_x, id_y] pathLength++ while (id_x != 0 || id_y != 0) { @@ -109,7 +94,7 @@ public fun dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : moveOption(DIAGONAL_OFFSET) } } - alignMatrix[id_x, id_y] = 1 + alignMatrix[id_x, id_y] = costMatrix[id_x, id_y] cost += costMatrix[id_x, id_y] pathLength++ } From 1773b49b1f592001ba2521d7264378ab19031ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Tue, 13 Jun 2023 03:39:29 +0300 Subject: [PATCH 06/12] Changed tests --- .../kscience/kmath/structures/MyTests.kt | 23 ------------------- .../space/kscience/kmath/series/DTWTest.kt | 18 +++++++-------- 2 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt diff --git a/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt b/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt deleted file mode 100644 index 8b4410db4..000000000 --- a/examples/src/main/kotlin/space/kscience/kmath/structures/MyTests.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.structures - -import space.kscience.kmath.operations.DoubleField -import space.kscience.kmath.operations.buffer -import space.kscience.kmath.operations.bufferAlgebra -import space.kscience.kmath.operations.withSize - -//inline fun MutableBuffer.Companion.same( -// n: Int, -// value: R -//): MutableBuffer = auto(n) { value } - - -fun main() { - with(DoubleField.bufferAlgebra.withSize(5)) { - println(number(2.0) + buffer(1, 2, 10, 4, 9)) - } -} \ No newline at end of file diff --git a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt index 649899fdb..b97dd1df9 100644 --- a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt +++ b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -6,33 +6,31 @@ 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.DoubleBuffer import space.kscience.kmath.structures.asBuffer import kotlin.test.Test -import kotlin.test.assertEquals + class DTWTest { @Test fun someData() : Unit { - with(Double.algebra.bufferAlgebra) { + 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: DoubleBuffer = firstSequence.asBuffer() - val seriesTwo: DoubleBuffer = secondSequence.asBuffer() + val seriesOne: Series = firstSequence.asBuffer().moveTo(0) + val seriesTwo: Series = secondSequence.asBuffer().moveTo(0) - val result = dynamicTimeWarping(seriesOne, seriesTwo) + 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] == 1) { - print("[$i, $j] ") - } + if (result.alignMatrix[i, j] > 0.0) { + print("[$i, $j] ") + } } } } From 11cbf7cdc7316c7f3790719c4e4eb929b8c4f522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Tue, 13 Jun 2023 23:52:13 +0300 Subject: [PATCH 07/12] Deleted waste file. --- .../kotlin/space/kscience/kmath/series/SomeTests.kt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt deleted file mode 100644 index d7b06978b..000000000 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/SomeTests.kt +++ /dev/null @@ -1,5 +0,0 @@ -package space.kscience.kmath.series - -fun main() { - println ("Hello world!") -} \ No newline at end of file From d5c3aa563e22b929808a2a06eeb9fdc4c45dda2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Wed, 14 Jun 2023 04:13:39 +0300 Subject: [PATCH 08/12] Enum class and some code corrections. --- .../kmath/series/DynamicTimeWarping.kt | 36 ++++++++++--------- .../space/kscience/kmath/series/DTWTest.kt | 4 +-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 387ad942b..55d583fc0 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -9,23 +9,27 @@ 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 /** * Offset constants which will be used later. Added them for avoiding "magical numbers" problem. */ -internal const val LEFT_OFFSET : Int = -1 -internal const val BOTTOM_OFFSET : Int = 1 -internal const val DIAGONAL_OFFSET : Int = 0 - +internal enum class DtwOffset { + LEFT, + BOTTOM, + DIAGONAL +} /** * Public class to store result of method. Class contains total penalty cost for series alignment. * Also, this class contains align matrix (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}) +public data class DynamicTimeWarpingData( + val totalCost : Double = 0.0, + val alignMatrix : DoubleBufferND = DoubleFieldOpsND.structureND(ShapeND(0, 0)) { (_, _) -> 0.0} +) /** * PathIndices class for better code perceptibility. @@ -33,15 +37,14 @@ public data class DynamicTimeWarpingData(val totalCost : Double = 0.0, * is flags for bottom, diagonal or left offsets respectively. */ internal data class PathIndices (var id_x: Int, var id_y: Int) { - fun moveOption (direction: Int) { + fun moveOption (direction: DtwOffset) { when(direction) { - BOTTOM_OFFSET -> id_x-- - DIAGONAL_OFFSET -> { + DtwOffset.BOTTOM -> id_x-- + DtwOffset.DIAGONAL -> { id_x-- id_y-- } - LEFT_OFFSET -> id_y-- - else -> throw Exception("There is no such offset flag!") + DtwOffset.LEFT -> id_y-- } } } @@ -51,7 +54,7 @@ internal data class PathIndices (var id_x: Int, var id_y: Int) { * for two series comparing and penalty for this alignment. */ @OptIn(PerformancePitfall::class) -public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series, series2 : Series) : DynamicTimeWarpingData { +public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : DoubleBuffer, series2 : DoubleBuffer) : DynamicTimeWarpingData { var cost = 0.0 var pathLength = 0 // Special matrix of costs alignment for two series. @@ -76,7 +79,7 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series, series2 // 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} - val indexes = PathIndices(alignMatrix.indices.shape.first() - 1, alignMatrix.indices.shape.last() - 1) + val indexes = PathIndices(series1.size - 1, series2.size - 1) with(indexes) { alignMatrix[id_x, id_y] = costMatrix[id_x, id_y] @@ -85,13 +88,13 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series, series2 while (id_x != 0 || id_y != 0) { when { id_x == 0 || costMatrix[id_x, id_y] == costMatrix[id_x, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(LEFT_OFFSET) + moveOption(DtwOffset.LEFT) } id_y == 0 || costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(BOTTOM_OFFSET) + moveOption(DtwOffset.BOTTOM) } costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(DIAGONAL_OFFSET) + moveOption(DtwOffset.DIAGONAL) } } alignMatrix[id_x, id_y] = costMatrix[id_x, id_y] @@ -103,3 +106,4 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : Series, series2 return DynamicTimeWarpingData(cost, alignMatrix) } + diff --git a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt index b97dd1df9..292fa5da2 100644 --- a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt +++ b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -20,8 +20,8 @@ class DTWTest { 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: Series = firstSequence.asBuffer().moveTo(0) - val seriesTwo: Series = secondSequence.asBuffer().moveTo(0) + val seriesOne = firstSequence.asBuffer() + val seriesTwo = secondSequence.asBuffer() val result = DoubleFieldOpsND.dynamicTimeWarping(seriesOne, seriesTwo) println("Total penalty coefficient: ${result.totalCost}") From 31be2b547b2efd1f06b34e7766ab7252e787863d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Wed, 14 Jun 2023 23:55:25 +0300 Subject: [PATCH 09/12] Comments fixed. --- .../kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 55d583fc0..9ab96998e 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -61,7 +61,7 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : DoubleBuffer, series2 : val costMatrix = structureND(ShapeND(series1.size, series2.size)) { (row, col) -> abs(series1[row] - series2[col]) } - // Formula: costMatrix[i, j] = euqlidNorm(series1(i), series2(j)) + + // Formula: costMatrix[i, j] = euqlideanNorm(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 { From 39244ebc5295d01328d782cb2c5e7d0ca7a5e322 Mon Sep 17 00:00:00 2001 From: Gleb Minaev <43728100+lounres@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:08:32 +0300 Subject: [PATCH 10/12] Remove extra enum class and data class. Fix docstrings and comments readability. Fix code style violations. --- .../kmath/series/DynamicTimeWarping.kt | 100 +++++++----------- .../space/kscience/kmath/series/DTWTest.kt | 32 +++--- 2 files changed, 52 insertions(+), 80 deletions(-) diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 9ab96998e..72737ae57 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -14,17 +14,9 @@ import kotlin.math.abs /** - * Offset constants which will be used later. Added them for avoiding "magical numbers" problem. - */ -internal enum class DtwOffset { - LEFT, - BOTTOM, - DIAGONAL -} - -/** - * Public class to store result of method. Class contains total penalty cost for series alignment. - * Also, this class contains align matrix (which point of the first series matches to point of the other series). + * 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, @@ -32,41 +24,21 @@ public data class DynamicTimeWarpingData( ) /** - * PathIndices class for better code perceptibility. - * Special fun moveOption represent offset for indices. Arguments of this function - * is flags for bottom, diagonal or left offsets respectively. - */ -internal data class PathIndices (var id_x: Int, var id_y: Int) { - fun moveOption (direction: DtwOffset) { - when(direction) { - DtwOffset.BOTTOM -> id_x-- - DtwOffset.DIAGONAL -> { - id_x-- - id_y-- - } - DtwOffset.LEFT -> id_y-- - } - } -} - -/** - * Final DTW method realization. Returns alignment matrix - * for two series comparing and penalty for this alignment. + * 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 { - var cost = 0.0 - var pathLength = 0 - // Special matrix of costs alignment for two series. - val costMatrix = structureND(ShapeND(series1.size, series2.size)) { - (row, col) -> abs(series1[row] - series2[col]) + // 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]) } - // Formula: costMatrix[i, j] = euqlideanNorm(series1(i), series2(j)) + - // min(costMatrix[i - 1, j], costMatrix[i, j - 1], costMatrix[i - 1, j - 1]). - for ( (row, col) in costMatrix.indices) { + + // 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 { - // There is special cases for i = 0 or j = 0. - row == 0 && col == 0 -> 0.0 + row == 0 && col == 0 -> continue row == 0 -> costMatrix[row, col - 1] col == 0 -> costMatrix[row - 1, col] else -> minOf( @@ -76,33 +48,37 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : DoubleBuffer, series2 : ) } } + // 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} - val indexes = PathIndices(series1.size - 1, series2.size - 1) + 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 - with(indexes) { - alignMatrix[id_x, id_y] = costMatrix[id_x, id_y] - cost += costMatrix[id_x, id_y] - pathLength++ - while (id_x != 0 || id_y != 0) { - when { - id_x == 0 || costMatrix[id_x, id_y] == costMatrix[id_x, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(DtwOffset.LEFT) - } - id_y == 0 || costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(DtwOffset.BOTTOM) - } - costMatrix[id_x, id_y] == costMatrix[id_x - 1, id_y - 1] + abs(series1[id_x] - series2[id_y]) -> { - moveOption(DtwOffset.DIAGONAL) - } + alignMatrix[index1, index2] = costMatrix[index1, index2] + cost += costMatrix[index1, index2] + pathLength++ + while (index1 != 0 || index2 != 0) { + when { + index1 == 0 || costMatrix[index1, index2] == costMatrix[index1, index2 - 1] + abs(series1[index1] - series2[index2]) -> { + index2-- + } + index2 == 0 || 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[id_x, id_y] = costMatrix[id_x, id_y] - cost += costMatrix[id_x, id_y] - pathLength++ } - cost /= pathLength + alignMatrix[index1, index2] = costMatrix[index1, index2] + cost += costMatrix[index1, index2] + pathLength++ } + cost /= pathLength + return DynamicTimeWarpingData(cost, alignMatrix) } diff --git a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt index 292fa5da2..6ff107426 100644 --- a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt +++ b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -15,25 +15,21 @@ import kotlin.test.Test class DTWTest { @Test - fun someData() : Unit { - 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] ") - } + fun someData() { + 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] ") } } } } - - From c6e1eb8406666df8b4acaa428f5a907dbda9d322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Sat, 24 Jun 2023 14:14:06 +0300 Subject: [PATCH 11/12] Fixed algorithm mistakes. There was "index out of bounds" situation. --- .../space/kscience/kmath/series/DynamicTimeWarping.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt index 72737ae57..e3cc4531c 100644 --- a/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt +++ b/kmath-stat/src/commonMain/kotlin/space/kscience/kmath/series/DynamicTimeWarping.kt @@ -62,10 +62,16 @@ public fun DoubleFieldOpsND.dynamicTimeWarping(series1 : DoubleBuffer, series2 : pathLength++ while (index1 != 0 || index2 != 0) { when { - index1 == 0 || costMatrix[index1, index2] == costMatrix[index1, index2 - 1] + abs(series1[index1] - series2[index2]) -> { + index1 == 0 -> { index2-- } - index2 == 0 || costMatrix[index1, index2] == costMatrix[index1 - 1, index2] + abs(series1[index1] - series2[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]) -> { From 051a3893ece62c22f48077c5534ab853125a53bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=C2=96=C2=96jenY-Poltavchiny?= Date: Sat, 24 Jun 2023 14:15:56 +0300 Subject: [PATCH 12/12] Test added, examples moved to folder "examples". --- .../space/kscience/kmath/series/DTWTest.kt | 29 ++++++++++++++ .../space/kscience/kmath/series/DTWTest.kt | 38 +++++++++++++++---- 2 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 examples/src/main/kotlin/space/kscience/kmath/series/DTWTest.kt diff --git a/examples/src/main/kotlin/space/kscience/kmath/series/DTWTest.kt b/examples/src/main/kotlin/space/kscience/kmath/series/DTWTest.kt new file mode 100644 index 000000000..33ef0497f --- /dev/null +++ b/examples/src/main/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -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] ") + } + } +} diff --git a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt index 6ff107426..5cbb2706c 100644 --- a/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt +++ b/kmath-stat/src/commonTest/kotlin/space/kscience/kmath/series/DTWTest.kt @@ -6,30 +6,52 @@ 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, 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 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) - 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] ") + 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) } + }