From afac2a2140140b2f66370eecc655678f0cd60e86 Mon Sep 17 00:00:00 2001 From: jmh530 Date: Sun, 25 Oct 2026 10:25:59 -0400 Subject: [PATCH 1/3] Extract the shared statistical result-type helper Move statType and its existing tests into mir.math.internal.stat_type. Update product type deduction to import the helper directly, removing its dependency on mir.math.stat. Preserve the package-visible statType import through mir.math.stat and register the new internal module in the Meson source list. AI-Assisted-By: Codex (GPT-6 Astra Medium) --- meson.build | 1 + source/mir/math/internal/stat_type.d | 152 +++++++++++++++++++++++++++ source/mir/math/numeric.d | 2 +- source/mir/math/stat.d | 145 +------------------------ 4 files changed, 156 insertions(+), 144 deletions(-) create mode 100644 source/mir/math/internal/stat_type.d diff --git a/meson.build b/meson.build index 263170b8..347f59ed 100644 --- a/meson.build +++ b/meson.build @@ -52,6 +52,7 @@ sources_list = [ 'mir/math/func/normal', 'mir/math/numeric', 'mir/math/stat', + 'mir/math/internal/stat_type', 'mir/math/sum', 'mir/ndslice/allocation', 'mir/ndslice/chunks', diff --git a/source/mir/math/internal/stat_type.d b/source/mir/math/internal/stat_type.d new file mode 100644 index 00000000..f33252a7 --- /dev/null +++ b/source/mir/math/internal/stat_type.d @@ -0,0 +1,152 @@ +/++ Internal result-type selection shared by statistical and product algorithms. +License: Apache-2.0 +Authors: Shigeki Karita, Ilia Ki, John Michael Hall +Copyright: 2020 Ilia Ki, Kaleidic Associates Advisory Limited, Symmetry Investments ++/ +module mir.math.internal.stat_type; + +import std.traits: Unqual; + +/// +package(mir) +template statType(T, bool checkComplex = true) +{ + import mir.internal.utility: isFloatingPoint; + + static if (isFloatingPoint!T) { + import std.traits: Unqual; + alias statType = Unqual!T; + } else static if (is(T : double)) { + alias statType = double; + } else static if (checkComplex) { + import mir.internal.utility: isComplex; + static if (isComplex!T) { + static if (__traits(getAliasThis, T).length == 1) + { + alias statType = .statType!(typeof(__traits(getMember, T, __traits(getAliasThis, T)[0]))); + } + else + { + alias statType = Unqual!T; + } + } else { + static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a complex floating point type"); + } + } else { + static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a floating point type"); + } +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + static assert(is(statType!int == double)); + static assert(is(statType!uint == double)); + static assert(is(statType!double == double)); + static assert(is(statType!float == float)); + static assert(is(statType!real == real)); + + static assert(is(statType!(const(int)) == double)); + static assert(is(statType!(immutable(int)) == double)); + static assert(is(statType!(const(double)) == double)); + static assert(is(statType!(immutable(double)) == double)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + import mir.complex: Complex; + + static assert(is(statType!(Complex!float) == Complex!float)); + static assert(is(statType!(Complex!double) == Complex!double)); + static assert(is(statType!(Complex!real) == Complex!real)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + static struct Foo { + float x; + alias x this; + } + + static assert(is(statType!Foo == double)); // note: this is not float +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + import mir.complex; + static struct Foo { + Complex!float x; + alias x this; + } + + static assert(is(statType!Foo == Complex!float)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + static struct Foo { + double x; + alias x this; + } + + static assert(is(statType!Foo == double)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + import mir.complex; + static struct Foo { + Complex!double x; + alias x this; + } + + static assert(is(statType!Foo == Complex!double)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + static struct Foo { + real x; + alias x this; + } + + static assert(is(statType!Foo == double)); // note: this is not real +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + import mir.complex; + static struct Foo { + Complex!real x; + alias x this; + } + + static assert(is(statType!Foo == Complex!real)); +} + +version(mir_test) +@safe pure nothrow @nogc +unittest +{ + static struct Foo { + int x; + alias x this; + } + + static assert(is(statType!Foo == double)); // note: this is not ints +} diff --git a/source/mir/math/numeric.d b/source/mir/math/numeric.d index 2a14cc56..dc717425 100644 --- a/source/mir/math/numeric.d +++ b/source/mir/math/numeric.d @@ -199,7 +199,7 @@ package template prodType(T) auto temp = U.init * U.init; temp *= U.init; })) { - import mir.math.stat: statType; + import mir.math.internal.stat_type: statType; alias V = typeof(U.init * U.init); alias prodType = statType!(V, false); diff --git a/source/mir/math/stat.d b/source/mir/math/stat.d index 72db9a0e..6f7b5bec 100644 --- a/source/mir/math/stat.d +++ b/source/mir/math/stat.d @@ -29,149 +29,8 @@ import std.traits: Unqual, isArray, isMutable, isIterable, isIntegral, CommonTyp /// public import mir.math.sum: Summation; -/// -package(mir) -template statType(T, bool checkComplex = true) -{ - import mir.internal.utility: isFloatingPoint; - - static if (isFloatingPoint!T) { - import std.traits: Unqual; - alias statType = Unqual!T; - } else static if (is(T : double)) { - alias statType = double; - } else static if (checkComplex) { - import mir.internal.utility: isComplex; - static if (isComplex!T) { - static if (__traits(getAliasThis, T).length == 1) - { - alias statType = .statType!(typeof(__traits(getMember, T, __traits(getAliasThis, T)[0]))); - } - else - { - alias statType = Unqual!T; - } - } else { - static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a complex floating point type"); - } - } else { - static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a floating point type"); - } -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - static assert(is(statType!int == double)); - static assert(is(statType!uint == double)); - static assert(is(statType!double == double)); - static assert(is(statType!float == float)); - static assert(is(statType!real == real)); - - static assert(is(statType!(const(int)) == double)); - static assert(is(statType!(immutable(int)) == double)); - static assert(is(statType!(const(double)) == double)); - static assert(is(statType!(immutable(double)) == double)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - import mir.complex: Complex; - - static assert(is(statType!(Complex!float) == Complex!float)); - static assert(is(statType!(Complex!double) == Complex!double)); - static assert(is(statType!(Complex!real) == Complex!real)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - static struct Foo { - float x; - alias x this; - } - - static assert(is(statType!Foo == double)); // note: this is not float -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - import mir.complex; - static struct Foo { - Complex!float x; - alias x this; - } - - static assert(is(statType!Foo == Complex!float)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - static struct Foo { - double x; - alias x this; - } - - static assert(is(statType!Foo == double)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - import mir.complex; - static struct Foo { - Complex!double x; - alias x this; - } - - static assert(is(statType!Foo == Complex!double)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - static struct Foo { - real x; - alias x this; - } - - static assert(is(statType!Foo == double)); // note: this is not real -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - import mir.complex; - static struct Foo { - Complex!real x; - alias x this; - } - - static assert(is(statType!Foo == Complex!real)); -} - -version(mir_test) -@safe pure nothrow @nogc -unittest -{ - static struct Foo { - int x; - alias x this; - } - - static assert(is(statType!Foo == double)); // note: this is not ints -} +// Preserve the package-visible import path for existing mir code. +package(mir) import mir.math.internal.stat_type: statType; /// package(mir) From f6906076c95bbd2c2bec9b463307e6a67a259620 Mon Sep 17 00:00:00 2001 From: jmh530 Date: Sun, 25 Oct 2026 10:26:07 -0400 Subject: [PATCH 2/3] Remove statistics dependencies from summation and ndslice examples Use Summator directly in the moving-average example, deriving the observation count from its fixed-size circular buffer. Define local averages using summation and element counts in the ndslice examples. Preserve their demonstrations and expected results without importing the statistics APIs planned for deprecation. AI-Assisted-By: Codex (GPT-6 Astra Medium) --- source/mir/math/sum.d | 12 +++++------- source/mir/ndslice/topology.d | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/source/mir/math/sum.d b/source/mir/math/sum.d index c74f106e..9a8700af 100644 --- a/source/mir/math/sum.d +++ b/source/mir/math/sum.d @@ -224,9 +224,7 @@ unittest struct MovingAverage(T) if (isFloatingPoint!T) { - import mir.math.stat: MeanAccumulator; - - MeanAccumulator!(T, Summation.precise) meanAccumulator; + Summator!(T, Summation.precise) summator; double[] circularBuffer; size_t frontIndex; @@ -234,24 +232,24 @@ unittest auto avg() @property const { - return meanAccumulator.mean; + return summator.sum / cast(T) circularBuffer.length; } this(double[] buffer) { assert(buffer.length); circularBuffer = buffer; - meanAccumulator.put(buffer); + summator.put(buffer); } ///operation without rounding void put(T x) { import mir.utility: swap; - meanAccumulator.summator += x; + summator += x; swap(circularBuffer[frontIndex++], x); frontIndex = frontIndex == circularBuffer.length ? 0 : frontIndex; - meanAccumulator.summator -= x; + summator -= x; } } diff --git a/source/mir/ndslice/topology.d b/source/mir/ndslice/topology.d index d8b76ffa..99e1ba83 100644 --- a/source/mir/ndslice/topology.d +++ b/source/mir/ndslice/topology.d @@ -2966,7 +2966,9 @@ unittest { import mir.ndslice.topology: byDim, alongDim; import mir.ndslice.fuse: fuse; - import mir.math.stat: mean; + import mir.math.sum: sum; + import mir.primitives: elementCount; + alias mean = (values) => values.sum / cast(double) values.elementCount; import mir.algorithm.iteration: all; import mir.math.common: approxEqual; @@ -3015,7 +3017,9 @@ pure version(mir_ndslice_test) unittest { import mir.algorithm.iteration: reduce; import mir.math.common: fmax; - import mir.math.stat: mean; + import mir.math.sum: sum; + import mir.primitives: elementCount; + alias mean = (values) => values.sum / cast(double) values.elementCount; import mir.math.sum; /// Returns maximal column average. auto maxAvg(S)(S matrix) { @@ -3187,7 +3191,9 @@ version(mir_ndslice_test) unittest { import mir.ndslice.fuse: fuse; - import mir.math.stat: mean; + import mir.math.sum: sum; + import mir.primitives: elementCount; + alias mean = (values) => values.sum / cast(double) values.elementCount; import mir.algorithm.iteration: all; import mir.math.common: approxEqual; @@ -3200,7 +3206,7 @@ unittest { double factor; this(double f) {factor = f;} - auto opCall(U)(U x) const {return x.mean + factor; } + auto opCall(U)(U x) const {return mean(x) + factor; } auto lightConst()() const @property { return Callable(factor); } } @@ -5089,7 +5095,9 @@ unittest { import mir.ndslice.topology: alongDim; import mir.ndslice.fuse: fuse; - import mir.math.stat: mean; + import mir.math.sum: sum; + import mir.primitives: elementCount; + alias mean = (values) => values.sum / cast(double) values.elementCount; import mir.algorithm.iteration: all; import mir.math.common: approxEqual; @@ -5432,7 +5440,9 @@ unittest { import mir.ndslice.topology: byDim; import mir.ndslice.fuse: fuse; - import mir.math.stat: mean; + import mir.math.sum: sum; + import mir.primitives: elementCount; + alias mean = (values) => values.sum / cast(double) values.elementCount; import mir.algorithm.iteration: all; import mir.math.common: approxEqual; From d1d5da38732d2fd9d3af6563627c2fe7434444bb Mon Sep 17 00:00:00 2001 From: jmh530 Date: Sun, 25 Oct 2026 10:26:07 -0400 Subject: [PATCH 3/3] Deprecate central-tendency APIs in favor of mir-stat Deprecate mean, hmean, gmean, MeanAccumulator, GMeanAccumulator, median, and center while preserving their implementations and defaults. Direct callers to mir.stat.descriptive for means and median, and mir.stat for center. Document the dependency and replacement imports, numerical differences, and distinct accumulator types. Leave simpleLinearRegression and the module itself undeprecated. AI-Assisted-By: Codex (GPT-6 Astra Medium) --- source/mir/math/stat.d | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/source/mir/math/stat.d b/source/mir/math/stat.d index 6f7b5bec..041be75e 100644 --- a/source/mir/math/stat.d +++ b/source/mir/math/stat.d @@ -1,6 +1,23 @@ /++ This module contains base statistical algorithms. +The mean functions (`mean`, `hmean`, `gmean`), their accumulators +(`MeanAccumulator`, `GMeanAccumulator`), and `median` are deprecated in favor of +the corresponding exports from `mir.stat.descriptive` in the +$(WEB github.com/libmir/mir-stat, mir-stat) package. Use `center` from `mir.stat`. +Add mir-stat as a dependency and update selective imports, for example: + +--- +import mir.stat.descriptive: mean, hmean, gmean, median, + MeanAccumulator, GMeanAccumulator; +import mir.stat: center; +--- + +The legacy implementations and defaults remain unchanged. Mir-stat includes +numerical fixes, so results can differ for extreme inputs. Accumulator types +from the two packages are distinct; migrate interacting declarations together. +`center` remains a lazy view and `median` preserves its input by default. + Note that used specialized summing algorithms execute more primitive operations than vanilla summation. Therefore, if in certain cases maximum speed is required at expense of precision, one can use $(REF_ALTTEXT $(TT Summation.fast), Summation.fast, mir, math, sum)$(NBSP). @@ -97,6 +114,7 @@ unittest /++ Output range for mean. +/ +deprecated("Use MeanAccumulator from mir.stat.descriptive (mir-stat) instead.") struct MeanAccumulator(T, Summation summation) { /// @@ -209,6 +227,7 @@ Returns: See_also: $(SUBREF sum, Summation) +/ +deprecated("Use mean from mir.stat.descriptive (mir-stat) instead.") template mean(F, Summation summation = Summation.appropriate) { /++ @@ -238,6 +257,7 @@ template mean(F, Summation summation = Summation.appropriate) } /// ditto +deprecated("Use mean from mir.stat.descriptive (mir-stat) instead.") template mean(Summation summation = Summation.appropriate) { /++ @@ -263,12 +283,14 @@ template mean(Summation summation = Summation.appropriate) } /// ditto +deprecated("Use mean from mir.stat.descriptive (mir-stat) instead.") template mean(F, string summation) { mixin("alias mean = .mean!(F, Summation." ~ summation ~ ");"); } /// ditto +deprecated("Use mean from mir.stat.descriptive (mir-stat) instead.") template mean(string summation) { mixin("alias mean = .mean!(Summation." ~ summation ~ ");"); @@ -537,6 +559,7 @@ Returns: See_also: $(SUBREF sum, Summation) +/ +deprecated("Use hmean from mir.stat.descriptive (mir-stat) instead.") template hmean(F, Summation summation = Summation.appropriate) { /++ @@ -589,6 +612,7 @@ template hmean(F, Summation summation = Summation.appropriate) } /// ditto +deprecated("Use hmean from mir.stat.descriptive (mir-stat) instead.") template hmean(Summation summation = Summation.appropriate) { /++ @@ -614,12 +638,14 @@ template hmean(Summation summation = Summation.appropriate) } /// ditto +deprecated("Use hmean from mir.stat.descriptive (mir-stat) instead.") template hmean(F, string summation) { mixin("alias hmean = .hmean!(F, Summation." ~ summation ~ ");"); } /// ditto +deprecated("Use hmean from mir.stat.descriptive (mir-stat) instead.") template hmean(string summation) { mixin("alias hmean = .hmean!(Summation." ~ summation ~ ");"); @@ -795,6 +821,7 @@ unittest /++ Output range for gmean. +/ +deprecated("Use GMeanAccumulator from mir.stat.descriptive (mir-stat) instead.") struct GMeanAccumulator(T) if (isMutable!T && isFloatingPoint!T) { @@ -916,6 +943,7 @@ Returns: See_also: $(SUBREF numeric, prod) +/ +deprecated("Use gmean from mir.stat.descriptive (mir-stat) instead.") @fmamath gmeanType!F gmean(F, Range)(Range r) if (isFloatingPoint!F && isIterable!Range) { @@ -926,6 +954,7 @@ See_also: } /// ditto +deprecated("Use gmean from mir.stat.descriptive (mir-stat) instead.") @fmamath gmeanType!Range gmean(Range)(Range r) if (isIterable!Range) { @@ -937,6 +966,7 @@ See_also: Params: ar = values +/ +deprecated("Use gmean from mir.stat.descriptive (mir-stat) instead.") @fmamath gmeanType!F gmean(F)(scope const F[] ar...) if (isFloatingPoint!F) { @@ -1175,6 +1205,7 @@ Returns: See_also: $(SUBREF stat, mean) +/ +deprecated("Use median from mir.stat.descriptive (mir-stat) instead.") template median(F, bool allowModify = false) { /++ @@ -1227,6 +1258,7 @@ template median(F, bool allowModify = false) } /// ditto +deprecated("Use median from mir.stat.descriptive (mir-stat) instead.") template median(bool allowModify = false) { /// ditto @@ -1245,6 +1277,7 @@ template median(bool allowModify = false) Params: ar = array +/ +deprecated("Use median from mir.stat.descriptive (mir-stat) instead.") meanType!(T[]) median(T)(scope const T[] ar...) { import mir.ndslice.slice: sliced; @@ -1257,6 +1290,7 @@ meanType!(T[]) median(T)(scope const T[] ar...) Params: withAsSlice = input that satisfies hasAsSlice +/ +deprecated("Use median from mir.stat.descriptive (mir-stat) instead.") auto median(T)(T withAsSlice) if (hasAsSlice!T) { @@ -1573,6 +1607,7 @@ provided using `centralTendency`. Returns: The elements in the slice with the average subtracted from them. +/ +deprecated("Use center from mir.stat (mir-stat) instead.") template center(alias centralTendency = mean!(Summation.appropriate)) { import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;