From 1295a407c37db40c0fa993c7b7c45068e623c079 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 10 Apr 2022 15:29:46 +0300 Subject: [PATCH] Refactor tree histogram --- gradle.properties | 2 +- .../space/kscience/kmath/structures/Buffer.kt | 10 + .../kscience/kmath/histogram/Histogram1D.kt | 1 + .../kscience/kmath/histogram/HistogramND.kt | 6 +- .../kmath/histogram/UniformHistogram1D.kt | 4 +- .../histogram/UniformHistogramGroupND.kt | 16 +- .../kmath/histogram/TreeHistogramGroup.kt | 179 ++++++++++++++++ .../kmath/histogram/TreeHistogramSpace.kt | 199 ------------------ .../kmath/histogram/TreeHistogramTest.kt | 11 +- 9 files changed, 212 insertions(+), 216 deletions(-) create mode 100644 kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramGroup.kt delete mode 100644 kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt diff --git a/gradle.properties b/gradle.properties index 847c3dda6..80108737e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ kotlin.code.style=official kotlin.jupyter.add.scanner=false kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -kotlin.incremental.js.ir=true +#kotlin.incremental.js.ir=true org.gradle.configureondemand=true org.gradle.jvmargs=-XX:MaxMetaspaceSize=1G diff --git a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/structures/Buffer.kt b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/structures/Buffer.kt index 58c6d5ded..a1b0307c4 100644 --- a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/structures/Buffer.kt +++ b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/structures/Buffer.kt @@ -105,6 +105,16 @@ public interface Buffer { */ public val Buffer<*>.indices: IntRange get() = 0 until size +public fun Buffer.first(): T { + require(size > 0) { "Can't get the first element of empty buffer" } + return get(0) +} + +public fun Buffer.last(): T { + require(size > 0) { "Can't get the last element of empty buffer" } + return get(size - 1) +} + /** * Immutable wrapper for [MutableBuffer]. * diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt index 0c9352e76..710e4478b 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt @@ -43,6 +43,7 @@ public interface Histogram1DBuilder : HistogramBuilder, value: V) { + require(point.size == 1) { "Only points with single value could be used in Histogram1D" } putValue(point[0], value) } } diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/HistogramND.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/HistogramND.kt index 2af03abd4..68b24db5d 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/HistogramND.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/HistogramND.kt @@ -43,7 +43,7 @@ public class HistogramND, D : Domain, V : Any>( public interface HistogramGroupND, D : Domain, V : Any> : Group>, ScaleOperations> { public val shape: Shape - public val valueAlgebra: FieldOpsND //= NDAlgebra.space(valueSpace, Buffer.Companion::boxing, *shape), + public val valueAlgebraND: FieldOpsND //= NDAlgebra.space(valueSpace, Buffer.Companion::boxing, *shape), /** * Resolve index of the bin including given [point]. Return null if point is outside histogram area @@ -63,12 +63,12 @@ public interface HistogramGroupND, D : Domain, V : Any> : require(left.group == this && right.group == this) { "A histogram belonging to a different group cannot be operated." } - return HistogramND(this, valueAlgebra { left.values + right.values }) + return HistogramND(this, valueAlgebraND { left.values + right.values }) } override fun scale(a: HistogramND, value: Double): HistogramND { require(a.group == this) { "A histogram belonging to a different group cannot be operated." } - return HistogramND(this, valueAlgebra { a.values * value }) + return HistogramND(this, valueAlgebraND { a.values * value }) } override val zero: HistogramND get() = produce { } diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogram1D.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogram1D.kt index cb6572af9..e13928394 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogram1D.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogram1D.kt @@ -126,11 +126,11 @@ public class UniformHistogram1DGroup( } public fun Histogram.Companion.uniform1D( - algebra: A, + valueAlgebra: A, binSize: Double, startPoint: Double = 0.0, ): UniformHistogram1DGroup where A : Ring, A : ScaleOperations = - UniformHistogram1DGroup(algebra, binSize, startPoint) + UniformHistogram1DGroup(valueAlgebra, binSize, startPoint) @UnstableKMathAPI public fun UniformHistogram1DGroup.produce( diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogramGroupND.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogramGroupND.kt index b93da10b2..90ec29ce3 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogramGroupND.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformHistogramGroupND.kt @@ -24,7 +24,7 @@ public typealias HyperSquareBin = DomainBin * @param bufferFactory is an optional parameter used to optimize buffer production. */ public class UniformHistogramGroupND>( - override val valueAlgebra: FieldOpsND, + override val valueAlgebraND: FieldOpsND, private val lower: Buffer, private val upper: Buffer, private val binNums: IntArray = IntArray(lower.size) { 20 }, @@ -84,11 +84,11 @@ public class UniformHistogramGroupND>( override fun produce(builder: HistogramBuilder.() -> Unit): HistogramND { - val ndCounter = StructureND.buffered(shape) { Counter.of(valueAlgebra.elementAlgebra) } + val ndCounter = StructureND.buffered(shape) { Counter.of(valueAlgebraND.elementAlgebra) } val hBuilder = object : HistogramBuilder { - override val defaultValue: V get() = valueAlgebra.elementAlgebra.one + override val defaultValue: V get() = valueAlgebraND.elementAlgebra.one - override fun putValue(point: Point, value: V) = with(valueAlgebra.elementAlgebra) { + override fun putValue(point: Point, value: V) = with(valueAlgebraND.elementAlgebra) { val index = getIndexOrNull(point) ndCounter[index].add(value) } @@ -112,11 +112,11 @@ public class UniformHistogramGroupND>( *``` */ public fun > Histogram.Companion.uniformNDFromRanges( - valueAlgebra: FieldOpsND, + valueAlgebraND: FieldOpsND, vararg ranges: ClosedFloatingPointRange, bufferFactory: BufferFactory = Buffer.Companion::boxing, ): UniformHistogramGroupND = UniformHistogramGroupND( - valueAlgebra, + valueAlgebraND, ranges.map(ClosedFloatingPointRange::start).asBuffer(), ranges.map(ClosedFloatingPointRange::endInclusive).asBuffer(), bufferFactory = bufferFactory @@ -138,11 +138,11 @@ public fun Histogram.Companion.uniformDoubleNDFromRanges( *``` */ public fun > Histogram.Companion.uniformNDFromRanges( - valueAlgebra: FieldOpsND, + valueAlgebraND: FieldOpsND, vararg ranges: Pair, Int>, bufferFactory: BufferFactory = Buffer.Companion::boxing, ): UniformHistogramGroupND = UniformHistogramGroupND( - valueAlgebra, + valueAlgebraND, ListBuffer( ranges .map(Pair, Int>::first) diff --git a/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramGroup.kt b/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramGroup.kt new file mode 100644 index 000000000..6bec01f9b --- /dev/null +++ b/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramGroup.kt @@ -0,0 +1,179 @@ +/* + * 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/LICENSE.txt file. + */ + +@file:OptIn(UnstableKMathAPI::class) + +package space.kscience.kmath.histogram + +import space.kscience.kmath.domains.DoubleDomain1D +import space.kscience.kmath.domains.center +import space.kscience.kmath.misc.UnstableKMathAPI +import space.kscience.kmath.misc.sorted +import space.kscience.kmath.operations.Group +import space.kscience.kmath.operations.Ring +import space.kscience.kmath.operations.ScaleOperations +import space.kscience.kmath.structures.Buffer +import space.kscience.kmath.structures.first +import space.kscience.kmath.structures.indices +import space.kscience.kmath.structures.last +import java.util.* + +private fun > TreeMap.getBin(value: Double): B? { + // check ceiling entry and return it if it is what needed + val ceil = ceilingEntry(value)?.value + if (ceil != null && value in ceil) return ceil + //check floor entry + val floor = floorEntry(value)?.value + if (floor != null && value in floor) return floor + //neither is valid, not found + return null +} + +//public data class ValueAndError(val value: Double, val error: Double) +// +//public typealias WeightedBin1D = Bin1D + +/** + * A histogram based on a tree map of values + */ +public class TreeHistogram( + private val binMap: TreeMap>, +) : Histogram1D { + override fun get(value: Double): Bin1D? = binMap.getBin(value) + override val bins: Collection> get() = binMap.values +} + +/** + * A space for univariate histograms with variable bin borders based on a tree map + */ +public class TreeHistogramGroup( + public val valueAlgebra: A, + @PublishedApi internal val binFactory: (Double) -> DoubleDomain1D, +) : Group>, ScaleOperations> where A : Ring, A : ScaleOperations { + + internal inner class DomainCounter(val domain: DoubleDomain1D, val counter: Counter = Counter.of(valueAlgebra)) : + ClosedRange by domain.range + + @PublishedApi + internal inner class TreeHistogramBuilder : Histogram1DBuilder { + + override val defaultValue: V get() = valueAlgebra.one + + private val bins: TreeMap = TreeMap() + + private fun createBin(value: Double): DomainCounter { + val binDefinition: DoubleDomain1D = binFactory(value) + val newBin = DomainCounter(binDefinition) + synchronized(this) { + bins[binDefinition.center] = newBin + } + return newBin + } + + /** + * Thread safe put operation + */ + override fun putValue(at: Double, value: V) { + (bins.getBin(at) ?: createBin(at)).counter.add(value) + } + + fun build(): TreeHistogram { + val map = bins.mapValuesTo(TreeMap>()) { (_, binCounter) -> + Bin1D(binCounter.domain, binCounter.counter.value) + } + return TreeHistogram(map) + } + } + + public inline fun produce(block: Histogram1DBuilder.() -> Unit): TreeHistogram = + TreeHistogramBuilder().apply(block).build() + + override fun add( + left: TreeHistogram, + right: TreeHistogram, + ): TreeHistogram { + val bins = TreeMap>().apply { + (left.bins.map { it.domain } union right.bins.map { it.domain }).forEach { def -> + put( + def.center, + Bin1D( + def, + with(valueAlgebra) { + (left[def.center]?.binValue ?: zero) + (right[def.center]?.binValue ?: zero) + } + ) + ) + } + } + return TreeHistogram(bins) + } + + override fun scale(a: TreeHistogram, value: Double): TreeHistogram { + val bins = TreeMap>().apply { + a.bins.forEach { bin -> + put( + bin.domain.center, + Bin1D(bin.domain, valueAlgebra.scale(bin.binValue, value)) + ) + } + } + + return TreeHistogram(bins) + } + + override fun TreeHistogram.unaryMinus(): TreeHistogram = this * (-1) + + override val zero: TreeHistogram = produce { } +} + + +///** +// * Build and fill a histogram with custom borders. Returns a read-only histogram. +// */ +//public inline fun Histogram.custom( +// borders: DoubleArray, +// builder: Histogram1DBuilder.() -> Unit, +//): TreeHistogram = custom(borders).fill(builder) +// +// +///** +// * Build and fill a [DoubleHistogram1D]. Returns a read-only histogram. +// */ +//public fun uniform( +// binSize: Double, +// start: Double = 0.0, +//): TreeHistogramSpace = TreeHistogramSpace { value -> +// val center = start + binSize * floor((value - start) / binSize + 0.5) +// DoubleDomain1D((center - binSize / 2)..(center + binSize / 2)) +//} + +/** + * Create a histogram group with custom cell borders + */ +public fun Histogram.Companion.custom1D( + valueAlgebra: A, + borders: Buffer, +): TreeHistogramGroup where A : Ring, A : ScaleOperations { + val sorted = borders.sorted() + + return TreeHistogramGroup(valueAlgebra) { value -> + when { + value <= sorted.first() -> DoubleDomain1D( + Double.NEGATIVE_INFINITY..sorted.first() + ) + + value > sorted.last() -> DoubleDomain1D( + sorted.last()..Double.POSITIVE_INFINITY + ) + + else -> { + val index = sorted.indices.first { value <= sorted[it] } + val left = sorted[index - 1] + val right = sorted[index] + DoubleDomain1D(left..right) + } + } + } +} \ No newline at end of file diff --git a/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt deleted file mode 100644 index bff20c22c..000000000 --- a/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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/LICENSE.txt file. - */ - -@file:OptIn(UnstableKMathAPI::class) - -package space.kscience.kmath.histogram - -import space.kscience.kmath.domains.DoubleDomain1D -import space.kscience.kmath.domains.center -import space.kscience.kmath.misc.UnstableKMathAPI -import space.kscience.kmath.operations.Group -import space.kscience.kmath.operations.ScaleOperations -import space.kscience.kmath.structures.Buffer -import java.util.* -import kotlin.math.abs -import kotlin.math.floor -import kotlin.math.sqrt - -private fun > TreeMap.getBin(value: Double): B? { - // check ceiling entry and return it if it is what needed - val ceil = ceilingEntry(value)?.value - if (ceil != null && value in ceil) return ceil - //check floor entry - val floor = floorEntry(value)?.value - if (floor != null && value in floor) return floor - //neither is valid, not found - return null -} - -public data class ValueAndError(val value: Double, val error: Double) - -public typealias WeightedBin1D = Bin1D - -public class TreeHistogram( - private val binMap: TreeMap, -) : Histogram1D { - override fun get(value: Double): WeightedBin1D? = binMap.getBin(value) - override val bins: Collection get() = binMap.values -} - -@PublishedApi -internal class TreeHistogramBuilder(val binFactory: (Double) -> DoubleDomain1D) : Histogram1DBuilder { - - override val defaultValue: Double get() = 1.0 - - internal class BinCounter(val domain: DoubleDomain1D, val counter: Counter = Counter.ofDouble()) : - ClosedRange by domain.range - - private val bins: TreeMap = TreeMap() - - fun get(value: Double): BinCounter? = bins.getBin(value) - - fun createBin(value: Double): BinCounter { - val binDefinition: DoubleDomain1D = binFactory(value) - val newBin = BinCounter(binDefinition) - synchronized(this) { - bins[binDefinition.center] = newBin - } - return newBin - } - - /** - * Thread safe put operation - */ - override fun putValue(at: Double, value: Double) { - (get(at) ?: createBin(at)).apply { - counter.add(value) - } - } - - override fun putValue(point: Buffer, value: Double) { - require(point.size == 1) { "Only points with single value could be used in univariate histogram" } - putValue(point[0], value.toDouble()) - } - - fun build(): TreeHistogram { - val map = bins.mapValuesTo(TreeMap()) { (_, binCounter) -> - val count: Double = binCounter.counter.value - WeightedBin1D(binCounter.domain, ValueAndError(count, sqrt(count))) - } - return TreeHistogram(map) - } -} - -/** - * A space for univariate histograms with variable bin borders based on a tree map - */ -public class TreeHistogramSpace( - @PublishedApi internal val binFactory: (Double) -> DoubleDomain1D, -) : Group, ScaleOperations { - - public inline fun fill(block: Histogram1DBuilder.() -> Unit): TreeHistogram = - TreeHistogramBuilder(binFactory).apply(block).build() - - override fun add( - left: TreeHistogram, - right: TreeHistogram, - ): TreeHistogram { -// require(a.context == this) { "Histogram $a does not belong to this context" } -// require(b.context == this) { "Histogram $b does not belong to this context" } - val bins = TreeMap().apply { - (left.bins.map { it.domain } union right.bins.map { it.domain }).forEach { def -> - put( - def.center, - WeightedBin1D( - def, - ValueAndError( - (left[def.center]?.binValue?.value ?: 0.0) + (right[def.center]?.binValue?.value ?: 0.0), - (left[def.center]?.binValue?.error ?: 0.0) + (right[def.center]?.binValue?.error ?: 0.0) - ) - ) - ) - } - } - return TreeHistogram(bins) - } - - override fun scale(a: TreeHistogram, value: Double): TreeHistogram { - val bins = TreeMap().apply { - a.bins.forEach { bin -> - put( - bin.domain.center, - WeightedBin1D( - bin.domain, - ValueAndError( - bin.binValue.value * value, - abs(bin.binValue.error * value) - ) - ) - ) - } - } - - return TreeHistogram(bins) - } - - override fun TreeHistogram.unaryMinus(): TreeHistogram = this * (-1) - - override val zero: TreeHistogram by lazy { fill { } } - - public companion object { - /** - * Build and fill a [TreeHistogram]. Returns a read-only histogram. - */ - public inline fun uniform( - binSize: Double, - start: Double = 0.0, - builder: Histogram1DBuilder.() -> Unit, - ): TreeHistogram = uniform(binSize, start).fill(builder) - - /** - * Build and fill a histogram with custom borders. Returns a read-only histogram. - */ - public inline fun custom( - borders: DoubleArray, - builder: Histogram1DBuilder.() -> Unit, - ): TreeHistogram = custom(borders).fill(builder) - - - /** - * Build and fill a [DoubleHistogram1D]. Returns a read-only histogram. - */ - public fun uniform( - binSize: Double, - start: Double = 0.0, - ): TreeHistogramSpace = TreeHistogramSpace { value -> - val center = start + binSize * floor((value - start) / binSize + 0.5) - DoubleDomain1D((center - binSize / 2)..(center + binSize / 2)) - } - - /** - * Create a histogram with custom cell borders - */ - public fun custom(borders: DoubleArray): TreeHistogramSpace { - val sorted = borders.sortedArray() - - return TreeHistogramSpace { value -> - when { - value < sorted.first() -> DoubleDomain1D( - Double.NEGATIVE_INFINITY..sorted.first() - ) - - value > sorted.last() -> DoubleDomain1D( - sorted.last()..Double.POSITIVE_INFINITY - ) - - else -> { - val index = sorted.indices.first { value > sorted[it] } - val left = sorted[index] - val right = sorted[index + 1] - DoubleDomain1D(left..right) - } - } - } - } - } -} \ No newline at end of file diff --git a/kmath-histograms/src/jvmTest/kotlin/space/kscience/kmath/histogram/TreeHistogramTest.kt b/kmath-histograms/src/jvmTest/kotlin/space/kscience/kmath/histogram/TreeHistogramTest.kt index f1a8f953b..c4eeb53cc 100644 --- a/kmath-histograms/src/jvmTest/kotlin/space/kscience/kmath/histogram/TreeHistogramTest.kt +++ b/kmath-histograms/src/jvmTest/kotlin/space/kscience/kmath/histogram/TreeHistogramTest.kt @@ -6,19 +6,24 @@ package space.kscience.kmath.histogram import org.junit.jupiter.api.Test +import space.kscience.kmath.operations.DoubleField +import space.kscience.kmath.real.step import kotlin.random.Random +import kotlin.test.assertEquals import kotlin.test.assertTrue class TreeHistogramTest { @Test fun normalFill() { - val histogram = TreeHistogramSpace.uniform(0.1) { + val random = Random(123) + val histogram = Histogram.custom1D(DoubleField, 0.0..1.0 step 0.1).produce { repeat(100_000) { - putValue(Random.nextDouble()) + putValue(random.nextDouble()) } } - assertTrue { histogram.bins.count() > 10 } + assertTrue { histogram.bins.count() > 8} + assertEquals(100_000, histogram.bins.sumOf { it.binValue }.toInt()) } } \ No newline at end of file