From 0f7a25762e98b48f7b7331c1714342cffc32b798 Mon Sep 17 00:00:00 2001 From: Alexis Manin Date: Sat, 13 Nov 2021 09:03:05 +0100 Subject: [PATCH 1/2] feat(Core): add a permutation sorting prototype for buffers This is a Buffer extension function to create a list of permuted indices that represent the sequence of naturally sorted buffer elements --- .../space/kscience/kmath/misc/sorting.kt | 40 +++++++++++ .../space/kscience/kmath/misc/PermSortTest.kt | 72 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt create mode 100644 kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt diff --git a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt new file mode 100644 index 000000000..a72ef19cc --- /dev/null +++ b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018-2021 KMath contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package space.kscience.kmath.misc + +import kotlin.comparisons.* +import space.kscience.kmath.misc.UnstableKMathAPI +import space.kscience.kmath.structures.Buffer +import space.kscience.kmath.structures.indices + +/** + * Return a new list filled with buffer indices. Indice order is defined by sorting associated buffer value. + * This feature allows to sort buffer values without reordering its content. + * + * @param descending True to revert sort order from highest to lowest values. Default to ascending order. + * @return List of buffer indices, sorted by associated value. + */ +@PerformancePitfall +@UnstableKMathAPI +public fun > Buffer.permSort(descending : Boolean = false) : IntArray { + if (size < 2) return IntArray(size) + + val comparator = if (descending) compareByDescending { get(it) } else compareBy { get(it) } + + /* TODO: optimisation : keep a constant big array of indices (Ex: from 0 to 4096), then create indice + * arrays more efficiently by copying subpart of cached one. For bigger needs, we could copy entire + * cached array, then fill remaining indices manually. Not done for now, because: + * 1. doing it right would require some statistics about common used buffer sizes. + * 2. Some benchmark would be needed to ensure it would really provide better performance + */ + val packedIndices = IntArray(size) { idx -> idx } + + /* TODO: find an efficient way to sort in-place instead, and return directly the IntArray. + * Not done for now, because no standard utility is provided yet. An open issue exists for this. + * See: https://youtrack.jetbrains.com/issue/KT-37860 + */ + return packedIndices.sortedWith(comparator).toIntArray() +} diff --git a/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt b/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt new file mode 100644 index 000000000..cb47f2d4b --- /dev/null +++ b/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2018-2021 KMath contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package space.kscience.kmath.misc + +import kotlin.collections.mutableListOf +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +import space.kscience.kmath.structures.IntBuffer + +class PermSortTest { + + /** + * Permutation on empty buffer should immediately return an empty array. + */ + @Test + fun testOnEmptyBuffer() { + val emptyBuffer = IntBuffer(0) {it} + var permutations = emptyBuffer.permSort() + assertTrue(permutations.isEmpty(), "permutation on an empty buffer should return an empty result") + permutations = emptyBuffer.permSort(true) + assertTrue(permutations.isEmpty(), "permutation on an empty buffer should return an empty result") + } + + @Test + fun testOnSingleValueBuffer() { + testPermutation(1) + } + + @Test + public fun testOnSomeValues() { + testPermutation(10) + } + + private fun testPermutation(bufferSize: Int) { + + val seed = Random.nextLong() + println("Test randomization seed: $seed") + + val buffer = Random(seed).buffer(bufferSize) + val indices = buffer.permSort() + + assertEquals(bufferSize, indices.size) + // Ensure no doublon is present in indices + assertEquals(indices.toSet().size, indices.size) + + for (i in 0 until (bufferSize-1)) { + val current = buffer[indices[i]] + val next = buffer[indices[i+1]] + assertTrue(current <= next, "Permutation indices not properly sorted") + } + + val descIndices = buffer.permSort(true) + assertEquals(bufferSize, descIndices.size) + // Ensure no doublon is present in indices + assertEquals(descIndices.toSet().size, descIndices.size) + + for (i in 0 until (bufferSize-1)) { + val current = buffer[descIndices[i]] + val next = buffer[descIndices[i+1]] + assertTrue(current >= next, "Permutation indices not properly sorted in descending order") + } + } + + private fun Random.buffer(size : Int) = IntBuffer(size) { nextInt() } +} From 06a6a99ef0577ab13400de290f5fbfa9978f2717 Mon Sep 17 00:00:00 2001 From: Alexis Manin Date: Thu, 18 Nov 2021 17:44:53 +0100 Subject: [PATCH 2/2] feat(Core): add new flavors of permSort: allow user to specify a comparator (sort with) or a custom field to use in buffer values (sort by). --- .../space/kscience/kmath/misc/sorting.kt | 35 +++++++++++----- .../space/kscience/kmath/misc/PermSortTest.kt | 41 ++++++++++++++++--- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt index a72ef19cc..a144e49b4 100644 --- a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt +++ b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/misc/sorting.kt @@ -6,23 +6,38 @@ package space.kscience.kmath.misc import kotlin.comparisons.* -import space.kscience.kmath.misc.UnstableKMathAPI import space.kscience.kmath.structures.Buffer -import space.kscience.kmath.structures.indices /** * Return a new list filled with buffer indices. Indice order is defined by sorting associated buffer value. * This feature allows to sort buffer values without reordering its content. - * - * @param descending True to revert sort order from highest to lowest values. Default to ascending order. + * * @return List of buffer indices, sorted by associated value. */ @PerformancePitfall @UnstableKMathAPI -public fun > Buffer.permSort(descending : Boolean = false) : IntArray { - if (size < 2) return IntArray(size) +public fun > Buffer.permSort() : IntArray = _permSortWith(compareBy { get(it) }) - val comparator = if (descending) compareByDescending { get(it) } else compareBy { get(it) } +@PerformancePitfall +@UnstableKMathAPI +public fun > Buffer.permSortDescending() : IntArray = _permSortWith(compareByDescending { get(it) }) + +@PerformancePitfall +@UnstableKMathAPI +public fun > Buffer.permSortBy(selector: (V) -> C) : IntArray = _permSortWith(compareBy { selector(get(it)) }) + +@PerformancePitfall +@UnstableKMathAPI +public fun > Buffer.permSortByDescending(selector: (V) -> C) : IntArray = _permSortWith(compareByDescending { selector(get(it)) }) + +@PerformancePitfall +@UnstableKMathAPI +public fun Buffer.permSortWith(comparator : Comparator) : IntArray = _permSortWith { i1, i2 -> comparator.compare(get(i1), get(i2)) } + +@PerformancePitfall +@UnstableKMathAPI +private fun Buffer._permSortWith(comparator : Comparator) : IntArray { + if (size < 2) return IntArray(size) /* TODO: optimisation : keep a constant big array of indices (Ex: from 0 to 4096), then create indice * arrays more efficiently by copying subpart of cached one. For bigger needs, we could copy entire @@ -31,10 +46,10 @@ public fun > Buffer.permSort(descending : Boolean = false) : * 2. Some benchmark would be needed to ensure it would really provide better performance */ val packedIndices = IntArray(size) { idx -> idx } - - /* TODO: find an efficient way to sort in-place instead, and return directly the IntArray. + + /* TODO: find an efficient way to sort in-place instead, and return directly the IntArray. * Not done for now, because no standard utility is provided yet. An open issue exists for this. - * See: https://youtrack.jetbrains.com/issue/KT-37860 + * See: https://youtrack.jetbrains.com/issue/KT-37860 */ return packedIndices.sortedWith(comparator).toIntArray() } diff --git a/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt b/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt index cb47f2d4b..0a2bb9138 100644 --- a/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt +++ b/kmath-core/src/commonTest/kotlin/space/kscience/kmath/misc/PermSortTest.kt @@ -5,17 +5,24 @@ package space.kscience.kmath.misc -import kotlin.collections.mutableListOf +import space.kscience.kmath.misc.PermSortTest.Platform.* import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -import kotlin.test.fail import space.kscience.kmath.structures.IntBuffer +import space.kscience.kmath.structures.asBuffer +import kotlin.test.assertContentEquals class PermSortTest { + private enum class Platform { + ANDROID, JVM, JS, NATIVE, WASM + } + + private val platforms = Platform.values().asBuffer() + /** * Permutation on empty buffer should immediately return an empty array. */ @@ -24,7 +31,7 @@ class PermSortTest { val emptyBuffer = IntBuffer(0) {it} var permutations = emptyBuffer.permSort() assertTrue(permutations.isEmpty(), "permutation on an empty buffer should return an empty result") - permutations = emptyBuffer.permSort(true) + permutations = emptyBuffer.permSortDescending() assertTrue(permutations.isEmpty(), "permutation on an empty buffer should return an empty result") } @@ -34,10 +41,34 @@ class PermSortTest { } @Test - public fun testOnSomeValues() { + fun testOnSomeValues() { testPermutation(10) } + @Test + fun testPermSortBy() { + val permutations = platforms.permSortBy { it.name } + val expected = listOf(ANDROID, JS, JVM, NATIVE, WASM) + assertContentEquals(expected, permutations.map { platforms[it] }, "Ascending PermSort by name") + } + + @Test + fun testPermSortByDescending() { + val permutations = platforms.permSortByDescending { it.name } + val expected = listOf(WASM, NATIVE, JVM, JS, ANDROID) + assertContentEquals(expected, permutations.map { platforms[it] }, "Descending PermSort by name") + } + + @Test + fun testPermSortWith() { + var permutations = platforms.permSortWith { p1, p2 -> p1.name.length.compareTo(p2.name.length) } + val expected = listOf(JS, JVM, WASM, NATIVE, ANDROID) + assertContentEquals(expected, permutations.map { platforms[it] }, "PermSort using custom ascending comparator") + + permutations = platforms.permSortWith(compareByDescending { it.name.length }) + assertContentEquals(expected.reversed(), permutations.map { platforms[it] }, "PermSort using custom descending comparator") + } + private fun testPermutation(bufferSize: Int) { val seed = Random.nextLong() @@ -56,7 +87,7 @@ class PermSortTest { assertTrue(current <= next, "Permutation indices not properly sorted") } - val descIndices = buffer.permSort(true) + val descIndices = buffer.permSortDescending() assertEquals(bufferSize, descIndices.size) // Ensure no doublon is present in indices assertEquals(descIndices.toSet().size, descIndices.size)