forked from kscience/kmath
Changed according to the notes
This commit is contained in:
parent
13da33ecde
commit
455c9d188d
@ -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
|
||||
|
||||
|
||||
|
||||
// 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.
|
||||
* 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
|
||||
|
||||
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 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})
|
||||
|
||||
/**
|
||||
* 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<Double>, series2 : Series<Double>) : 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++
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user