From 2fdba53911633722677fe8eade648dc7cbacd242 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 11 Feb 2021 11:18:16 +0300 Subject: [PATCH 1/4] WIP histograms refactor --- kmath-histograms/build.gradle.kts | 12 +- .../kscience/kmath/histogram/Counters.kt | 47 ++++- .../kscience/kmath/histogram/Histogram.kt | 18 +- .../kscience/kmath/histogram/RealHistogram.kt | 13 +- .../histogram/MultivariateHistogramTest.kt | 4 +- .../kscience/kmath/histogram/Counters.kt | 37 ---- .../histogram/AbstractUnivariateHistogram.kt | 33 ++++ .../kscience/kmath/histogram/Counters.kt | 7 - .../kmath/histogram/UnivariateHistogram.kt | 133 ++++--------- .../histogram/UnivariateHistogramSpace.kt | 176 +++++++++++++++++- 10 files changed, 298 insertions(+), 182 deletions(-) delete mode 100644 kmath-histograms/src/jsMain/kotlin/kscience/kmath/histogram/Counters.kt create mode 100644 kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt delete mode 100644 kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/Counters.kt diff --git a/kmath-histograms/build.gradle.kts b/kmath-histograms/build.gradle.kts index 40196416e..6031b3e85 100644 --- a/kmath-histograms/build.gradle.kts +++ b/kmath-histograms/build.gradle.kts @@ -1,4 +1,10 @@ -plugins { id("ru.mipt.npm.mpp") } +plugins { + id("ru.mipt.npm.mpp") +} + +kscience { + useAtomic() +} kotlin.sourceSets { commonMain { @@ -6,8 +12,8 @@ kotlin.sourceSets { api(project(":kmath-core")) } } - commonTest{ - dependencies{ + commonTest { + dependencies { implementation(project(":kmath-for-real")) } } diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt index 7a263a9fc..a7a42873f 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt @@ -1,20 +1,47 @@ package kscience.kmath.histogram +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.getAndUpdate +import kscience.kmath.operations.Space + /* * Common representation for atomic counters * TODO replace with atomics */ -public expect class LongCounter() { - public fun decrement() - public fun increment() - public fun reset() - public fun sum(): Long - public fun add(l: Long) +public interface Counter { + public fun add(delta: T) + public val value: T } -public expect class DoubleCounter() { - public fun reset() - public fun sum(): Double - public fun add(d: Double) +public class IntCounter : Counter { + private val innerValue = atomic(0) + + override fun add(delta: Int) { + innerValue += delta + } + + override val value: Int get() = innerValue.value } + +public class LongCounter : Counter { + private val innerValue = atomic(0L) + + override fun add(delta: Long) { + innerValue += delta + } + + override val value: Long get() = innerValue.value +} + +public class ObjectCounter(public val space: Space) : Counter { + private val innerValue = atomic(space.zero) + + override fun add(delta: T) { + innerValue.getAndUpdate { space.run { it + delta } } + } + + override val value: T get() = innerValue.value +} + + diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt index 370a01215..2bc40af85 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt @@ -6,7 +6,7 @@ import kscience.kmath.structures.ArrayBuffer import kscience.kmath.structures.RealBuffer /** - * The bin in the histogram. The histogram is by definition always done in the real space + * The binned data element. Could be a histogram bin with a number of counts or an artificial construct */ public interface Bin : Domain { /** @@ -17,7 +17,7 @@ public interface Bin : Domain { public val center: Point } -public interface Histogram> : Iterable { +public interface Histogram> { /** * Find existing bin, corresponding to given coordinates */ @@ -27,9 +27,11 @@ public interface Histogram> : Iterable { * Dimension of the histogram */ public val dimension: Int + + public val bins: Collection } -public interface MutableHistogram> : Histogram { +public interface HistogramBuilder> : Histogram { /** * Increment appropriate bin @@ -39,16 +41,16 @@ public interface MutableHistogram> : Histogram { public fun put(point: Point): Unit = putWithWeight(point, 1.0) } -public fun MutableHistogram.put(vararg point: T): Unit = put(ArrayBuffer(point)) +public fun HistogramBuilder.put(vararg point: T): Unit = put(ArrayBuffer(point)) -public fun MutableHistogram.put(vararg point: Number): Unit = +public fun HistogramBuilder.put(vararg point: Number): Unit = put(RealBuffer(point.map { it.toDouble() }.toDoubleArray())) -public fun MutableHistogram.put(vararg point: Double): Unit = put(RealBuffer(point)) -public fun MutableHistogram.fill(sequence: Iterable>): Unit = sequence.forEach { put(it) } +public fun HistogramBuilder.put(vararg point: Double): Unit = put(RealBuffer(point)) +public fun HistogramBuilder.fill(sequence: Iterable>): Unit = sequence.forEach { put(it) } /** * Pass a sequence builder into histogram */ -public fun MutableHistogram.fill(block: suspend SequenceScope>.() -> Unit): Unit = +public fun HistogramBuilder.fill(block: suspend SequenceScope>.() -> Unit): Unit = fill(sequence(block).asIterable()) diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt index 21d873806..c8f9958e0 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt @@ -43,7 +43,7 @@ public class RealHistogram( private val lower: Buffer, private val upper: Buffer, private val binNums: IntArray = IntArray(lower.size) { 20 }, -) : MutableHistogram> { +) : HistogramBuilder> { private val strides = DefaultStrides(IntArray(binNums.size) { binNums[it] + 2 }) private val counts: NDStructure = NDStructure.auto(strides) { LongCounter() } private val values: NDStructure = NDStructure.auto(strides) { DoubleCounter() } @@ -88,11 +88,12 @@ public class RealHistogram( return MultivariateBinDefinition(RealBufferFieldOperations, center, binSize) } - public fun getBinDefinition(point: Buffer): MultivariateBinDefinition = getBinDefinition(getIndex(point)) + public fun getBinDefinition(point: Buffer): MultivariateBinDefinition = + getBinDefinition(getIndex(point)) public override operator fun get(point: Buffer): MultivariateBin? { val index = getIndex(point) - return MultivariateBin(getBinDefinition(index), getCount(index),getValue(index)) + return MultivariateBin(getBinDefinition(index), getCount(index), getValue(index)) } // fun put(point: Point){ @@ -106,10 +107,10 @@ public class RealHistogram( values[index].add(weight) } - public override operator fun iterator(): Iterator> = - strides.indices().map { index-> + override val bins: Collection> + get() = strides.indices().map { index -> MultivariateBin(getBinDefinition(index), counts[index].sum(), values[index].sum()) - }.iterator() + }.toList() /** * NDStructure containing number of events in bins without weights diff --git a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt index 87a2b3e68..ef2c1445a 100644 --- a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt +++ b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt @@ -16,7 +16,7 @@ internal class MultivariateHistogramTest { (-1.0..1.0) ) histogram.put(0.55, 0.55) - val bin = histogram.find { it.value.toInt() > 0 } ?: fail() + val bin = histogram.bins.find { it.value.toInt() > 0 } ?: fail() assertTrue { bin.contains(RealVector(0.55, 0.55)) } assertTrue { bin.contains(RealVector(0.6, 0.5)) } assertFalse { bin.contains(RealVector(-0.55, 0.55)) } @@ -40,6 +40,6 @@ internal class MultivariateHistogramTest { yield(RealVector(nextDouble(), nextDouble(), nextDouble())) } } - assertEquals(n, histogram.sumBy { it.value.toInt() }) + assertEquals(n, histogram.bins.sumBy { it.value.toInt() }) } } \ No newline at end of file diff --git a/kmath-histograms/src/jsMain/kotlin/kscience/kmath/histogram/Counters.kt b/kmath-histograms/src/jsMain/kotlin/kscience/kmath/histogram/Counters.kt deleted file mode 100644 index d0fa1f4c2..000000000 --- a/kmath-histograms/src/jsMain/kotlin/kscience/kmath/histogram/Counters.kt +++ /dev/null @@ -1,37 +0,0 @@ -package kscience.kmath.histogram - -public actual class LongCounter { - private var sum: Long = 0L - - public actual fun decrement() { - sum-- - } - - public actual fun increment() { - sum++ - } - - public actual fun reset() { - sum = 0 - } - - public actual fun sum(): Long = sum - - public actual fun add(l: Long) { - sum += l - } -} - -public actual class DoubleCounter { - private var sum: Double = 0.0 - - public actual fun reset() { - sum = 0.0 - } - - public actual fun sum(): Double = sum - - public actual fun add(d: Double) { - sum += d - } -} diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt new file mode 100644 index 000000000..efc6e92de --- /dev/null +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt @@ -0,0 +1,33 @@ +package kscience.kmath.histogram + + +/** + * Univariate histogram with log(n) bin search speed + */ +//private abstract class AbstractUnivariateHistogram{ +// +// public abstract val bins: TreeMap +// +// public open operator fun get(value: Double): B? { +// // check ceiling entry and return it if it is what needed +// val ceil = bins.ceilingEntry(value)?.value +// if (ceil != null && value in ceil) return ceil +// //check floor entry +// val floor = bins.floorEntry(value)?.value +// if (floor != null && value in floor) return floor +// //neither is valid, not found +// return null +// } + +// public override operator fun get(point: Buffer): B? = get(point[0]) +// +// public override val dimension: Int get() = 1 +// +// public override operator fun iterator(): Iterator = bins.values.iterator() +// +// public companion object { + +// } +//} + + diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/Counters.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/Counters.kt deleted file mode 100644 index efbd185ef..000000000 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/Counters.kt +++ /dev/null @@ -1,7 +0,0 @@ -package kscience.kmath.histogram - -import java.util.concurrent.atomic.DoubleAdder -import java.util.concurrent.atomic.LongAdder - -public actual typealias LongCounter = LongAdder -public actual typealias DoubleCounter = DoubleAdder diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt index 10aa9f8ca..1fb5172ce 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt @@ -6,71 +6,47 @@ import kscience.kmath.operations.SpaceElement import kscience.kmath.structures.Buffer import kscience.kmath.structures.asBuffer import kscience.kmath.structures.asSequence -import java.util.* -import kotlin.math.floor -//TODO move to common +public data class UnivariateHistogramBinDefinition( + val position: Double, + val size: Double, +) : Comparable { + override fun compareTo(other: UnivariateHistogramBinDefinition): Int = this.position.compareTo(other.position) +} -public class UnivariateBin( - public val position: Double, - public val size: Double, -) : Bin { - //internal mutation operations - internal val counter: LongCounter = LongCounter() - internal val weightCounter: DoubleCounter = DoubleCounter() +public interface UnivariateBin : Bin { + public val def: UnivariateHistogramBinDefinition - /** - * The precise number of events ignoring weighting - */ - public val count: Long get() = counter.sum() + public val position: Double get() = def.position + public val size: Double get() = def.size /** * The value of histogram including weighting */ - public override val value: Double get() = weightCounter.sum() + public override val value: Double + + /** + * Standard deviation of the bin value. Zero if not applicable + */ + public val standardDeviation: Double public override val center: Point get() = doubleArrayOf(position).asBuffer() + public override val dimension: Int get() = 1 - public operator fun contains(value: Double): Boolean = value in (position - size / 2)..(position + size / 2) public override fun contains(point: Buffer): Boolean = contains(point[0]) } -/** - * Univariate histogram with log(n) bin search speed - */ +public operator fun UnivariateBin.contains(value: Double): Boolean = + value in (position - size / 2)..(position + size / 2) + @OptIn(UnstableKMathAPI::class) -public abstract class UnivariateHistogram protected constructor( - protected val bins: TreeMap = TreeMap(), -) : Histogram, SpaceElement { - - public operator fun get(value: Double): UnivariateBin? { - // check ceiling entry and return it if it is what needed - val ceil = bins.ceilingEntry(value)?.value - if (ceil != null && value in ceil) return ceil - //check floor entry - val floor = bins.floorEntry(value)?.value - if (floor != null && value in floor) return floor - //neither is valid, not found - return null - } - +public interface UnivariateHistogram : Histogram, + SpaceElement { + public operator fun get(value: Double): UnivariateBin? public override operator fun get(point: Buffer): UnivariateBin? = get(point[0]) - public override val dimension: Int get() = 1 - - public override operator fun iterator(): Iterator = bins.values.iterator() - public companion object { - /** - * Build a histogram with a uniform binning with a start at [start] and a bin size of [binSize] - */ - public fun uniformBuilder(binSize: Double, start: Double = 0.0): UnivariateHistogramBuilder = - UnivariateHistogramSpace { value -> - val center = start + binSize * floor((value - start) / binSize + 0.5) - UnivariateBin(center, binSize) - }.builder() - /** * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. */ @@ -78,35 +54,7 @@ public abstract class UnivariateHistogram protected constructor( binSize: Double, start: Double = 0.0, builder: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = uniformBuilder(binSize, start).apply(builder) - - /** - * Create a histogram with custom cell borders - */ - public fun customBuilder(borders: DoubleArray): UnivariateHistogramBuilder { - val sorted = borders.sortedArray() - - return UnivariateHistogramSpace { value -> - when { - value < sorted.first() -> UnivariateBin( - Double.NEGATIVE_INFINITY, - Double.MAX_VALUE - ) - - value > sorted.last() -> UnivariateBin( - Double.POSITIVE_INFINITY, - Double.MAX_VALUE - ) - - else -> { - val index = sorted.indices.first { value > sorted[it] } - val left = sorted[index] - val right = sorted[index + 1] - UnivariateBin((left + right) / 2, (right - left)) - } - } - }.builder() - } + ): UnivariateHistogram = UnivariateHistogramSpace.uniform(binSize, start).produce(builder) /** * Build and fill a histogram with custom borders. Returns a read-only histogram. @@ -114,41 +62,24 @@ public abstract class UnivariateHistogram protected constructor( public fun custom( borders: DoubleArray, builder: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = customBuilder(borders).apply(builder) + ): UnivariateHistogram = UnivariateHistogramSpace.custom(borders).produce(builder) + } } -public class UnivariateHistogramBuilder internal constructor( - override val context: UnivariateHistogramSpace, -) : UnivariateHistogram(), MutableHistogram { - - private fun createBin(value: Double): UnivariateBin = context.binFactory(value).also { - synchronized(this) { bins[it.position] = it } - } - +public interface UnivariateHistogramBuilder { /** * Thread safe put operation */ - public fun put(value: Double, weight: Double = 1.0) { - (get(value) ?: createBin(value)).apply { - counter.increment() - weightCounter.add(weight) - } - } - - override fun putWithWeight(point: Buffer, weight: Double) { - put(point[0], weight) - } + public fun put(value: Double, weight: Double = 1.0) + public fun putWithWeight(point: Buffer, weight: Double) /** * Put several items into a single bin */ - public fun putMany(value: Double, count: Int, weight: Double = count.toDouble()) { - (get(value) ?: createBin(value)).apply { - counter.add(count.toLong()) - weightCounter.add(weight) - } - } + public fun putMany(value: Double, count: Int, weight: Double = count.toDouble()) + + public fun build(): UnivariateHistogram } @UnstableKMathAPI diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt index 0deeb0a97..b18835b7b 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt @@ -1,25 +1,185 @@ package kscience.kmath.histogram import kscience.kmath.operations.Space +import kscience.kmath.structures.Buffer +import java.util.* +import kotlin.math.abs +import kotlin.math.sqrt -public class UnivariateHistogramSpace(public val binFactory: (Double) -> UnivariateBin) : Space { +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 fun builder(): UnivariateHistogramBuilder = UnivariateHistogramBuilder(this) - public fun produce(builder: UnivariateHistogramBuilder.() -> Unit): UnivariateHistogram = builder().apply(builder) +private class UnivariateHistogramImpl( + override val context: UnivariateHistogramSpace, + val binMap: TreeMap, +) : UnivariateHistogram { + override fun get(value: Double): UnivariateBin? = binMap.getBin(value) + override val dimension: Int get() = 1 + override val bins: Collection get() = binMap.values +} + +private class UnivariateBinCounter( + override val def: UnivariateHistogramBinDefinition, +) : UnivariateBin { + val counter: LongCounter = LongCounter() + val valueCounter: DoubleCounter = DoubleCounter() + + /** + * The precise number of events ignoring weighting + */ + val count: Long get() = counter.sum() + + override val standardDeviation: Double get() = sqrt(count.toDouble()) / count * value + + /** + * The value of histogram including weighting + */ + override val value: Double get() = valueCounter.sum() + + public fun increment(count: Long, value: Double) { + counter.add(count) + valueCounter.add(value) + } +} + +private class UnivariateBinValue( + override val def: UnivariateHistogramBinDefinition, + override val value: Double, + override val standardDeviation: Double, +) : UnivariateBin + + +public class UnivariateHistogramSpace( + public val binFactory: (Double) -> UnivariateHistogramBinDefinition, +) : Space { + + private inner class UnivariateHistogramBuilderImpl : UnivariateHistogramBuilder { + + val bins: TreeMap = TreeMap() + fun get(value: Double): UnivariateBinCounter? = bins.getBin(value) + + private fun createBin(value: Double): UnivariateBinCounter { + val binDefinition = binFactory(value) + val newBin = UnivariateBinCounter(binDefinition) + synchronized(this) { bins[binDefinition.position] = newBin } + return newBin + } + + /** + * Thread safe put operation + */ + override fun put(value: Double, weight: Double) { + (get(value) ?: createBin(value)).apply { + increment(1, weight) + } + } + + override fun putWithWeight(point: Buffer, weight: Double) { + put(point[0], weight) + } + + /** + * Put several items into a single bin + */ + override fun putMany(value: Double, count: Int, weight: Double) { + (get(value) ?: createBin(value)).apply { + increment(count.toLong(), weight) + } + } + + override fun build(): UnivariateHistogram = UnivariateHistogramImpl(this@UnivariateHistogramSpace, bins) + } + + + public fun builder(): UnivariateHistogramBuilder = UnivariateHistogramBuilderImpl() + + public fun produce(builder: UnivariateHistogramBuilder.() -> Unit): UnivariateHistogram = + UnivariateHistogramBuilderImpl().apply(builder).build() override fun add( a: UnivariateHistogram, b: UnivariateHistogram, ): UnivariateHistogram { - require(a.context == this){"Histogram $a does not belong to this context"} - require(b.context == this){"Histogram $b does not belong to this context"} - TODO() + 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 { + (a.bins.map { it.def } union b.bins.map { it.def }).forEach { def -> + val newBin = UnivariateBinValue( + def, + value = (a[def.position]?.value ?: 0.0) + (b[def.position]?.value ?: 0.0), + standardDeviation = (a[def.position]?.standardDeviation + ?: 0.0) + (b[def.position]?.standardDeviation ?: 0.0) + ) + } + } + return UnivariateHistogramImpl(this, bins) } override fun multiply(a: UnivariateHistogram, k: Number): UnivariateHistogram { - TODO("Not yet implemented") + val bins = TreeMap().apply { + a.bins.forEach { bin -> + put(bin.position, + UnivariateBinValue( + bin.def, + value = bin.value * k.toDouble(), + standardDeviation = abs(bin.standardDeviation * k.toDouble()) + ) + ) + } + } + + return UnivariateHistogramImpl(this, bins) } - override val zero: UnivariateHistogram = produce { } + override val zero: UnivariateHistogram = produce { } + + public companion object { + /** + * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. + */ + public fun uniform( + binSize: Double, + start: Double = 0.0 + ): UnivariateHistogramSpace = UnivariateHistogramSpace { value -> + val center = start + binSize * Math.floor((value - start) / binSize + 0.5) + UnivariateHistogramBinDefinition(center, binSize) + } + + /** + * Create a histogram with custom cell borders + */ + public fun custom(borders: DoubleArray): UnivariateHistogramSpace { + val sorted = borders.sortedArray() + + return UnivariateHistogramSpace { value -> + when { + value < sorted.first() -> UnivariateHistogramBinDefinition( + Double.NEGATIVE_INFINITY, + Double.MAX_VALUE + ) + + value > sorted.last() -> UnivariateHistogramBinDefinition( + Double.POSITIVE_INFINITY, + Double.MAX_VALUE + ) + + else -> { + val index = sorted.indices.first { value > sorted[it] } + val left = sorted[index] + val right = sorted[index + 1] + UnivariateHistogramBinDefinition((left + right) / 2, (right - left)) + } + } + } + } + } } \ No newline at end of file From 987997cc02b4e8c73033b437cfbef1adcd6f3334 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 12 Feb 2021 22:43:35 +0300 Subject: [PATCH 2/4] Histograms refactor --- CHANGELOG.md | 1 + examples/build.gradle.kts | 8 + .../kscience/kmath/ast/MstExpression.kt | 13 +- kmath-core/api/kmath-core.api | 2 +- .../kmath/domains/HyperSquareDomain.kt | 11 +- .../kotlin/kscience/kmath/nd/NDStructure.kt | 15 +- .../kscience/kmath/structures/Buffer.kt | 2 +- .../kscience/kmath/histogram/Counters.kt | 8 +- .../kscience/kmath/histogram/Histogram.kt | 21 ++- .../kmath/histogram/IndexedHistogramSpace.kt | 76 ++++++++ .../kscience/kmath/histogram/RealHistogram.kt | 166 ------------------ .../kmath/histogram/RealHistogramSpace.kt | 121 +++++++++++++ .../histogram/MultivariateHistogramTest.kt | 19 +- .../kmath/histogram/UnivariateHistogram.kt | 10 +- .../histogram/UnivariateHistogramSpace.kt | 10 +- 15 files changed, 267 insertions(+), 216 deletions(-) create mode 100644 kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/IndexedHistogramSpace.kt delete mode 100644 kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt create mode 100644 kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2396c6b27..f4d279362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Refactor histograms. They are marked as prototype - `Complex` and related features moved to a separate module `kmath-complex` - Refactor AlgebraElement +- Add `out` projection to `Buffer` generic ### Deprecated diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 054945eb7..5439d46dc 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -76,6 +76,14 @@ benchmark { iterationTimeUnit = "ms" // time unity for iterationTime, default is seconds include("DotBenchmark") } + + configurations.register("expressions") { + warmups = 1 // number of warmup iterations + iterations = 3 // number of iterations + iterationTime = 500 // time in seconds per iteration + iterationTimeUnit = "ms" // time unity for iterationTime, default is seconds + include("ExpressionsInterpretersBenchmark") + } } kotlin.sourceSets.all { diff --git a/kmath-ast/src/commonMain/kotlin/kscience/kmath/ast/MstExpression.kt b/kmath-ast/src/commonMain/kotlin/kscience/kmath/ast/MstExpression.kt index 03d33aa2b..8da96f770 100644 --- a/kmath-ast/src/commonMain/kotlin/kscience/kmath/ast/MstExpression.kt +++ b/kmath-ast/src/commonMain/kotlin/kscience/kmath/ast/MstExpression.kt @@ -21,8 +21,17 @@ public class MstExpression>(public val algebra: A, public null } ?: arguments.getValue(StringSymbol(value)) - override fun unaryOperationFunction(operation: String): (arg: T) -> T = algebra.unaryOperationFunction(operation) - override fun binaryOperationFunction(operation: String): (left: T, right: T) -> T = algebra.binaryOperationFunction(operation) + override fun unaryOperation(operation: String, arg: T): T = + algebra.unaryOperation(operation, arg) + + override fun binaryOperation(operation: String, left: T, right: T): T = + algebra.binaryOperation(operation, left, right) + + override fun unaryOperationFunction(operation: String): (arg: T) -> T = + algebra.unaryOperationFunction(operation) + + override fun binaryOperationFunction(operation: String): (left: T, right: T) -> T = + algebra.binaryOperationFunction(operation) @Suppress("UNCHECKED_CAST") override fun number(value: Number): T = if (algebra is NumericAlgebra<*>) diff --git a/kmath-core/api/kmath-core.api b/kmath-core/api/kmath-core.api index e58e5d45d..1bfc13c3b 100644 --- a/kmath-core/api/kmath-core.api +++ b/kmath-core/api/kmath-core.api @@ -4,7 +4,7 @@ public abstract interface class kscience/kmath/domains/Domain { } public final class kscience/kmath/domains/HyperSquareDomain : kscience/kmath/domains/RealDomain { - public synthetic fun ([D[DLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkscience/kmath/structures/Buffer;Lkscience/kmath/structures/Buffer;)V public fun contains (Lkscience/kmath/structures/Buffer;)Z public fun getDimension ()I public fun getLowerBound (I)Ljava/lang/Double; diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/domains/HyperSquareDomain.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/domains/HyperSquareDomain.kt index b45cf6bf5..20f19864f 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/domains/HyperSquareDomain.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/domains/HyperSquareDomain.kt @@ -16,6 +16,7 @@ package kscience.kmath.domains import kscience.kmath.linear.Point +import kscience.kmath.structures.Buffer import kscience.kmath.structures.RealBuffer import kscience.kmath.structures.indices @@ -25,20 +26,20 @@ import kscience.kmath.structures.indices * * @author Alexander Nozik */ -public class HyperSquareDomain(private val lower: RealBuffer, private val upper: RealBuffer) : RealDomain { +public class HyperSquareDomain(private val lower: Buffer, private val upper: Buffer) : RealDomain { public override val dimension: Int get() = lower.size public override operator fun contains(point: Point): Boolean = point.indices.all { i -> point[i] in lower[i]..upper[i] } - public override fun getLowerBound(num: Int, point: Point): Double? = lower[num] + public override fun getLowerBound(num: Int, point: Point): Double = lower[num] - public override fun getLowerBound(num: Int): Double? = lower[num] + public override fun getLowerBound(num: Int): Double = lower[num] - public override fun getUpperBound(num: Int, point: Point): Double? = upper[num] + public override fun getUpperBound(num: Int, point: Point): Double = upper[num] - public override fun getUpperBound(num: Int): Double? = upper[num] + public override fun getUpperBound(num: Int): Double = upper[num] public override fun nearestInDomain(point: Point): Point { val res = DoubleArray(point.size) { i -> diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/nd/NDStructure.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/nd/NDStructure.kt index 4aa1c7d52..c3de02925 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/nd/NDStructure.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/nd/NDStructure.kt @@ -78,8 +78,7 @@ public interface NDStructure { strides: Strides, bufferFactory: BufferFactory = Buffer.Companion::boxing, initializer: (IntArray) -> T, - ): NDBuffer = - NDBuffer(strides, bufferFactory(strides.linearSize) { i -> initializer(strides.index(i)) }) + ): NDBuffer = NDBuffer(strides, bufferFactory(strides.linearSize) { i -> initializer(strides.index(i)) }) /** * Inline create NDStructure with non-boxing buffer implementation if it is possible @@ -87,15 +86,13 @@ public interface NDStructure { public inline fun auto( strides: Strides, crossinline initializer: (IntArray) -> T, - ): NDBuffer = - NDBuffer(strides, Buffer.auto(strides.linearSize) { i -> initializer(strides.index(i)) }) + ): NDBuffer = NDBuffer(strides, Buffer.auto(strides.linearSize) { i -> initializer(strides.index(i)) }) public inline fun auto( type: KClass, strides: Strides, crossinline initializer: (IntArray) -> T, - ): NDBuffer = - NDBuffer(strides, Buffer.auto(type, strides.linearSize) { i -> initializer(strides.index(i)) }) + ): NDBuffer = NDBuffer(strides, Buffer.auto(type, strides.linearSize) { i -> initializer(strides.index(i)) }) public fun build( shape: IntArray, @@ -106,8 +103,7 @@ public interface NDStructure { public inline fun auto( shape: IntArray, crossinline initializer: (IntArray) -> T, - ): NDBuffer = - auto(DefaultStrides(shape), initializer) + ): NDBuffer = auto(DefaultStrides(shape), initializer) @JvmName("autoVarArg") public inline fun auto( @@ -120,8 +116,7 @@ public interface NDStructure { type: KClass, vararg shape: Int, crossinline initializer: (IntArray) -> T, - ): NDBuffer = - auto(type, DefaultStrides(shape), initializer) + ): NDBuffer = auto(type, DefaultStrides(shape), initializer) } } diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/structures/Buffer.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/structures/Buffer.kt index be48055c7..d214eeba1 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/structures/Buffer.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/structures/Buffer.kt @@ -21,7 +21,7 @@ public typealias MutableBufferFactory = (Int, (Int) -> T) -> MutableBuffer * * @param T the type of elements contained in the buffer. */ -public interface Buffer { +public interface Buffer { /** * The size of this buffer. */ diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt index a7a42873f..bdc03cc1d 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Counters.kt @@ -2,16 +2,18 @@ package kscience.kmath.histogram import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate +import kscience.kmath.operations.RealField import kscience.kmath.operations.Space -/* +/** * Common representation for atomic counters - * TODO replace with atomics */ - public interface Counter { public fun add(delta: T) public val value: T + public companion object{ + public fun real(): ObjectCounter = ObjectCounter(RealField) + } } public class IntCounter : Counter { diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt index 2bc40af85..91032fc31 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/Histogram.kt @@ -13,8 +13,6 @@ public interface Bin : Domain { * The value of this bin. */ public val value: Number - - public val center: Point } public interface Histogram> { @@ -28,29 +26,30 @@ public interface Histogram> { */ public val dimension: Int - public val bins: Collection + public val bins: Iterable } -public interface HistogramBuilder> : Histogram { +public fun interface HistogramBuilder { /** * Increment appropriate bin */ - public fun putWithWeight(point: Point, weight: Double) + public fun putValue(point: Point, value: Number) - public fun put(point: Point): Unit = putWithWeight(point, 1.0) } -public fun HistogramBuilder.put(vararg point: T): Unit = put(ArrayBuffer(point)) +public fun > HistogramBuilder.put(point: Point): Unit = putValue(point, 1.0) -public fun HistogramBuilder.put(vararg point: Number): Unit = +public fun HistogramBuilder.put(vararg point: T): Unit = put(ArrayBuffer(point)) + +public fun HistogramBuilder.put(vararg point: Number): Unit = put(RealBuffer(point.map { it.toDouble() }.toDoubleArray())) -public fun HistogramBuilder.put(vararg point: Double): Unit = put(RealBuffer(point)) -public fun HistogramBuilder.fill(sequence: Iterable>): Unit = sequence.forEach { put(it) } +public fun HistogramBuilder.put(vararg point: Double): Unit = put(RealBuffer(point)) +public fun HistogramBuilder.fill(sequence: Iterable>): Unit = sequence.forEach { put(it) } /** * Pass a sequence builder into histogram */ -public fun HistogramBuilder.fill(block: suspend SequenceScope>.() -> Unit): Unit = +public fun HistogramBuilder.fill(block: suspend SequenceScope>.() -> Unit): Unit = fill(sequence(block).asIterable()) diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/IndexedHistogramSpace.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/IndexedHistogramSpace.kt new file mode 100644 index 000000000..53761452f --- /dev/null +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/IndexedHistogramSpace.kt @@ -0,0 +1,76 @@ +package kscience.kmath.histogram + +import kscience.kmath.domains.Domain +import kscience.kmath.linear.Point +import kscience.kmath.misc.UnstableKMathAPI +import kscience.kmath.nd.NDSpace +import kscience.kmath.nd.NDStructure +import kscience.kmath.nd.Strides +import kscience.kmath.operations.Space +import kscience.kmath.operations.SpaceElement +import kscience.kmath.operations.invoke + +/** + * A simple histogram bin based on domain + */ +public data class DomainBin>( + public val domain: Domain, + public override val value: Number, +) : Bin, Domain by domain + +@OptIn(UnstableKMathAPI::class) +public class IndexedHistogram, V : Any>( + override val context: IndexedHistogramSpace, + public val values: NDStructure, +) : Histogram>, SpaceElement, IndexedHistogramSpace> { + + override fun get(point: Point): Bin? { + val index = context.getIndex(point) ?: return null + return context.produceBin(index, values[index]) + } + + override val dimension: Int get() = context.strides.shape.size + + override val bins: Iterable> + get() = context.strides.indices().map { + context.produceBin(it, values[it]) + }.asIterable() + +} + +/** + * A space for producing histograms with values in a NDStructure + */ +public interface IndexedHistogramSpace, V : Any> : Space> { + //public val valueSpace: Space + public val strides: Strides + public val histogramValueSpace: NDSpace //= NDAlgebra.space(valueSpace, Buffer.Companion::boxing, *shape), + + /** + * Resolve index of the bin including given [point] + */ + public fun getIndex(point: Point): IntArray? + + /** + * Get a bin domain represented by given index + */ + public fun getDomain(index: IntArray): Domain? + + public fun produceBin(index: IntArray, value: V): Bin + + public fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram + + override fun add(a: IndexedHistogram, b: IndexedHistogram): IndexedHistogram { + require(a.context == this) { "Can't operate on a histogram produced by external space" } + require(b.context == this) { "Can't operate on a histogram produced by external space" } + return IndexedHistogram(this, histogramValueSpace.invoke { a.values + b.values }) + } + + override fun multiply(a: IndexedHistogram, k: Number): IndexedHistogram { + require(a.context == this) { "Can't operate on a histogram produced by external space" } + return IndexedHistogram(this, histogramValueSpace.invoke { a.values * k }) + } + + override val zero: IndexedHistogram get() = produce { } +} + diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt deleted file mode 100644 index c8f9958e0..000000000 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogram.kt +++ /dev/null @@ -1,166 +0,0 @@ -package kscience.kmath.histogram - -import kscience.kmath.linear.Point -import kscience.kmath.nd.DefaultStrides -import kscience.kmath.nd.NDStructure -import kscience.kmath.operations.SpaceOperations -import kscience.kmath.operations.invoke -import kscience.kmath.structures.* -import kotlin.math.floor - -public data class MultivariateBinDefinition>( - public val space: SpaceOperations>, - public val center: Point, - public val sizes: Point, -) { - public fun contains(vector: Point): Boolean { - require(vector.size == center.size) { "Dimension mismatch for input vector. Expected ${center.size}, but found ${vector.size}" } - val upper = space { center + sizes / 2.0 } - val lower = space { center - sizes / 2.0 } - return vector.asSequence().mapIndexed { i, value -> value in lower[i]..upper[i] }.all { it } - } -} - - -public class MultivariateBin>( - public val definition: MultivariateBinDefinition, - public val count: Long, - public override val value: Double, -) : Bin { - public override val dimension: Int - get() = definition.center.size - - public override val center: Point - get() = definition.center - - public override operator fun contains(point: Point): Boolean = definition.contains(point) -} - -/** - * Uniform multivariate histogram with fixed borders. Based on NDStructure implementation with complexity of m for bin search, where m is the number of dimensions. - */ -public class RealHistogram( - private val lower: Buffer, - private val upper: Buffer, - private val binNums: IntArray = IntArray(lower.size) { 20 }, -) : HistogramBuilder> { - private val strides = DefaultStrides(IntArray(binNums.size) { binNums[it] + 2 }) - private val counts: NDStructure = NDStructure.auto(strides) { LongCounter() } - private val values: NDStructure = NDStructure.auto(strides) { DoubleCounter() } - public override val dimension: Int get() = lower.size - private val binSize = RealBuffer(dimension) { (upper[it] - lower[it]) / binNums[it] } - - init { - // argument checks - require(lower.size == upper.size) { "Dimension mismatch in histogram lower and upper limits." } - require(lower.size == binNums.size) { "Dimension mismatch in bin count." } - require(!(0 until dimension).any { upper[it] - lower[it] < 0 }) { "Range for one of axis is not strictly positive" } - } - - /** - * Get internal [NDStructure] bin index for given axis - */ - private fun getIndex(axis: Int, value: Double): Int = when { - value >= upper[axis] -> binNums[axis] + 1 // overflow - value < lower[axis] -> 0 // underflow - else -> floor((value - lower[axis]) / binSize[axis]).toInt() + 1 - } - - private fun getIndex(point: Buffer): IntArray = IntArray(dimension) { getIndex(it, point[it]) } - - private fun getCount(index: IntArray): Long = counts[index].sum() - - public fun getCount(point: Buffer): Long = getCount(getIndex(point)) - - private fun getValue(index: IntArray): Double = values[index].sum() - - public fun getValue(point: Buffer): Double = getValue(getIndex(point)) - - private fun getBinDefinition(index: IntArray): MultivariateBinDefinition { - val center = index.mapIndexed { axis, i -> - when (i) { - 0 -> Double.NEGATIVE_INFINITY - strides.shape[axis] - 1 -> Double.POSITIVE_INFINITY - else -> lower[axis] + (i.toDouble() - 0.5) * binSize[axis] - } - }.asBuffer() - - return MultivariateBinDefinition(RealBufferFieldOperations, center, binSize) - } - - public fun getBinDefinition(point: Buffer): MultivariateBinDefinition = - getBinDefinition(getIndex(point)) - - public override operator fun get(point: Buffer): MultivariateBin? { - val index = getIndex(point) - return MultivariateBin(getBinDefinition(index), getCount(index), getValue(index)) - } - -// fun put(point: Point){ -// val index = getIndex(point) -// values[index].increment() -// } - - public override fun putWithWeight(point: Buffer, weight: Double) { - val index = getIndex(point) - counts[index].increment() - values[index].add(weight) - } - - override val bins: Collection> - get() = strides.indices().map { index -> - MultivariateBin(getBinDefinition(index), counts[index].sum(), values[index].sum()) - }.toList() - - /** - * NDStructure containing number of events in bins without weights - */ - public fun counts(): NDStructure = NDStructure.auto(counts.shape) { counts[it].sum() } - - /** - * NDStructure containing values of bins including weights - */ - public fun values(): NDStructure = NDStructure.auto(values.shape) { values[it].sum() } - - public companion object { - /** - * Use it like - * ``` - *FastHistogram.fromRanges( - * (-1.0..1.0), - * (-1.0..1.0) - *) - *``` - */ - public fun fromRanges(vararg ranges: ClosedFloatingPointRange): RealHistogram = RealHistogram( - ranges.map(ClosedFloatingPointRange::start).asBuffer(), - ranges.map(ClosedFloatingPointRange::endInclusive).asBuffer() - ) - - /** - * Use it like - * ``` - *FastHistogram.fromRanges( - * (-1.0..1.0) to 50, - * (-1.0..1.0) to 32 - *) - *``` - */ - public fun fromRanges(vararg ranges: Pair, Int>): RealHistogram = - RealHistogram( - ListBuffer( - ranges - .map(Pair, Int>::first) - .map(ClosedFloatingPointRange::start) - ), - - ListBuffer( - ranges - .map(Pair, Int>::first) - .map(ClosedFloatingPointRange::endInclusive) - ), - - ranges.map(Pair, Int>::second).toIntArray() - ) - } -} diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt new file mode 100644 index 000000000..ef2d9771b --- /dev/null +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt @@ -0,0 +1,121 @@ +package kscience.kmath.histogram + +import kscience.kmath.domains.Domain +import kscience.kmath.domains.HyperSquareDomain +import kscience.kmath.nd.* +import kscience.kmath.structures.* +import kotlin.math.floor + +public class RealHistogramSpace( + private val lower: Buffer, + private val upper: Buffer, + private val binNums: IntArray = IntArray(lower.size) { 20 }, +) : IndexedHistogramSpace { + + init { + // argument checks + require(lower.size == upper.size) { "Dimension mismatch in histogram lower and upper limits." } + require(lower.size == binNums.size) { "Dimension mismatch in bin count." } + require(!lower.indices.any { upper[it] - lower[it] < 0 }) { "Range for one of axis is not strictly positive" } + } + + public val dimension: Int get() = lower.size + + private val shape = IntArray(binNums.size) { binNums[it] + 2 } + override val histogramValueSpace: RealNDField = NDAlgebra.real(*shape) + + override val strides: Strides get() = histogramValueSpace.strides + private val binSize = RealBuffer(dimension) { (upper[it] - lower[it]) / binNums[it] } + + /** + * Get internal [NDStructure] bin index for given axis + */ + private fun getIndex(axis: Int, value: Double): Int = when { + value >= upper[axis] -> binNums[axis] + 1 // overflow + value < lower[axis] -> 0 // underflow + else -> floor((value - lower[axis]) / binSize[axis]).toInt() + } + + override fun getIndex(point: Buffer): IntArray = IntArray(dimension) { + getIndex(it, point[it]) + } + + override fun getDomain(index: IntArray): Domain { + val lowerBoundary = index.mapIndexed { axis, i -> + when (i) { + 0 -> Double.NEGATIVE_INFINITY + strides.shape[axis] - 1 -> upper[axis] + else -> lower[axis] + (i.toDouble()) * binSize[axis] + } + }.asBuffer() + + val upperBoundary = index.mapIndexed { axis, i -> + when (i) { + 0 -> lower[axis] + strides.shape[axis] - 1 -> Double.POSITIVE_INFINITY + else -> lower[axis] + (i.toDouble() + 1) * binSize[axis] + } + }.asBuffer() + + return HyperSquareDomain(lowerBoundary, upperBoundary) + } + + + override fun produceBin(index: IntArray, value: Double): Bin { + val domain = getDomain(index) + return DomainBin(domain, value) + } + + override fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram { + val ndCounter = NDStructure.auto(strides) { Counter.real() } + val hBuilder = HistogramBuilder { point, value -> + val index = getIndex(point) + ndCounter[index].add(1.0) + } + hBuilder.apply(builder) + val values: NDBuffer = ndCounter.mapToBuffer { it.value } + return IndexedHistogram(this, values) + } + + public companion object{ + /** + * Use it like + * ``` + *FastHistogram.fromRanges( + * (-1.0..1.0), + * (-1.0..1.0) + *) + *``` + */ + public fun fromRanges(vararg ranges: ClosedFloatingPointRange): RealHistogramSpace = RealHistogramSpace( + ranges.map(ClosedFloatingPointRange::start).asBuffer(), + ranges.map(ClosedFloatingPointRange::endInclusive).asBuffer() + ) + + /** + * Use it like + * ``` + *FastHistogram.fromRanges( + * (-1.0..1.0) to 50, + * (-1.0..1.0) to 32 + *) + *``` + */ + public fun fromRanges(vararg ranges: Pair, Int>): RealHistogramSpace = + RealHistogramSpace( + ListBuffer( + ranges + .map(Pair, Int>::first) + .map(ClosedFloatingPointRange::start) + ), + + ListBuffer( + ranges + .map(Pair, Int>::first) + .map(ClosedFloatingPointRange::endInclusive) + ), + + ranges.map(Pair, Int>::second).toIntArray() + ) + } +} \ No newline at end of file diff --git a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt index ef2c1445a..860b047ae 100644 --- a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt +++ b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt @@ -1,6 +1,6 @@ package scietifik.kmath.histogram -import kscience.kmath.histogram.RealHistogram +import kscience.kmath.histogram.RealHistogramSpace import kscience.kmath.histogram.fill import kscience.kmath.histogram.put import kscience.kmath.real.RealVector @@ -11,11 +11,13 @@ import kotlin.test.* internal class MultivariateHistogramTest { @Test fun testSinglePutHistogram() { - val histogram = RealHistogram.fromRanges( + val hSpace = RealHistogramSpace.fromRanges( (-1.0..1.0), (-1.0..1.0) ) - histogram.put(0.55, 0.55) + val histogram = hSpace.produce { + put(0.55, 0.55) + } val bin = histogram.bins.find { it.value.toInt() > 0 } ?: fail() assertTrue { bin.contains(RealVector(0.55, 0.55)) } assertTrue { bin.contains(RealVector(0.6, 0.5)) } @@ -24,7 +26,7 @@ internal class MultivariateHistogramTest { @Test fun testSequentialPut() { - val histogram = RealHistogram.fromRanges( + val hSpace = RealHistogramSpace.fromRanges( (-1.0..1.0), (-1.0..1.0), (-1.0..1.0) @@ -34,10 +36,11 @@ internal class MultivariateHistogramTest { fun nextDouble() = random.nextDouble(-1.0, 1.0) val n = 10000 - - histogram.fill { - repeat(n) { - yield(RealVector(nextDouble(), nextDouble(), nextDouble())) + val histogram = hSpace.produce { + fill { + repeat(n) { + yield(RealVector(nextDouble(), nextDouble(), nextDouble())) + } } } assertEquals(n, histogram.bins.sumBy { it.value.toInt() }) diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt index 1fb5172ce..7d8f874a2 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt @@ -30,7 +30,7 @@ public interface UnivariateBin : Bin { */ public val standardDeviation: Double - public override val center: Point get() = doubleArrayOf(position).asBuffer() + public val center: Point get() = doubleArrayOf(position).asBuffer() public override val dimension: Int get() = 1 @@ -44,7 +44,7 @@ public operator fun UnivariateBin.contains(value: Double): Boolean = public interface UnivariateHistogram : Histogram, SpaceElement { public operator fun get(value: Double): UnivariateBin? - public override operator fun get(point: Buffer): UnivariateBin? = get(point[0]) + public override operator fun get(point: Buffer): UnivariateBin? = get(point[0]) public companion object { /** @@ -67,12 +67,14 @@ public interface UnivariateHistogram : Histogram, } } -public interface UnivariateHistogramBuilder { +public interface UnivariateHistogramBuilder: HistogramBuilder { + /** * Thread safe put operation */ public fun put(value: Double, weight: Double = 1.0) - public fun putWithWeight(point: Buffer, weight: Double) + + override fun putValue(point: Buffer, value: Number) /** * Put several items into a single bin diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt index b18835b7b..3d1bd20c0 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt @@ -31,19 +31,19 @@ private class UnivariateBinCounter( override val def: UnivariateHistogramBinDefinition, ) : UnivariateBin { val counter: LongCounter = LongCounter() - val valueCounter: DoubleCounter = DoubleCounter() + val valueCounter: ObjectCounter = Counter.real() /** * The precise number of events ignoring weighting */ - val count: Long get() = counter.sum() + val count: Long get() = counter.value override val standardDeviation: Double get() = sqrt(count.toDouble()) / count * value /** * The value of histogram including weighting */ - override val value: Double get() = valueCounter.sum() + override val value: Double get() = valueCounter.value public fun increment(count: Long, value: Double) { counter.add(count) @@ -83,8 +83,8 @@ public class UnivariateHistogramSpace( } } - override fun putWithWeight(point: Buffer, weight: Double) { - put(point[0], weight) + override fun putValue(point: Buffer, value: Number) { + put(point[0], value.toDouble()) } /** From ce18e85a0afebc1ab105a7e02129897e0595d0e0 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 14 Feb 2021 22:44:05 +0300 Subject: [PATCH 3/4] Histograms refactor --- .../kmath/histogram/RealHistogramSpace.kt | 2 +- .../histogram/MultivariateHistogramTest.kt | 42 +++- .../histogram/AbstractUnivariateHistogram.kt | 33 ---- .../kmath/histogram/TreeHistogramSpace.kt | 156 +++++++++++++++ .../kmath/histogram/UnivariateHistogram.kt | 52 ++--- .../histogram/UnivariateHistogramSpace.kt | 185 ------------------ 6 files changed, 211 insertions(+), 259 deletions(-) delete mode 100644 kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt create mode 100644 kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt delete mode 100644 kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt diff --git a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt index ef2d9771b..c9beb3a73 100644 --- a/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt +++ b/kmath-histograms/src/commonMain/kotlin/kscience/kmath/histogram/RealHistogramSpace.kt @@ -77,7 +77,7 @@ public class RealHistogramSpace( return IndexedHistogram(this, values) } - public companion object{ + public companion object { /** * Use it like * ``` diff --git a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt index 860b047ae..44b9410d2 100644 --- a/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt +++ b/kmath-histograms/src/commonTest/kotlin/scietifik/kmath/histogram/MultivariateHistogramTest.kt @@ -1,8 +1,8 @@ package scietifik.kmath.histogram import kscience.kmath.histogram.RealHistogramSpace -import kscience.kmath.histogram.fill import kscience.kmath.histogram.put +import kscience.kmath.operations.invoke import kscience.kmath.real.RealVector import kscience.kmath.real.invoke import kotlin.random.Random @@ -37,12 +37,44 @@ internal class MultivariateHistogramTest { val n = 10000 val histogram = hSpace.produce { - fill { - repeat(n) { - yield(RealVector(nextDouble(), nextDouble(), nextDouble())) - } + repeat(n) { + put(nextDouble(), nextDouble(), nextDouble()) } } assertEquals(n, histogram.bins.sumBy { it.value.toInt() }) } + + @Test + fun testHistogramAlgebra() { + val hSpace = RealHistogramSpace.fromRanges( + (-1.0..1.0), + (-1.0..1.0), + (-1.0..1.0) + ).invoke { + val random = Random(1234) + + fun nextDouble() = random.nextDouble(-1.0, 1.0) + val n = 10000 + val histogram1 = produce { + repeat(n) { + put(nextDouble(), nextDouble(), nextDouble()) + } + } + val histogram2 = produce { + repeat(n) { + put(nextDouble(), nextDouble(), nextDouble()) + } + } + val res = histogram1 - histogram2 + assertTrue { + strides.indices().all { index -> + res.values[index] <= histogram1.values[index] + } + } + assertTrue { + res.bins.count() >= histogram1.bins.count() + } + assertEquals(0.0, res.bins.sumByDouble { it.value.toDouble() }) + } + } } \ No newline at end of file diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt deleted file mode 100644 index efc6e92de..000000000 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/AbstractUnivariateHistogram.kt +++ /dev/null @@ -1,33 +0,0 @@ -package kscience.kmath.histogram - - -/** - * Univariate histogram with log(n) bin search speed - */ -//private abstract class AbstractUnivariateHistogram{ -// -// public abstract val bins: TreeMap -// -// public open operator fun get(value: Double): B? { -// // check ceiling entry and return it if it is what needed -// val ceil = bins.ceilingEntry(value)?.value -// if (ceil != null && value in ceil) return ceil -// //check floor entry -// val floor = bins.floorEntry(value)?.value -// if (floor != null && value in floor) return floor -// //neither is valid, not found -// return null -// } - -// public override operator fun get(point: Buffer): B? = get(point[0]) -// -// public override val dimension: Int get() = 1 -// -// public override operator fun iterator(): Iterator = bins.values.iterator() -// -// public companion object { - -// } -//} - - diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt new file mode 100644 index 000000000..b5872c601 --- /dev/null +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt @@ -0,0 +1,156 @@ +package kscience.kmath.histogram + +import kscience.kmath.domains.UnivariateDomain +import kscience.kmath.misc.UnstableKMathAPI +import kscience.kmath.operations.Space +import kscience.kmath.structures.Buffer +import java.util.* +import kotlin.math.abs +import kotlin.math.sqrt + +internal 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 +} + +@UnstableKMathAPI +public class TreeHistogram( + override val context: TreeHistogramSpace, + private val binMap: TreeMap, +) : UnivariateHistogram { + override fun get(value: Double): UnivariateBin? = binMap.getBin(value) + override val dimension: Int get() = 1 + override val bins: Collection get() = binMap.values +} + +private class UnivariateBinValue( + override val domain: UnivariateDomain, + override val value: Double, + override val standardDeviation: Double, +) : UnivariateBin, ClosedFloatingPointRange by domain.range + +@UnstableKMathAPI +public class TreeHistogramSpace( + public val binFactory: (Double) -> UnivariateDomain, +) : Space { + + private class BinCounter(val domain: UnivariateDomain, val counter: Counter = Counter.real()) : + ClosedFloatingPointRange by domain.range + + public fun produce(builder: UnivariateHistogramBuilder.() -> Unit): UnivariateHistogram { + val bins: TreeMap = TreeMap() + val hBuilder = object : UnivariateHistogramBuilder { + + fun get(value: Double): BinCounter? = bins.getBin(value) + + fun createBin(value: Double): BinCounter { + val binDefinition = 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: Number) { + put(point[0], value.toDouble()) + } + } + hBuilder.apply(builder) + val resBins = TreeMap() + bins.forEach { key, binCounter -> + val count = binCounter.counter.value + resBins[key] = UnivariateBinValue(binCounter.domain, count, sqrt(count)) + } + return TreeHistogram(this, resBins) + } + + override fun add( + a: UnivariateHistogram, + b: UnivariateHistogram, + ): UnivariateHistogram { + 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 { + (a.bins.map { it.domain } union b.bins.map { it.domain }).forEach { def -> + val newBin = UnivariateBinValue( + def, + value = (a[def.center]?.value ?: 0.0) + (b[def.center]?.value ?: 0.0), + standardDeviation = (a[def.center]?.standardDeviation + ?: 0.0) + (b[def.center]?.standardDeviation ?: 0.0) + ) + } + } + return TreeHistogram(this, bins) + } + + override fun multiply(a: UnivariateHistogram, k: Number): UnivariateHistogram { + val bins = TreeMap().apply { + a.bins.forEach { bin -> + put(bin.domain.center, + UnivariateBinValue( + bin.domain, + value = bin.value * k.toDouble(), + standardDeviation = abs(bin.standardDeviation * k.toDouble()) + ) + ) + } + } + + return TreeHistogram(this, bins) + } + + override val zero: UnivariateHistogram = produce { } + + public companion object { + /** + * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. + */ + public fun uniform( + binSize: Double, + start: Double = 0.0, + ): TreeHistogramSpace = TreeHistogramSpace { value -> + val center = start + binSize * Math.floor((value - start) / binSize + 0.5) + UnivariateDomain((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() -> UnivariateDomain( + Double.NEGATIVE_INFINITY..sorted.first() + ) + + value > sorted.last() -> UnivariateDomain( + sorted.last()..Double.POSITIVE_INFINITY + ) + + else -> { + val index = sorted.indices.first { value > sorted[it] } + val left = sorted[index] + val right = sorted[index + 1] + UnivariateDomain(left..right) + } + } + } + } + } +} \ No newline at end of file diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt index 7d8f874a2..87c56cf79 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt @@ -1,24 +1,17 @@ package kscience.kmath.histogram -import kscience.kmath.linear.Point +import kscience.kmath.domains.UnivariateDomain import kscience.kmath.misc.UnstableKMathAPI +import kscience.kmath.operations.Space import kscience.kmath.operations.SpaceElement import kscience.kmath.structures.Buffer -import kscience.kmath.structures.asBuffer import kscience.kmath.structures.asSequence -public data class UnivariateHistogramBinDefinition( - val position: Double, - val size: Double, -) : Comparable { - override fun compareTo(other: UnivariateHistogramBinDefinition): Int = this.position.compareTo(other.position) -} -public interface UnivariateBin : Bin { - public val def: UnivariateHistogramBinDefinition +public val UnivariateDomain.center: Double get() = (range.endInclusive - range.start) / 2 - public val position: Double get() = def.position - public val size: Double get() = def.size +public interface UnivariateBin : Bin, ClosedFloatingPointRange { + public val domain: UnivariateDomain /** * The value of histogram including weighting @@ -30,19 +23,15 @@ public interface UnivariateBin : Bin { */ public val standardDeviation: Double - public val center: Point get() = doubleArrayOf(position).asBuffer() - public override val dimension: Int get() = 1 - public override fun contains(point: Buffer): Boolean = contains(point[0]) + public override fun contains(point: Buffer): Boolean = point.size == 1 && contains(point[0]) + } -public operator fun UnivariateBin.contains(value: Double): Boolean = - value in (position - size / 2)..(position + size / 2) - -@OptIn(UnstableKMathAPI::class) +@UnstableKMathAPI public interface UnivariateHistogram : Histogram, - SpaceElement { + SpaceElement> { public operator fun get(value: Double): UnivariateBin? public override operator fun get(point: Buffer): UnivariateBin? = get(point[0]) @@ -54,7 +43,7 @@ public interface UnivariateHistogram : Histogram, binSize: Double, start: Double = 0.0, builder: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = UnivariateHistogramSpace.uniform(binSize, start).produce(builder) + ): UnivariateHistogram = TreeHistogramSpace.uniform(binSize, start).produce(builder) /** * Build and fill a histogram with custom borders. Returns a read-only histogram. @@ -62,33 +51,26 @@ public interface UnivariateHistogram : Histogram, public fun custom( borders: DoubleArray, builder: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = UnivariateHistogramSpace.custom(borders).produce(builder) + ): UnivariateHistogram = TreeHistogramSpace.custom(borders).produce(builder) } } -public interface UnivariateHistogramBuilder: HistogramBuilder { - +@UnstableKMathAPI +public interface UnivariateHistogramBuilder : HistogramBuilder { /** * Thread safe put operation */ - public fun put(value: Double, weight: Double = 1.0) + public fun putValue(at: Double, value: Double = 1.0) override fun putValue(point: Buffer, value: Number) - - /** - * Put several items into a single bin - */ - public fun putMany(value: Double, count: Int, weight: Double = count.toDouble()) - - public fun build(): UnivariateHistogram } @UnstableKMathAPI -public fun UnivariateHistogramBuilder.fill(items: Iterable): Unit = items.forEach(::put) +public fun UnivariateHistogramBuilder.fill(items: Iterable): Unit = items.forEach(this::putValue) @UnstableKMathAPI -public fun UnivariateHistogramBuilder.fill(array: DoubleArray): Unit = array.forEach(::put) +public fun UnivariateHistogramBuilder.fill(array: DoubleArray): Unit = array.forEach(this::putValue) @UnstableKMathAPI -public fun UnivariateHistogramBuilder.fill(buffer: Buffer): Unit = buffer.asSequence().forEach(::put) \ No newline at end of file +public fun UnivariateHistogramBuilder.fill(buffer: Buffer): Unit = buffer.asSequence().forEach(this::putValue) \ No newline at end of file diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt deleted file mode 100644 index 3d1bd20c0..000000000 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogramSpace.kt +++ /dev/null @@ -1,185 +0,0 @@ -package kscience.kmath.histogram - -import kscience.kmath.operations.Space -import kscience.kmath.structures.Buffer -import java.util.* -import kotlin.math.abs -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 -} - - -private class UnivariateHistogramImpl( - override val context: UnivariateHistogramSpace, - val binMap: TreeMap, -) : UnivariateHistogram { - override fun get(value: Double): UnivariateBin? = binMap.getBin(value) - override val dimension: Int get() = 1 - override val bins: Collection get() = binMap.values -} - -private class UnivariateBinCounter( - override val def: UnivariateHistogramBinDefinition, -) : UnivariateBin { - val counter: LongCounter = LongCounter() - val valueCounter: ObjectCounter = Counter.real() - - /** - * The precise number of events ignoring weighting - */ - val count: Long get() = counter.value - - override val standardDeviation: Double get() = sqrt(count.toDouble()) / count * value - - /** - * The value of histogram including weighting - */ - override val value: Double get() = valueCounter.value - - public fun increment(count: Long, value: Double) { - counter.add(count) - valueCounter.add(value) - } -} - -private class UnivariateBinValue( - override val def: UnivariateHistogramBinDefinition, - override val value: Double, - override val standardDeviation: Double, -) : UnivariateBin - - -public class UnivariateHistogramSpace( - public val binFactory: (Double) -> UnivariateHistogramBinDefinition, -) : Space { - - private inner class UnivariateHistogramBuilderImpl : UnivariateHistogramBuilder { - - val bins: TreeMap = TreeMap() - fun get(value: Double): UnivariateBinCounter? = bins.getBin(value) - - private fun createBin(value: Double): UnivariateBinCounter { - val binDefinition = binFactory(value) - val newBin = UnivariateBinCounter(binDefinition) - synchronized(this) { bins[binDefinition.position] = newBin } - return newBin - } - - /** - * Thread safe put operation - */ - override fun put(value: Double, weight: Double) { - (get(value) ?: createBin(value)).apply { - increment(1, weight) - } - } - - override fun putValue(point: Buffer, value: Number) { - put(point[0], value.toDouble()) - } - - /** - * Put several items into a single bin - */ - override fun putMany(value: Double, count: Int, weight: Double) { - (get(value) ?: createBin(value)).apply { - increment(count.toLong(), weight) - } - } - - override fun build(): UnivariateHistogram = UnivariateHistogramImpl(this@UnivariateHistogramSpace, bins) - } - - - public fun builder(): UnivariateHistogramBuilder = UnivariateHistogramBuilderImpl() - - public fun produce(builder: UnivariateHistogramBuilder.() -> Unit): UnivariateHistogram = - UnivariateHistogramBuilderImpl().apply(builder).build() - - override fun add( - a: UnivariateHistogram, - b: UnivariateHistogram, - ): UnivariateHistogram { - 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 { - (a.bins.map { it.def } union b.bins.map { it.def }).forEach { def -> - val newBin = UnivariateBinValue( - def, - value = (a[def.position]?.value ?: 0.0) + (b[def.position]?.value ?: 0.0), - standardDeviation = (a[def.position]?.standardDeviation - ?: 0.0) + (b[def.position]?.standardDeviation ?: 0.0) - ) - } - } - return UnivariateHistogramImpl(this, bins) - } - - override fun multiply(a: UnivariateHistogram, k: Number): UnivariateHistogram { - val bins = TreeMap().apply { - a.bins.forEach { bin -> - put(bin.position, - UnivariateBinValue( - bin.def, - value = bin.value * k.toDouble(), - standardDeviation = abs(bin.standardDeviation * k.toDouble()) - ) - ) - } - } - - return UnivariateHistogramImpl(this, bins) - } - - override val zero: UnivariateHistogram = produce { } - - public companion object { - /** - * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. - */ - public fun uniform( - binSize: Double, - start: Double = 0.0 - ): UnivariateHistogramSpace = UnivariateHistogramSpace { value -> - val center = start + binSize * Math.floor((value - start) / binSize + 0.5) - UnivariateHistogramBinDefinition(center, binSize) - } - - /** - * Create a histogram with custom cell borders - */ - public fun custom(borders: DoubleArray): UnivariateHistogramSpace { - val sorted = borders.sortedArray() - - return UnivariateHistogramSpace { value -> - when { - value < sorted.first() -> UnivariateHistogramBinDefinition( - Double.NEGATIVE_INFINITY, - Double.MAX_VALUE - ) - - value > sorted.last() -> UnivariateHistogramBinDefinition( - Double.POSITIVE_INFINITY, - Double.MAX_VALUE - ) - - else -> { - val index = sorted.indices.first { value > sorted[it] } - val left = sorted[index] - val right = sorted[index + 1] - UnivariateHistogramBinDefinition((left + right) / 2, (right - left)) - } - } - } - } - } -} \ No newline at end of file From d1b4d1ac115ef5de448bec5ae358475f2f9029a4 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 15 Feb 2021 09:32:25 +0300 Subject: [PATCH 4/4] Histograms refactor --- .../kmath/histogram/TreeHistogramSpace.kt | 27 +++++++++---------- .../kmath/histogram/UnivariateHistogram.kt | 25 ++++++++--------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt index b5872c601..67964fbeb 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/TreeHistogramSpace.kt @@ -8,7 +8,7 @@ import java.util.* import kotlin.math.abs import kotlin.math.sqrt -internal fun > TreeMap.getBin(value: Double): B? { +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 @@ -29,12 +29,9 @@ public class TreeHistogram( override val bins: Collection get() = binMap.values } -private class UnivariateBinValue( - override val domain: UnivariateDomain, - override val value: Double, - override val standardDeviation: Double, -) : UnivariateBin, ClosedFloatingPointRange by domain.range - +/** + * A space for univariate histograms with variable bin borders based on a tree map + */ @UnstableKMathAPI public class TreeHistogramSpace( public val binFactory: (Double) -> UnivariateDomain, @@ -73,7 +70,7 @@ public class TreeHistogramSpace( val resBins = TreeMap() bins.forEach { key, binCounter -> val count = binCounter.counter.value - resBins[key] = UnivariateBinValue(binCounter.domain, count, sqrt(count)) + resBins[key] = UnivariateBin(binCounter.domain, count, sqrt(count)) } return TreeHistogram(this, resBins) } @@ -86,11 +83,13 @@ public class TreeHistogramSpace( require(b.context == this) { "Histogram $b does not belong to this context" } val bins = TreeMap().apply { (a.bins.map { it.domain } union b.bins.map { it.domain }).forEach { def -> - val newBin = UnivariateBinValue( - def, - value = (a[def.center]?.value ?: 0.0) + (b[def.center]?.value ?: 0.0), - standardDeviation = (a[def.center]?.standardDeviation - ?: 0.0) + (b[def.center]?.standardDeviation ?: 0.0) + put(def.center, + UnivariateBin( + def, + value = (a[def.center]?.value ?: 0.0) + (b[def.center]?.value ?: 0.0), + standardDeviation = (a[def.center]?.standardDeviation + ?: 0.0) + (b[def.center]?.standardDeviation ?: 0.0) + ) ) } } @@ -101,7 +100,7 @@ public class TreeHistogramSpace( val bins = TreeMap().apply { a.bins.forEach { bin -> put(bin.domain.center, - UnivariateBinValue( + UnivariateBin( bin.domain, value = bin.value * k.toDouble(), standardDeviation = abs(bin.standardDeviation * k.toDouble()) diff --git a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt index 87c56cf79..68b9019d0 100644 --- a/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt +++ b/kmath-histograms/src/jvmMain/kotlin/kscience/kmath/histogram/UnivariateHistogram.kt @@ -10,26 +10,23 @@ import kscience.kmath.structures.asSequence public val UnivariateDomain.center: Double get() = (range.endInclusive - range.start) / 2 -public interface UnivariateBin : Bin, ClosedFloatingPointRange { - public val domain: UnivariateDomain - - /** - * The value of histogram including weighting - */ - public override val value: Double - - /** - * Standard deviation of the bin value. Zero if not applicable - */ - public val standardDeviation: Double +/** + * A univariate bin based an a range + * @param value The value of histogram including weighting + * @param standardDeviation Standard deviation of the bin value. Zero or negative if not applicable + */ +public class UnivariateBin( + public val domain: UnivariateDomain, + override val value: Double, + public val standardDeviation: Double, +) : Bin, ClosedFloatingPointRange by domain.range { public override val dimension: Int get() = 1 public override fun contains(point: Buffer): Boolean = point.size == 1 && contains(point[0]) - } -@UnstableKMathAPI +@OptIn(UnstableKMathAPI::class) public interface UnivariateHistogram : Histogram, SpaceElement> { public operator fun get(value: Double): UnivariateBin?