From 29369cd6d7ef4a0b127df5551953e0b7239ffdec Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 22 Mar 2022 22:17:20 +0300 Subject: [PATCH] [WIP] Another histogram refactor --- .../{UnivariateDomain.kt => Domain1D.kt} | 15 +++- .../kmath/histogram/DoubleHistogramSpace.kt | 22 +++--- .../kscience/kmath/histogram/Histogram.kt | 33 +++++---- .../kscience/kmath/histogram/Histogram1D.kt | 56 +++++++++++++++ .../kmath/histogram/IndexedHistogramSpace.kt | 27 +++---- .../histogram/UniformDoubleHistogram1D.kt | 9 +++ .../kmath/histogram/UnivariateHistogram.kt | 59 --------------- .../kmath/histogram/TreeHistogramSpace.kt | 71 +++++++++---------- 8 files changed, 159 insertions(+), 133 deletions(-) rename kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/{UnivariateDomain.kt => Domain1D.kt} (57%) create mode 100644 kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt create mode 100644 kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformDoubleHistogram1D.kt delete mode 100644 kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UnivariateHistogram.kt diff --git a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/UnivariateDomain.kt b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/Domain1D.kt similarity index 57% rename from kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/UnivariateDomain.kt rename to kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/Domain1D.kt index 9020ef8cb..f50f16c11 100644 --- a/kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/UnivariateDomain.kt +++ b/kmath-core/src/commonMain/kotlin/space/kscience/kmath/domains/Domain1D.kt @@ -9,16 +9,21 @@ import space.kscience.kmath.linear.Point import space.kscience.kmath.misc.UnstableKMathAPI @UnstableKMathAPI -public class UnivariateDomain(public val range: ClosedFloatingPointRange) : DoubleDomain { +public abstract class Domain1D>(public val range: ClosedRange) : Domain { override val dimension: Int get() = 1 - public operator fun contains(d: Double): Boolean = range.contains(d) + public operator fun contains(value: T): Boolean = range.contains(value) - override operator fun contains(point: Point): Boolean { + override operator fun contains(point: Point): Boolean { require(point.size == 0) return contains(point[0]) } +} +@UnstableKMathAPI +public class DoubleDomain1D( + @Suppress("CanBeParameter") public val doubleRange: ClosedFloatingPointRange, +) : Domain1D(doubleRange), DoubleDomain { override fun getLowerBound(num: Int): Double { require(num == 0) return range.start @@ -31,3 +36,7 @@ public class UnivariateDomain(public val range: ClosedFloatingPointRange override fun volume(): Double = range.endInclusive - range.start } + +@UnstableKMathAPI +public val DoubleDomain1D.center: Double + get() = (range.endInclusive + range.start) / 2 diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/DoubleHistogramSpace.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/DoubleHistogramSpace.kt index 27f04d60b..c0df8c2cc 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/DoubleHistogramSpace.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/DoubleHistogramSpace.kt @@ -6,6 +6,7 @@ package space.kscience.kmath.histogram import space.kscience.kmath.domains.HyperSquareDomain +import space.kscience.kmath.linear.Point import space.kscience.kmath.misc.UnstableKMathAPI import space.kscience.kmath.nd.* import space.kscience.kmath.operations.DoubleField @@ -31,7 +32,7 @@ public class DoubleHistogramSpace( public val dimension: Int get() = lower.size override val shape: IntArray = IntArray(binNums.size) { binNums[it] + 2 } - override val histogramValueSpace: DoubleFieldND = DoubleField.ndAlgebra(*shape) + override val histogramValueAlgebra: DoubleFieldND = DoubleField.ndAlgebra(*shape) private val binSize = DoubleBuffer(dimension) { (upper[it] - lower[it]) / binNums[it] } @@ -70,21 +71,20 @@ public class DoubleHistogramSpace( } @OptIn(UnstableKMathAPI::class) - public val Bin.domain: HyperSquareDomain - get() = (this as? DomainBin)?.domain as? HyperSquareDomain - ?: error("Im a teapot. This is not my bin") - - @OptIn(UnstableKMathAPI::class) - override fun produceBin(index: IntArray, value: Double): DomainBin { + override fun produceBin(index: IntArray, value: Double): DomainBin { val domain = getDomain(index) return DomainBin(domain, value) } - override fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram { + override fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram { val ndCounter = StructureND.auto(shape) { Counter.double() } - val hBuilder = HistogramBuilder { point, value -> - val index = getIndex(point) - ndCounter[index].add(value.toDouble()) + val hBuilder = object : HistogramBuilder { + override val defaultValue: Double get() = 1.0 + + override fun putValue(point: Point, value: Double) { + val index = getIndex(point) + ndCounter[index].add(value) + } } hBuilder.apply(builder) val values: BufferND = ndCounter.mapToBuffer { it.value } diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram.kt index 4e803fc63..64c031c7a 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram.kt @@ -13,14 +13,14 @@ import space.kscience.kmath.structures.asBuffer /** * The binned data element. Could be a histogram bin with a number of counts or an artificial construct. */ -public interface Bin : Domain { +public interface Bin : Domain { /** * The value of this bin. */ - public val value: Number + public val value: V } -public interface Histogram> { +public interface Histogram> { /** * Find existing bin, corresponding to given coordinates */ @@ -32,29 +32,38 @@ public interface Histogram> { public val dimension: Int public val bins: Iterable + + public companion object { + //A discoverability root + } } -public fun interface HistogramBuilder { +public interface HistogramBuilder { /** - * Increment appropriate bin + * The default value increment for a bin */ - public fun putValue(point: Point, value: Number) + public val defaultValue: V + + /** + * Increment appropriate bin with given value + */ + public fun putValue(point: Point, value: V = defaultValue) } -public fun > HistogramBuilder.put(point: Point): Unit = putValue(point, 1.0) +public fun HistogramBuilder.put(point: Point): Unit = putValue(point) -public fun HistogramBuilder.put(vararg point: T): Unit = put(point.asBuffer()) +public fun HistogramBuilder.put(vararg point: T): Unit = put(point.asBuffer()) -public fun HistogramBuilder.put(vararg point: Number): Unit = +public fun HistogramBuilder.put(vararg point: Number): Unit = put(DoubleBuffer(point.map { it.toDouble() }.toDoubleArray())) -public fun HistogramBuilder.put(vararg point: Double): Unit = put(DoubleBuffer(point)) -public fun HistogramBuilder.fill(sequence: Iterable>): Unit = sequence.forEach { put(it) } +public fun HistogramBuilder.put(vararg point: Double): Unit = put(DoubleBuffer(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/space/kscience/kmath/histogram/Histogram1D.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt new file mode 100644 index 000000000..e9c62b141 --- /dev/null +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/Histogram1D.kt @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package space.kscience.kmath.histogram + +import space.kscience.kmath.domains.Domain1D +import space.kscience.kmath.misc.UnstableKMathAPI +import space.kscience.kmath.operations.asSequence +import space.kscience.kmath.structures.Buffer + + +/** + * A univariate bin based on a range + * + * @property value The value of histogram including weighting + * @property standardDeviation Standard deviation of the bin value. Zero or negative if not applicable + */ +@UnstableKMathAPI +public class Bin1D, out V>( + public val domain: Domain1D, + override val value: V, +) : Bin, ClosedRange by domain.range { + + override val dimension: Int get() = 1 + + override fun contains(point: Buffer): Boolean = point.size == 1 && contains(point[0]) +} + +@OptIn(UnstableKMathAPI::class) +public interface Histogram1D, V> : Histogram> { + override val dimension: Int get() = 1 + public operator fun get(value: T): Bin1D? + override operator fun get(point: Buffer): Bin1D? = get(point[0]) +} + +@UnstableKMathAPI +public interface Histogram1DBuilder : HistogramBuilder { + /** + * Thread safe put operation + */ + public fun putValue(at: T, value: V = defaultValue) +} + +@UnstableKMathAPI +public fun Histogram1DBuilder.fill(items: Iterable): Unit = + items.forEach(this::putValue) + +@UnstableKMathAPI +public fun Histogram1DBuilder.fill(array: DoubleArray): Unit = + array.forEach(this::putValue) + +@UnstableKMathAPI +public fun Histogram1DBuilder.fill(buffer: Buffer): Unit = + buffer.asSequence().forEach(this::putValue) \ No newline at end of file diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/IndexedHistogramSpace.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/IndexedHistogramSpace.kt index bfacebb43..a2623b1a1 100644 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/IndexedHistogramSpace.kt +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/IndexedHistogramSpace.kt @@ -18,24 +18,28 @@ import space.kscience.kmath.operations.invoke /** * A simple histogram bin based on domain */ -public data class DomainBin>( +public data class DomainBin, out V>( public val domain: Domain, - override val value: Number, -) : Bin, Domain by domain + override val value: V, +) : Bin, Domain by domain +/** + * @param T the type of the argument space + * @param V the type of bin value + */ public class IndexedHistogram, V : Any>( public val histogramSpace: IndexedHistogramSpace, public val values: StructureND, -) : Histogram> { +) : Histogram> { - override fun get(point: Point): Bin? { + override fun get(point: Point): DomainBin? { val index = histogramSpace.getIndex(point) ?: return null return histogramSpace.produceBin(index, values[index]) } override val dimension: Int get() = histogramSpace.shape.size - override val bins: Iterable> + override val bins: Iterable> get() = DefaultStrides(histogramSpace.shape).asSequence().map { histogramSpace.produceBin(it, values[it]) }.asIterable() @@ -46,9 +50,8 @@ public class IndexedHistogram, V : Any>( */ public interface IndexedHistogramSpace, V : Any> : Group>, ScaleOperations> { - //public val valueSpace: Space public val shape: Shape - public val histogramValueSpace: FieldND //= NDAlgebra.space(valueSpace, Buffer.Companion::boxing, *shape), + public val histogramValueAlgebra: FieldND //= NDAlgebra.space(valueSpace, Buffer.Companion::boxing, *shape), /** * Resolve index of the bin including given [point] @@ -60,19 +63,19 @@ public interface IndexedHistogramSpace, V : Any> */ public fun getDomain(index: IntArray): Domain? - public fun produceBin(index: IntArray, value: V): Bin + public fun produceBin(index: IntArray, value: V): DomainBin - public fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram + public fun produce(builder: HistogramBuilder.() -> Unit): IndexedHistogram override fun add(left: IndexedHistogram, right: IndexedHistogram): IndexedHistogram { require(left.histogramSpace == this) { "Can't operate on a histogram produced by external space" } require(right.histogramSpace == this) { "Can't operate on a histogram produced by external space" } - return IndexedHistogram(this, histogramValueSpace { left.values + right.values }) + return IndexedHistogram(this, histogramValueAlgebra { left.values + right.values }) } override fun scale(a: IndexedHistogram, value: Double): IndexedHistogram { require(a.histogramSpace == this) { "Can't operate on a histogram produced by external space" } - return IndexedHistogram(this, histogramValueSpace { a.values * value }) + return IndexedHistogram(this, histogramValueAlgebra { a.values * value }) } override val zero: IndexedHistogram get() = produce { } diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformDoubleHistogram1D.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformDoubleHistogram1D.kt new file mode 100644 index 000000000..856cd8592 --- /dev/null +++ b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UniformDoubleHistogram1D.kt @@ -0,0 +1,9 @@ +/* + * 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. + */ + +package space.kscience.kmath.histogram + +//class UniformDoubleHistogram1D: DoubleHistogram1D { +//} \ No newline at end of file diff --git a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UnivariateHistogram.kt b/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UnivariateHistogram.kt deleted file mode 100644 index 69ea83ae3..000000000 --- a/kmath-histograms/src/commonMain/kotlin/space/kscience/kmath/histogram/UnivariateHistogram.kt +++ /dev/null @@ -1,59 +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. - */ - -package space.kscience.kmath.histogram - -import space.kscience.kmath.domains.UnivariateDomain -import space.kscience.kmath.misc.UnstableKMathAPI -import space.kscience.kmath.operations.asSequence -import space.kscience.kmath.structures.Buffer - - -@UnstableKMathAPI -public val UnivariateDomain.center: Double - get() = (range.endInclusive + range.start) / 2 - -/** - * A univariate bin based on a range - * - * @property value The value of histogram including weighting - * @property standardDeviation Standard deviation of the bin value. Zero or negative if not applicable - */ -@UnstableKMathAPI -public class UnivariateBin( - public val domain: UnivariateDomain, - override val value: Double, - public val standardDeviation: Double, -) : Bin, ClosedFloatingPointRange by domain.range { - - override val dimension: Int get() = 1 - - override fun contains(point: Buffer): Boolean = point.size == 1 && contains(point[0]) -} - -@OptIn(UnstableKMathAPI::class) -public interface UnivariateHistogram : Histogram { - public operator fun get(value: Double): UnivariateBin? - override operator fun get(point: Buffer): UnivariateBin? = get(point[0]) -} - -@UnstableKMathAPI -public interface UnivariateHistogramBuilder : HistogramBuilder { - /** - * Thread safe put operation - */ - public fun putValue(at: Double, value: Double = 1.0) - - override fun putValue(point: Buffer, value: Number) -} - -@UnstableKMathAPI -public fun UnivariateHistogramBuilder.fill(items: Iterable): Unit = items.forEach(this::putValue) - -@UnstableKMathAPI -public fun UnivariateHistogramBuilder.fill(array: DoubleArray): Unit = array.forEach(this::putValue) - -@UnstableKMathAPI -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/space/kscience/kmath/histogram/TreeHistogramSpace.kt b/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt index 1ab49003c..9d07d5014 100644 --- a/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt +++ b/kmath-histograms/src/jvmMain/kotlin/space/kscience/kmath/histogram/TreeHistogramSpace.kt @@ -5,7 +5,7 @@ package space.kscience.kmath.histogram -import space.kscience.kmath.domains.UnivariateDomain +import space.kscience.kmath.domains.DoubleDomain1D import space.kscience.kmath.misc.UnstableKMathAPI import space.kscience.kmath.operations.Group import space.kscience.kmath.operations.ScaleOperations @@ -15,7 +15,7 @@ import kotlin.math.abs import kotlin.math.floor import kotlin.math.sqrt -private 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 @@ -28,19 +28,18 @@ private fun > TreeMap.getBin(val @UnstableKMathAPI public class TreeHistogram( - 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 val binMap: TreeMap>, +) : Histogram1D { + override fun get(value: Double): Bin1D? = binMap.getBin(value) + override val bins: Collection> get() = binMap.values } @OptIn(UnstableKMathAPI::class) @PublishedApi -internal class TreeHistogramBuilder(val binFactory: (Double) -> UnivariateDomain) : UnivariateHistogramBuilder { +internal class TreeHistogramBuilder(val binFactory: (Double) -> DoubleDomain1D) : Histogram1DBuilder { - internal class BinCounter(val domain: UnivariateDomain, val counter: Counter = Counter.double()) : - ClosedFloatingPointRange by domain.range + internal class BinCounter(val domain: DoubleDomain1D, val counter: Counter = Counter.double()) : + ClosedRange by domain.range private val bins: TreeMap = TreeMap() @@ -64,15 +63,15 @@ internal class TreeHistogramBuilder(val binFactory: (Double) -> UnivariateDomain } } - override fun putValue(point: Buffer, value: Number) { + 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 map = bins.mapValuesTo(TreeMap>()) { (_, binCounter) -> val count = binCounter.counter.value - UnivariateBin(binCounter.domain, count, sqrt(count)) + Bin1D(binCounter.domain, count, sqrt(count)) } return TreeHistogram(map) } @@ -83,23 +82,23 @@ internal class TreeHistogramBuilder(val binFactory: (Double) -> UnivariateDomain */ @UnstableKMathAPI public class TreeHistogramSpace( - @PublishedApi internal val binFactory: (Double) -> UnivariateDomain, -) : Group, ScaleOperations { + @PublishedApi internal val binFactory: (Double) -> DoubleDomain1D, +) : Group>, ScaleOperations> { - public inline fun fill(block: UnivariateHistogramBuilder.() -> Unit): UnivariateHistogram = + public inline fun fill(block: Histogram1DBuilder.() -> Unit): Histogram1D = TreeHistogramBuilder(binFactory).apply(block).build() override fun add( - left: UnivariateHistogram, - right: UnivariateHistogram, - ): UnivariateHistogram { + left: Histogram1D, + right: Histogram1D, + ): Histogram1D { // 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 { + val bins = TreeMap>().apply { (left.bins.map { it.domain } union right.bins.map { it.domain }).forEach { def -> put( def.center, - UnivariateBin( + Bin1D( def, value = (left[def.center]?.value ?: 0.0) + (right[def.center]?.value ?: 0.0), standardDeviation = (left[def.center]?.standardDeviation @@ -111,12 +110,12 @@ public class TreeHistogramSpace( return TreeHistogram(bins) } - override fun scale(a: UnivariateHistogram, value: Double): UnivariateHistogram { - val bins = TreeMap().apply { + override fun scale(a: Histogram1D, value: Double): Histogram1D { + val bins = TreeMap>().apply { a.bins.forEach { bin -> put( bin.domain.center, - UnivariateBin( + Bin1D( bin.domain, value = bin.value * value, standardDeviation = abs(bin.standardDeviation * value) @@ -128,38 +127,38 @@ public class TreeHistogramSpace( return TreeHistogram(bins) } - override fun UnivariateHistogram.unaryMinus(): UnivariateHistogram = this * (-1) + override fun Histogram1D.unaryMinus(): Histogram1D = this * (-1) - override val zero: UnivariateHistogram by lazy { fill { } } + override val zero: Histogram1D by lazy { fill { } } public companion object { /** - * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. + * Build and fill a [DoubleHistogram1D]. Returns a read-only histogram. */ public inline fun uniform( binSize: Double, start: Double = 0.0, - builder: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = uniform(binSize, start).fill(builder) + builder: Histogram1DBuilder.() -> Unit, + ): Histogram1D = 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: UnivariateHistogramBuilder.() -> Unit, - ): UnivariateHistogram = custom(borders).fill(builder) + builder: Histogram1DBuilder.() -> Unit, + ): Histogram1D = custom(borders).fill(builder) /** - * Build and fill a [UnivariateHistogram]. Returns a read-only histogram. + * 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) - UnivariateDomain((center - binSize / 2)..(center + binSize / 2)) + DoubleDomain1D((center - binSize / 2)..(center + binSize / 2)) } /** @@ -170,11 +169,11 @@ public class TreeHistogramSpace( return TreeHistogramSpace { value -> when { - value < sorted.first() -> UnivariateDomain( + value < sorted.first() -> DoubleDomain1D( Double.NEGATIVE_INFINITY..sorted.first() ) - value > sorted.last() -> UnivariateDomain( + value > sorted.last() -> DoubleDomain1D( sorted.last()..Double.POSITIVE_INFINITY ) @@ -182,7 +181,7 @@ public class TreeHistogramSpace( val index = sorted.indices.first { value > sorted[it] } val left = sorted[index] val right = sorted[index + 1] - UnivariateDomain(left..right) + DoubleDomain1D(left..right) } } }