The OpenD Programming Language

1 /++
2 This module contains algorithms for descriptive statistics with weights.
3 
4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
5 
6 Authors: John Michael Hall
7 
8 Copyright: 2022 Mir Stat Authors.
9 
10 Macros:
11 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, stat, $1)$(NBSP)
12 SUB2REF = $(REF_ALTTEXT $(TT $2), $2, mir, stat, descriptive, $1)$(NBSP)
13 MATHREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, math, $1)$(NBSP)
14 NDSLICEREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, ndslice, $1)$(NBSP)
15 T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
16 T3=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $+))
17 T4=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3) $(TD $4))
18 
19 +/
20 
21 module mir.stat.descriptive.weighted;
22 
23 import mir.math.sum: ResolveSummationType, Summation, Summator;
24 
25 private void putter2(Slices, T, U, Summation summation1, Summation summation2)
26     (scope Slices slices, ref Summator!(T, summation1) seed1, ref Summator!(U, summation2) seed2)
27 {
28     import mir.functional: Tuple;
29     static if (is(Slices == Tuple!(V1, V2), V1, V2)) {
30         seed1.put(slices[0]);
31         seed2.put(slices[1]);
32     } else {
33         import mir.ndslice.internal: frontOf2;
34         do
35         {
36             frontOf2!(slices)[0].putter2(seed1, seed2);
37             slices.popFront;
38         }
39         while(!slices.empty);
40     }
41 }
42 
43 /++
44 Assumptions used for weighted moments
45 +/
46 enum AssumeWeights : bool
47 {
48     /++
49     Primary, does not assume weights sum to one
50     +/
51     primary,
52     
53     /++
54     Assumes weights sum to one
55     +/
56     sumToOne
57 }
58 
59 /++
60 Output range for wmean.
61 +/
62 struct WMeanAccumulator(T, Summation summation, AssumeWeights assumeWeights,
63                         U = T, Summation weightsSummation = summation)
64 {
65     import mir.ndslice.slice: isConvertibleToSlice, isSlice, kindOf;
66     import std.range.primitives: isInputRange;
67     import std.traits: isIterable;
68 
69     ///
70     Summator!(T, summation) wsummator;
71 
72     static if (!assumeWeights) {
73         ///
74         Summator!(U, weightsSummation) weights;
75     }
76 
77     ///
78     F wmean(F = T)() const @safe @property pure nothrow @nogc
79     {
80         static if (assumeWeights) {
81             return this.wsum!F;
82         } else {
83             assert(this.weight!F != 0, "weight must not equal zero");
84             return this.wsum!F / this.weight!F;
85         }
86     }
87 
88     ///
89     F wsum(F = T)() const @safe @property pure nothrow @nogc
90     {
91         return cast(F) wsummator.sum;
92     }
93 
94     ///
95     F weight(F = U)() const @safe @property pure nothrow @nogc
96     {
97         return cast(F) weights.sum;
98     }
99 
100     ///
101     void put(Slice1, Slice2)(Slice1 s, Slice2 w)
102         if (isSlice!Slice1 && isSlice!Slice2)
103     {
104         static assert (Slice1.N == Slice2.N, "s and w must have the same number of dimensions");
105         static assert (kindOf!Slice1 == kindOf!Slice2, "s and w must have the same kind");
106 
107         import mir.ndslice.slice: Contiguous;
108         import mir.ndslice.topology: zip, map;
109 
110         assert(s._lengths == w._lengths, "WMeanAcumulator.put: both slices must have the same lengths");
111 
112         static if (kindOf!Slice1 != Contiguous && Slice1.N > 1) {
113             assert(s.strides == w.strides, "WMeanAccumulator.put: cannot put canonical and universal slices when strides do not match");
114             auto combine = s.zip!true(w);
115         } else {
116             auto combine = s.zip!false(w);
117         }
118 
119         static if (assumeWeights) {
120             auto combine2 = combine.map!"a * b";
121             wsummator.put(combine2);
122         } else {
123             auto combine2 = combine.map!("b", "a * b");
124             combine2.putter2(weights, wsummator);
125         }
126     }
127 
128     ///
129     void put(SliceLike1, SliceLike2)(SliceLike1 s, SliceLike2 w)
130         if (isConvertibleToSlice!SliceLike1 && !isSlice!SliceLike1 &&
131             isConvertibleToSlice!SliceLike2 && !isSlice!SliceLike2)
132     {
133         import mir.ndslice.slice: toSlice;
134         this.put(s.toSlice, w.toSlice);
135     }
136 
137     ///
138     void put(Range)(Range r)
139         if (isIterable!Range && !assumeWeights)
140     {
141         import mir.primitives: hasShape, elementCount;
142         static if (hasShape!Range) {
143             wsummator.put(r);
144             weights.put(cast(U) r.elementCount);
145         } else {
146             foreach(x; r)
147             {
148                 this.put(x);
149             }
150         }
151     }
152 
153     ///
154     void put(RangeA, RangeB)(RangeA r, RangeB w)
155         if (isInputRange!RangeA && !isConvertibleToSlice!RangeA &&
156             isInputRange!RangeB && !isConvertibleToSlice!RangeB)
157     {
158         do
159         {
160             assert(!(!r.empty && w.empty) && !(r.empty && !w.empty),
161                    "r and w must both be empty at the same time, one cannot be empty while the other has remaining items");
162             this.put(r.front, w.front);
163             r.popFront;
164             w.popFront;
165         } while(!r.empty || !w.empty); // Using an || instead of && so that the loop does not end early. mis-matched lengths of r and w sould be caught by above assert
166     }
167 
168     ///
169     void put()(T x, U w)
170     {
171         static if (!assumeWeights) {
172             weights.put(w);
173         }
174         wsummator.put(x * w);
175     }
176 
177     ///
178     void put()(T x)
179         if (!assumeWeights)
180     {
181         weights.put(cast(U) 1);
182         wsummator.put(x);
183     }
184 
185     ///
186     void put(F = T, G = U)(WMeanAccumulator!(F, summation, assumeWeights, G, weightsSummation) wm)
187         if (!assumeWeights) // because calculating is easier. When assumeWeightsSumtoOne = true, need to divide original wsummator and wm by 2.
188     {
189         weights.put(cast(U) wm.weights);
190         wsummator.put(cast(T) wm.wsummator);
191     }
192 }
193 
194 /// Assume weights sum to 1
195 version(mir_stat_test)
196 @safe pure nothrow
197 unittest
198 {
199     import mir.math.sum: Summation;
200     import mir.ndslice.slice: sliced;
201     import mir.test: should;
202 
203     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
204     x.put([0.0, 1, 2, 3, 4].sliced, [0.2, 0.2, 0.2, 0.2, 0.2].sliced);
205     x.wmean.should == 2;
206     x.put(5, 0.0);
207     x.wmean.should == 2;
208 }
209 
210 // dynamic array test, assume weights sum to 1
211 version(mir_stat_test)
212 @safe pure nothrow
213 unittest
214 {
215     import mir.math.sum: Summation;
216     import mir.test: should;
217 
218     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
219     x.put([0.0, 1, 2, 3, 4], [0.2, 0.2, 0.2, 0.2, 0.2]);
220     x.wmean.should == 2;
221 }
222 
223 // static array test, assume weights sum to 1
224 version(mir_stat_test)
225 @safe pure nothrow @nogc
226 unittest
227 {
228     import mir.math.sum: Summation;
229     import mir.test: should;
230 
231     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
232     static immutable y = [0.0, 1, 2, 3, 4];
233     static immutable w = [0.2, 0.2, 0.2, 0.2, 0.2];
234     x.put(y, w);
235     x.wmean.should == 2;
236 }
237 
238 // 2-d slice test, assume weights sum to 1
239 version(mir_stat_test)
240 @safe pure
241 unittest
242 {
243     import mir.math.sum: Summation;
244     import mir.ndslice.fuse: fuse;
245     import mir.test: shouldApprox;
246 
247     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
248     auto y = [
249         [0.0, 1, 2],
250         [3.0, 4, 5]
251     ].fuse;
252     auto w = [
253         [1.0 / 21, 2.0 / 21, 3.0 / 21],
254         [4.0 / 21, 5.0 / 21, 6.0 / 21]
255     ].fuse;
256     x.put(y, w);
257     x.wmean.shouldApprox == 70.0 / 21;
258 }
259 
260 // universal 2-d slice test, assume weights sum to 1, using map
261 version(mir_stat_test)
262 @safe pure nothrow
263 unittest
264 {
265     import mir.math.sum: Summation;
266     import mir.ndslice.topology: iota, map, universal;
267     import mir.test: shouldApprox;
268 
269     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
270     auto y = iota([2, 3]).universal;
271     auto w = iota([2, 3], 1).map!(a => a / 21.0).universal;
272     x.put(y, w);
273     x.wmean.shouldApprox == 70.0 / 21;
274 }
275 
276 // 2-d canonical slice test, assume weights sum to 1, using map
277 version(mir_stat_test)
278 @safe pure nothrow
279 unittest
280 {
281     import mir.math.sum: Summation;
282     import mir.ndslice.topology: canonical, iota, map;
283     import mir.test: shouldApprox;
284 
285     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
286     auto y = iota([2, 3]).canonical;
287     auto w = iota([2, 3], 1).map!(a => a / 21.0).canonical;
288     x.put(y, w);
289     x.wmean.shouldApprox == 70.0 / 21;
290 }
291 
292 /// Do not assume weights sum to 1
293 version(mir_stat_test)
294 @safe pure nothrow
295 unittest
296 {
297     import mir.math.sum: Summation;
298     import mir.ndslice.slice: sliced;
299     import mir.test: shouldApprox;
300 
301     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
302     x.put([0.0, 1, 2, 3, 4].sliced, [1, 2, 3, 4, 5].sliced);
303     x.wmean.shouldApprox == 40.0 / 15;
304     x.put(5, 6);
305     x.wmean.shouldApprox == 70.0 / 21;
306 }
307 
308 // dynamic array test, do not assume weights sum to 1
309 version(mir_stat_test)
310 @safe pure nothrow
311 unittest
312 {
313     import mir.math.sum: Summation;
314     import mir.test: shouldApprox;
315 
316     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
317     x.put([0.0, 1, 2, 3, 4], [1, 2, 3, 4, 5]);
318     x.wmean.shouldApprox == 40.0 / 15;
319 }
320 
321 // static array test, do not assume weights sum to 1
322 version(mir_stat_test)
323 @safe pure nothrow @nogc
324 unittest
325 {
326     import mir.math.sum: Summation;
327     import mir.test: shouldApprox;
328 
329     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
330     static immutable y = [0.0, 1, 2, 3, 4];
331     static immutable w = [1, 2, 3, 4, 5];
332     x.put(y, w);
333     x.wmean.shouldApprox == 40.0 / 15;
334 }
335 
336 // 2-d slice test, do not assume weights sum to 1
337 version(mir_stat_test)
338 @safe pure
339 unittest
340 {
341     import mir.math.sum: Summation;
342     import mir.ndslice.fuse: fuse;
343     import mir.test: shouldApprox;
344 
345     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
346     auto y = [
347         [0.0, 1, 2],
348         [3.0, 4, 5]
349     ].fuse;
350     auto w = [
351         [1.0, 2, 3],
352         [4.0, 5, 6]
353     ].fuse;
354     x.put(y, w);
355     x.wmean.shouldApprox == 70.0 / 21;
356 }
357 
358 // universal slice test, do not assume weights sum to 1
359 version(mir_stat_test)
360 @safe pure nothrow
361 unittest
362 {
363     import mir.math.sum: Summation;
364     import mir.ndslice.topology: iota, universal;
365     import mir.test: shouldApprox;
366 
367     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
368     auto y = iota(6).universal;
369     auto w = iota([6], 1).universal;
370     x.put(y, w);
371     x.wmean.shouldApprox == 70.0 / 21;
372 }
373 
374 // canonical slice test, do not assume weights sum to 1
375 version(mir_stat_test)
376 @safe pure nothrow
377 unittest
378 {
379     import mir.math.sum: Summation;
380     import mir.ndslice.topology: canonical, iota;
381     import mir.test: shouldApprox;
382 
383     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
384     auto y = iota(6).canonical;
385     auto w = iota([6], 1).canonical;
386     x.put(y, w);
387     x.wmean.shouldApprox == 70.0 / 21;
388 }
389 
390 // 2-d universal slice test, do not assume weights sum to 1
391 version(mir_stat_test)
392 @safe pure nothrow
393 unittest
394 {
395     import mir.math.sum: Summation;
396     import mir.ndslice.topology: iota, universal;
397     import mir.test: shouldApprox;
398 
399     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
400     auto y = iota([2, 3]).universal;
401     auto w = iota([2, 3], 1).universal;
402     x.put(y, w);
403     x.wmean.shouldApprox == 70.0 / 21;
404 }
405 
406 // 2-d canonical slice test, do not assume weights sum to 1
407 version(mir_stat_test)
408 @safe pure nothrow
409 unittest
410 {
411     import mir.math.sum: Summation;
412     import mir.ndslice.topology: canonical, iota;
413     import mir.test: shouldApprox;
414 
415     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
416     auto y = iota([2, 3]).canonical;
417     auto w = iota([2, 3], 1).canonical;
418     x.put(y, w);
419     x.wmean.shouldApprox == 70.0 / 21;
420 }
421 
422 /// Assume no weights, like MeanAccumulator
423 version(mir_stat_test)
424 @safe pure nothrow
425 unittest
426 {
427     import mir.math.sum: Summation;
428     import mir.ndslice.slice: sliced;
429     import mir.test: shouldApprox;
430 
431     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
432     x.put([0.0, 1, 2, 3, 4].sliced);
433     x.wmean.shouldApprox == 2;
434     x.put(5);
435     x.wmean.shouldApprox == 2.5;
436 }
437 
438 // dynamic array test, assume no weights
439 version(mir_stat_test)
440 @safe pure nothrow
441 unittest
442 {
443     import mir.math.sum: Summation;
444     import mir.ndslice.slice: sliced;
445     import mir.test: should;
446 
447     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
448     x.put([0.0, 1, 2, 3, 4]);
449     x.wmean.should == 2;
450 }
451 
452 // static array test, assume no weights
453 version(mir_stat_test)
454 @safe pure nothrow @nogc
455 unittest
456 {
457     import mir.math.sum: Summation;
458     import mir.test: should;
459 
460     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
461     static immutable y = [0.0, 1, 2, 3, 4];
462     x.put(y);
463     x.wmean.should == 2;
464 }
465 
466 // Adding WMeanAccmulators
467 version(mir_stat_test)
468 @safe pure nothrow
469 unittest
470 {
471     import mir.math.sum: Summation;
472     import mir.test: shouldApprox;
473 
474     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25];
475     double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
476     
477     WMeanAccumulator!(float, Summation.pairwise, AssumeWeights.primary) m0;
478     m0.put(x);
479     WMeanAccumulator!(float, Summation.pairwise, AssumeWeights.primary) m1;
480     m1.put(y);
481     m0.put(m1);
482     m0.wmean.shouldApprox == 29.25 / 12;
483 }
484 
485 // repeat test, assume weights sum to 1
486 version(mir_stat_test)
487 @safe pure nothrow
488 unittest
489 {
490     import mir.math.sum: Summation;
491     import mir.ndslice.slice: slicedField;
492     import mir.ndslice.topology: iota, map, repeat;
493     import mir.test: shouldApprox;
494 
495     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
496     auto y = iota(6);
497     auto w = repeat(1.0, 6).map!(a => a / 6.0).slicedField;
498     x.put(y, w);
499     x.wmean.shouldApprox == 15.0 / 6;
500 }
501 
502 // repeat test, do not assume weights sum to 1
503 version(mir_stat_test)
504 @safe pure nothrow
505 unittest
506 {
507     import mir.math.sum: Summation;
508     import mir.ndslice.slice: slicedField;
509     import mir.ndslice.topology: iota, repeat;
510     import mir.test: shouldApprox;
511 
512     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
513     auto y = iota(6);
514     auto w = repeat(1.0, 6).slicedField;
515     x.put(y, w);
516     x.wmean.shouldApprox == 15.0 / 6;
517 }
518 
519 // range test without shape, assume weights sum to 1
520 version(mir_stat_test)
521 @safe pure nothrow
522 unittest
523 {
524     import mir.math.sum: Summation;
525     import std.algorithm: map;
526     import std.range: iota;
527     import mir.test: shouldApprox;
528 
529     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x;
530     auto y = iota(6);
531     auto w = iota(1, 7).map!(a => a / 21.0);
532     x.put(y, w);
533     x.wmean.shouldApprox == 70.0 / 21;
534 }
535 
536 // range test without shape, do not assume weights sum to 1
537 version(mir_stat_test)
538 @safe pure nothrow
539 unittest
540 {
541     import mir.math.sum: Summation;
542     import mir.test: shouldApprox;
543     import std.range: iota;
544 
545     WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x;
546     auto y = iota(6);
547     auto w = iota(1, 7);
548     x.put(y, w);
549     x.wmean.shouldApprox == 70.0 / 21;
550 }
551 
552 // complex test, do not assume weights sum to 1
553 version(mir_stat_test)
554 @safe pure nothrow
555 unittest
556 {
557     import mir.complex;
558     import mir.math.sum: Summation;
559     import mir.test: should;
560     alias C = Complex!double;
561 
562     WMeanAccumulator!(C, Summation.pairwise, AssumeWeights.primary, double) x;
563     x.put([C(1, 3), C(2), C(3)]);
564     x.wmean.should == C(2, 1);
565 }
566 
567 /++
568 Computes the weighted mean of the input.
569 
570 By default, if `F` is not floating point type or complex type, then the result
571 will have a `double` type if `F` is implicitly convertible to a floating point 
572 type or a type for which `isComplex!F` is true.
573 
574 Params:
575     F = controls type of output
576     summation = algorithm for calculating sums (default: Summation.appropriate)
577     assumeWeights = true if weights are assumed to add to 1 (default = AssumeWeights.primary)
578     G = controls the type of weights
579 Returns:
580     The weighted mean of all the elements in the input, must be floating point or complex type
581 
582 See_also: 
583     $(MATHREF sum, Summation),
584     $(SUB2REF univariate, mean),
585     $(SUB2REF univariate, meanType)
586 +/
587 template wmean(F, Summation summation = Summation.appropriate,
588                AssumeWeights assumeWeights = AssumeWeights.primary, 
589                G = F, Summation weightsSummation = Summation.appropriate)
590     if (!is(F : AssumeWeights))
591 {
592     import mir.math.common: fmamath;
593     import mir.ndslice.slice: isConvertibleToSlice;
594     import mir.stat.descriptive.univariate: meanType;
595     import std.traits: isIterable;
596 
597     /++
598     Params:
599         s = slice-like
600         w = weights
601     +/
602     @fmamath meanType!F wmean(SliceA, SliceB)(SliceA s, SliceB w)
603         if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB)
604     {
605         import core.lifetime: move;
606 
607         alias H = typeof(return);
608         WMeanAccumulator!(H, ResolveSummationType!(summation, SliceA, H), assumeWeights, 
609                           G, ResolveSummationType!(weightsSummation, SliceB, G)) wmean;
610         wmean.put(s.move, w.move);
611         return wmean.wmean;
612     }
613 
614     /++
615     Params:
616         r = range, must be finite iterable
617     +/
618     @fmamath meanType!F wmean(Range)(Range r)
619         if (isIterable!Range)
620     {
621         import core.lifetime: move;
622 
623         alias H = typeof(return);
624         WMeanAccumulator!(H, ResolveSummationType!(summation, Range, H), assumeWeights, G, ResolveSummationType!(weightsSummation, Range, G)) wmean;
625         wmean.put(r.move);
626         return wmean.wmean;
627     }
628 }
629 
630 /// ditto
631 template wmean(Summation summation = Summation.appropriate,
632                AssumeWeights assumeWeights = AssumeWeights.primary,
633                Summation weightsSummation = Summation.appropriate)
634 {
635     import mir.math.common: fmamath;
636     import mir.ndslice.slice: isConvertibleToSlice;
637     import mir.stat.descriptive.univariate: meanType;
638     import std.traits: isIterable;
639 
640     /++
641     Params:
642         s = slice-like
643         w = weights
644     +/
645     @fmamath meanType!SliceA wmean(SliceA, SliceB)(SliceA s, SliceB w)
646         if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB)
647     {
648         import core.lifetime: move;
649         import mir.math.sum: sumType;
650 
651         alias F = typeof(return);
652         return .wmean!(F, summation, assumeWeights, sumType!SliceB, weightsSummation)(s.move, w.move);
653     }
654 
655     /++
656     Params:
657         r = range, must be finite iterable
658     +/
659     @fmamath meanType!Range wmean(Range)(Range r)
660         if (isIterable!Range)
661     {
662         import core.lifetime: move;
663 
664         alias F = typeof(return);
665         return .wmean!(F, summation, assumeWeights, F, weightsSummation)(r.move);
666     }
667 }
668 
669 /// ditto
670 template wmean(F, AssumeWeights assumeWeights, Summation summation = Summation.appropriate, 
671                G = F, Summation weightsSummation = Summation.appropriate)
672     if (!is(F : AssumeWeights))
673 {
674     import mir.math.common: fmamath;
675     import mir.ndslice.slice: isConvertibleToSlice;
676     import mir.stat.descriptive.univariate: meanType;
677     import std.traits: isIterable;
678 
679     /++
680     Params:
681         s = slice-like
682         w = weights
683     +/
684     @fmamath meanType!F wmean(SliceA, SliceB)(SliceA s, SliceB w)
685         if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB)
686     {
687         import core.lifetime: move;
688         import mir.math.sum: sumType;
689 
690         alias H = typeof(return);
691         return .wmean!(H, summation, assumeWeights, G, weightsSummation)(s.move, w.move);
692     }
693 
694     /++
695     Params:
696         r = range, must be finite iterable
697     +/
698     @fmamath meanType!Range wmean(Range)(Range r)
699         if (isIterable!Range)
700     {
701         import core.lifetime: move;
702 
703         alias F = typeof(return);
704         return .wmean!(F, summation, assumeWeights, G, weightsSummation)(r.move);
705     }
706 }
707 
708 /// ditto
709 template wmean(F, bool assumeWeights, string summation = "appropriate", 
710                G = F, string weightsSummation = "appropriate")
711     if (!is(F : AssumeWeights))
712 {
713     mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");");
714 }
715 
716 /// ditto
717 template wmean(bool assumeWeights, string summation = "appropriate",
718                string weightsSummation = "appropriate")
719 {
720     mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");");
721 }
722 
723 /// ditto
724 template wmean(F, string summation, bool assumeWeights = false,
725                G = F, string weightsSummation = "appropriate")
726     if (!is(F : AssumeWeights))
727 {
728     mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");");
729 }
730 
731 /// ditto
732 template wmean(string summation, bool assumeWeights = false,
733                string weightsSummation = "appropriate")
734 {
735     mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");");
736 }
737 
738 /// ditto
739 template wmean(F, string summation, G, string weightsSummation, bool assumeWeights)
740     if (!is(F : AssumeWeights))
741 {
742     mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");");
743 }
744 
745 /// ditto
746 template wmean(string summation, string weightsSummation, bool assumeWeights = false)
747 {
748     mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");");
749 }
750 
751 ///
752 version(mir_stat_test)
753 @safe pure nothrow
754 unittest
755 {
756     import mir.complex;
757     import mir.ndslice.slice: sliced;
758     import mir.test: should, shouldApprox;
759     alias C = Complex!double;
760 
761     wmean([1.0, 2, 3], [1, 2, 3]).shouldApprox == (1.0 + 4.0 + 9.0) / 6;
762     wmean!true([1.0, 2, 3], [1.0 / 6, 2.0 / 6, 3.0 / 6]).shouldApprox == (1.0 + 4.0 + 9.0) / 6;
763     wmean([C(1, 3), C(2), C(3)], [1, 2, 3]).should == C(14.0 / 6, 3.0 / 6);
764 
765     wmean!float([0, 1, 2, 3, 4, 5].sliced(3, 2), [1, 2, 3, 4, 5, 6].sliced(3, 2)).shouldApprox == 70.0 / 21;
766 
767     static assert(is(typeof(wmean!float([1, 2, 3], [1, 2, 3])) == float));
768 }
769 
770 /// If weights are not provided, then behaves like mean
771 version(mir_stat_test)
772 @safe pure nothrow
773 unittest
774 {
775     import mir.complex;
776     import mir.ndslice.slice: sliced;
777     import mir.test: should;
778     alias C = Complex!double;
779 
780     wmean([1.0, 2, 3]).should == 2;
781     wmean([C(1, 3), C(2), C(3)]).should == C(2, 1);
782 
783     wmean!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).should == 2.5;
784 
785     static assert(is(typeof(wmean!float([1, 2, 3])) == float));
786 }
787 
788 /// Weighted mean of vector
789 version(mir_stat_test)
790 @safe pure nothrow
791 unittest
792 {
793     import mir.ndslice.slice: sliced;
794     import mir.ndslice.topology: iota, map;
795     import mir.test: shouldApprox;
796 
797     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
798               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
799     auto w = iota([12], 1);
800     auto w_SumToOne = w.map!(a => a / 78.0);
801 
802     x.wmean.shouldApprox == 29.25 / 12;
803     x.wmean(w).shouldApprox == 203.0 / 78;
804     x.wmean!true(w_SumToOne).shouldApprox == 203.0 / 78;
805 }
806 
807 /// Weighted mean of matrix
808 version(mir_stat_test)
809 @safe pure
810 unittest
811 {
812     import mir.ndslice.fuse: fuse;
813     import mir.ndslice.topology: iota, map;
814     import mir.test: shouldApprox;
815 
816     auto x = [
817         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
818         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
819     ].fuse;
820     auto w = iota([2, 6], 1);
821     auto w_SumToOne = w.map!(a => a / 78.0);
822 
823     x.wmean.shouldApprox == 29.25 / 12;
824     x.wmean(w).shouldApprox == 203.0 / 78;
825     x.wmean!true(w_SumToOne).shouldApprox == 203.0 / 78;
826 }
827 
828 /// Column mean of matrix
829 version(mir_stat_test)
830 @safe pure
831 unittest
832 {
833     import mir.algorithm.iteration: all;
834     import mir.math.common: approxEqual;
835     import mir.ndslice.fuse: fuse;
836     import mir.ndslice.topology: alongDim, byDim, iota, map, universal;
837 
838     auto x = [
839         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
840         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
841     ].fuse;
842     auto w = iota([2], 1).universal;
843     auto result = [4.0 / 3, 16.0 / 3, 11.5 / 3, 4.0 / 3, 6.5 / 3, 4.25 / 3];
844 
845     // Use byDim or alongDim with map to compute mean of row/column.
846     assert(x.byDim!1.map!(a => a.wmean(w)).all!approxEqual(result));
847     assert(x.alongDim!0.map!(a => a.wmean(w)).all!approxEqual(result));
848 
849     // FIXME
850     // Without using map, computes the mean of the whole slice
851     // assert(x.byDim!1.wmean(w) == x.sliced.wmean);
852     // assert(x.alongDim!0.wmean(w) == x.sliced.wmean);
853 }
854 
855 /// Can also set algorithm or output type
856 version(mir_stat_test)
857 @safe pure nothrow
858 unittest
859 {
860     import mir.ndslice.slice: sliced;
861     import mir.ndslice.topology: repeat, universal;
862     import mir.test: shouldApprox;
863 
864     //Set sum algorithm (also for weights) or output type
865 
866     auto a = [1, 1e100, 1, -1e100].sliced;
867 
868     auto x = a * 10_000;
869     auto w1 = [1, 1, 1, 1].sliced;
870     auto w2 = [0.25, 0.25, 0.25, 0.25].sliced;
871 
872     x.wmean!"kbn"(w1).shouldApprox == 20_000 / 4;
873     x.wmean!(true, "kbn")(w2).shouldApprox == 20_000 / 4;
874     x.wmean!("kbn", true)(w2).shouldApprox == 20_000 / 4;
875     x.wmean!("kbn", true, "pairwise")(w2).shouldApprox == 20_000 / 4;
876     x.wmean!(true, "kbn", "pairwise")(w2).shouldApprox == 20_000 / 4;
877     x.wmean!"kb2"(w1).shouldApprox == 20_000 / 4;
878     x.wmean!"precise"(w1).shouldApprox == 20_000 / 4;
879     x.wmean!(double, "precise")(w1).shouldApprox == 20_000.0 / 4;
880 
881     auto y = uint.max.repeat(3);
882     y.wmean!ulong([1, 1, 1].sliced.universal).shouldApprox == 12884901885 / 3;
883 }
884 
885 /++
886 For integral slices, can pass output type as template parameter to ensure output
887 type is correct.
888 +/
889 version(mir_stat_test)
890 @safe pure nothrow
891 unittest
892 {
893     import mir.math.common: approxEqual;
894     import mir.ndslice.slice: sliced;
895     import mir.test: shouldApprox;
896 
897     auto x = [0, 1, 1, 2, 4, 4,
898               2, 7, 5, 1, 2, 0].sliced;
899     auto w = [1, 2, 3,  4,  5,  6,
900               7, 8, 9, 10, 11, 12].sliced;
901 
902     auto y = x.wmean(w);
903     y.shouldApprox(1.0e-10) == 204.0 / 78;
904     static assert(is(typeof(y) == double));
905 
906     x.wmean!float(w).shouldApprox(1.0e-10) == 204f / 78;
907 }
908 
909 /++
910 Mean works for complex numbers and other user-defined types (provided they
911 can be converted to a floating point or complex type)
912 +/
913 version(mir_test_weighted)
914 @safe pure nothrow
915 unittest
916 {
917     import mir.complex;
918     import mir.ndslice.slice: sliced;
919     import mir.test: should;
920     alias C = Complex!double;
921 
922     auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
923     auto w = [1, 2, 3, 4].sliced;
924     x.wmean(w).should == C(3, 4);
925 }
926 
927 /// Compute weighted mean tensors along specified dimention of tensors
928 version(mir_stat_test)
929 @safe pure
930 unittest
931 {
932     import mir.ndslice.fuse: fuse;
933     import mir.ndslice.slice: sliced;
934     import mir.ndslice.topology: alongDim, as, iota, map, universal;
935     /++
936       [[0,1,2],
937        [3,4,5]]
938      +/
939     auto x = [
940         [0, 1, 2],
941         [3, 4, 5]
942     ].fuse.as!double;
943     auto w = [
944         [1, 2, 3],
945         [4, 5, 6]
946     ].fuse;
947     auto w1 = [1, 2].sliced.universal;
948     auto w2 = [1, 2, 3].sliced;
949 
950     assert(x.wmean(w) == (70.0 / 21));
951 
952     auto m0 = [(0.0 + 6.0) / 3, (1.0 + 8.0) / 3, (2.0 + 10.0) / 3];
953     assert(x.alongDim!0.map!(a => a.wmean(w1)) == m0);
954     assert(x.alongDim!(-2).map!(a => a.wmean(w1)) == m0);
955 
956     auto m1 = [(0.0 + 2.0 + 6.0) / 6, (3.0 + 8.0 + 15.0) / 6];
957     assert(x.alongDim!1.map!(a => a.wmean(w2)) == m1);
958     assert(x.alongDim!(-1).map!(a => a.wmean(w2)) == m1);
959 
960     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!wmean == iota([3, 4, 5], 3 * 4 * 5 / 2));
961 }
962 
963 // test chaining
964 version(mir_stat_test)
965 @safe pure nothrow
966 unittest
967 {
968     import mir.test: should;
969     [1.0, 2, 3, 4].wmean.should == 2.5;
970 }
971 
972 // additional alongDim tests
973 version(mir_stat_test)
974 @safe pure nothrow
975 unittest
976 {
977     import mir.algorithm.iteration: all;
978     import mir.stat.descriptive.univariate: meanType;
979     import mir.ndslice.topology: iota, alongDim, map;
980 
981     auto x = iota([2, 2], 1);
982     auto w = iota([2], 2);
983     auto y = x.alongDim!1.map!(a => a.wmean(w));
984     static assert(is(meanType!(typeof(y)) == double));
985 }
986 
987 // @nogc test
988 version(mir_stat_test)
989 @safe pure nothrow @nogc
990 unittest
991 {
992     import mir.ndslice.slice: sliced;
993     import mir.test: shouldApprox;
994 
995     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
996                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
997     static immutable w = [1.0, 2, 3,  4,  5,  6,
998                             7, 8, 9, 10, 11, 12];
999 
1000     x.wmean.shouldApprox == 29.25 / 12;
1001     x.wmean(w).shouldApprox == 203.0 / 78;
1002 }
1003 
1004 /++
1005 Output range for wsum.
1006 +/
1007 struct WSummator(T, Summation summation, U = T)
1008 {
1009     import mir.ndslice.slice: isConvertibleToSlice, isSlice, kindOf;
1010     import std.range.primitives: isInputRange;
1011     import std.traits: isIterable;
1012 
1013     ///
1014     Summator!(T, summation) wsummator;
1015 
1016     ///
1017     F wsum(F = T)() const @safe @property pure nothrow @nogc
1018     {
1019         return cast(F) wsummator.sum;
1020     }
1021 
1022     ///
1023     void put(Slice1, Slice2)(Slice1 s, Slice2 w)
1024         if (isSlice!Slice1 && isSlice!Slice2)
1025     {
1026         static assert (Slice1.N == Slice2.N, "s and w must have the same number of dimensions");
1027         static assert (kindOf!Slice1 == kindOf!Slice2, "s and w must have the same kind");
1028 
1029         import mir.ndslice.slice: Contiguous;
1030         import mir.ndslice.topology: zip, map;
1031 
1032         assert(s._lengths == w._lengths, "WMeanAcumulator.put: both slices must have the same lengths");
1033 
1034         static if (kindOf!Slice1 != Contiguous && Slice1.N > 1) {
1035             assert(s.strides == w.strides, "WMeanAccumulator.put: cannot put canonical and universal slices when strides do not match");
1036             auto combine = s.zip!true(w);
1037         } else {
1038             auto combine = s.zip!false(w);
1039         }
1040 
1041         auto combine2 = combine.map!"a * b";
1042         wsummator.put(combine2);
1043     }
1044 
1045     ///
1046     void put(SliceLike1, SliceLike2)(SliceLike1 s, SliceLike2 w)
1047         if (isConvertibleToSlice!SliceLike1 && !isSlice!SliceLike1 &&
1048             isConvertibleToSlice!SliceLike2 && !isSlice!SliceLike2)
1049     {
1050         import mir.ndslice.slice: toSlice;
1051         this.put(s.toSlice, w.toSlice);
1052     }
1053 
1054     ///
1055     void put(Range)(Range r)
1056         if (isIterable!Range)
1057     {
1058         wsummator.put(r);
1059     }
1060 
1061     ///
1062     void put(RangeA, RangeB)(RangeA r, RangeB w)
1063         if (isInputRange!RangeA && !isConvertibleToSlice!RangeA &&
1064             isInputRange!RangeB && !isConvertibleToSlice!RangeB)
1065     {
1066         do
1067         {
1068             assert(!(!r.empty && w.empty) && !(r.empty && !w.empty),
1069                    "r and w must both be empty at the same time, one cannot be empty while the other has remaining items");
1070             this.put(r.front, w.front);
1071             r.popFront;
1072             w.popFront;
1073         } while(!r.empty || !w.empty); // Using an || instead of && so that the loop does not end early. mis-matched lengths of r and w sould be caught by above assert
1074     }
1075 
1076     ///
1077     void put()(T x, U w)
1078     {
1079         wsummator.put(x * w);
1080     }
1081 
1082     ///
1083     void put()(T x)
1084     {
1085         wsummator.put(x);
1086     }
1087 
1088     ///
1089     void put(F = T, G = U)(WSummator!(F, summation, G) wm)
1090     {
1091         wsummator.put(cast(T) wm.wsummator);
1092     }
1093 }
1094 
1095 ///
1096 version(mir_stat_test)
1097 @safe pure nothrow
1098 unittest
1099 {
1100     import mir.math.sum: Summation;
1101     import mir.ndslice.slice: sliced;
1102     import mir.test: should;
1103 
1104     WSummator!(double, Summation.pairwise) x;
1105     x.put([0.0, 1, 2, 3, 4].sliced, [1, 2, 3, 4, 5].sliced);
1106     x.wsum.should == 40;
1107     x.put(5, 6);
1108     x.wsum.should == 70;
1109 }
1110 
1111 // dynamic array test
1112 version(mir_stat_test)
1113 @safe pure nothrow
1114 unittest
1115 {
1116     import mir.math.sum: Summation;
1117     import mir.test: should;
1118 
1119     WSummator!(double, Summation.pairwise) x;
1120     x.put([0.0, 1, 2, 3, 4], [1, 2, 3, 4, 5]);
1121     x.wsum.should == 40;
1122 }
1123 
1124 // static array test
1125 version(mir_stat_test)
1126 @safe pure nothrow @nogc
1127 unittest
1128 {
1129     import mir.math.sum: Summation;
1130     import mir.test: should;
1131 
1132     WSummator!(double, Summation.pairwise) x;
1133     static immutable y = [0.0, 1, 2, 3, 4];
1134     static immutable w = [1, 2, 3, 4, 5];
1135     x.put(y, w);
1136     x.wsum.should == 40;
1137 }
1138 
1139 // 2-d slice test
1140 version(mir_stat_test)
1141 @safe pure
1142 unittest
1143 {
1144     import mir.math.sum: Summation;
1145     import mir.ndslice.fuse: fuse;
1146     import mir.test: should;
1147 
1148     WSummator!(double, Summation.pairwise) x;
1149     auto y = [
1150         [0.0, 1, 2],
1151         [3.0, 4, 5]
1152     ].fuse;
1153     auto w = [
1154         [1.0, 2, 3],
1155         [4.0, 5, 6]
1156     ].fuse;
1157     x.put(y, w);
1158     x.wsum.should == 70;
1159 }
1160 
1161 // universal slice test
1162 version(mir_stat_test)
1163 @safe pure nothrow
1164 unittest
1165 {
1166     import mir.math.sum: Summation;
1167     import mir.ndslice.topology: iota, universal;
1168     import mir.test: should;
1169 
1170     WSummator!(double, Summation.pairwise) x;
1171     auto y = iota(6).universal;
1172     auto w = iota([6], 1).universal;
1173     x.put(y, w);
1174     x.wsum.should == 70;
1175 }
1176 
1177 // canonical slice test
1178 version(mir_stat_test)
1179 @safe pure nothrow
1180 unittest
1181 {
1182     import mir.math.sum: Summation;
1183     import mir.ndslice.topology: canonical, iota;
1184     import mir.test: should;
1185 
1186     WSummator!(double, Summation.pairwise) x;
1187     auto y = iota(6).canonical;
1188     auto w = iota([6], 1).canonical;
1189     x.put(y, w);
1190     x.wsum.should == 70;
1191 }
1192 
1193 // 2-d universal slice test
1194 version(mir_stat_test)
1195 @safe pure nothrow
1196 unittest
1197 {
1198     import mir.math.sum: Summation;
1199     import mir.ndslice.topology: iota, universal;
1200     import mir.test: should;
1201 
1202     WSummator!(double, Summation.pairwise) x;
1203     auto y = iota([2, 3]).universal;
1204     auto w = iota([2, 3], 1).universal;
1205     x.put(y, w);
1206     x.wsum.should == 70;
1207 }
1208 
1209 // 2-d canonical slice test
1210 version(mir_stat_test)
1211 @safe pure nothrow
1212 unittest
1213 {
1214     import mir.math.sum: Summation;
1215     import mir.ndslice.topology: canonical, iota;
1216     import mir.test: should;
1217 
1218     WSummator!(double, Summation.pairwise) x;
1219     auto y = iota([2, 3]).canonical;
1220     auto w = iota([2, 3], 1).canonical;
1221     x.put(y, w);
1222     x.wsum.should == 70;
1223 }
1224 
1225 /// Assume no weights, like Summator
1226 version(mir_stat_test)
1227 @safe pure nothrow
1228 unittest
1229 {
1230     import mir.math.sum: Summation;
1231     import mir.ndslice.slice: sliced;
1232     import mir.test: should;
1233 
1234     WSummator!(double, Summation.pairwise) x;
1235     x.put([0.0, 1, 2, 3, 4].sliced);
1236     x.wsum.should == 10;
1237     x.put(5);
1238     x.wsum.should == 15;
1239 }
1240 
1241 // dynamic array test, assume no weights
1242 version(mir_stat_test)
1243 @safe pure nothrow
1244 unittest
1245 {
1246     import mir.math.sum: Summation;
1247     import mir.test: should;
1248 
1249     WSummator!(double, Summation.pairwise) x;
1250     x.put([0.0, 1, 2, 3, 4]);
1251     x.wsum.should == 10;
1252 }
1253 
1254 // static array test, assume no weights
1255 version(mir_stat_test)
1256 @safe pure nothrow @nogc
1257 unittest
1258 {
1259     import mir.math.sum: Summation;
1260     import mir.test: should;
1261 
1262     WSummator!(double, Summation.pairwise) x;
1263     static immutable y = [0.0, 1, 2, 3, 4];
1264     x.put(y);
1265     x.wsum.should == 10;
1266 }
1267 
1268 // Adding WSummators
1269 version(mir_stat_test)
1270 @safe pure nothrow
1271 unittest
1272 {
1273     import mir.math.sum: Summation;
1274     import mir.test: should;
1275 
1276     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25];
1277     double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
1278 
1279     WSummator!(float, Summation.pairwise) m0;
1280     m0.put(x);
1281     WSummator!(float, Summation.pairwise) m1;
1282     m1.put(y);
1283     m0.put(m1);
1284     m0.wsum.should == 29.25;
1285 }
1286 
1287 // repeat test
1288 version(mir_stat_test)
1289 @safe pure nothrow
1290 unittest
1291 {
1292     import mir.math.sum: Summation;
1293     import mir.ndslice.slice: slicedField;
1294     import mir.ndslice.topology: iota, repeat;
1295     import mir.test: should;
1296 
1297     WSummator!(double, Summation.pairwise) x;
1298     auto y = iota(6);
1299     auto w = repeat(1.0, 6).slicedField;
1300     x.put(y, w);
1301     x.wsum.should == 15;
1302 }
1303 
1304 // range test without shape
1305 version(mir_stat_test)
1306 @safe pure nothrow
1307 unittest
1308 {
1309     import mir.math.sum: Summation;
1310     import mir.test: should;
1311     import std.range: iota;
1312 
1313     WSummator!(double, Summation.pairwise) x;
1314     auto y = iota(6);
1315     auto w = iota(1, 7);
1316     x.put(y, w);
1317     x.wsum.should == 70;
1318 }
1319 
1320 // complex test
1321 version(mir_stat_test)
1322 @safe pure nothrow
1323 unittest
1324 {
1325     import mir.math.sum: Summation;
1326     import mir.complex;
1327     import mir.test: should;
1328     alias C = Complex!double;
1329 
1330     WSummator!(C, Summation.pairwise, double) x;
1331     x.put([C(1, 3), C(2), C(3)]);
1332     x.wsum.should == C(6, 3);
1333 }
1334 
1335 /++
1336 Computes the weighted sum of the input.
1337 
1338 Params:
1339     F = controls type of output
1340     summation = algorithm for calculating sums (default: Summation.appropriate)
1341     G = controls the type of weights
1342 Returns:
1343     The weighted sum of all the elements in the input
1344 
1345 See_also: 
1346     $(MATHREF sum, Summation)
1347 +/
1348 template wsum(F, Summation summation = Summation.appropriate, G = F)
1349 {
1350     import mir.math.common: fmamath;
1351     import mir.math.sum: sumType;
1352     import mir.ndslice.slice: isConvertibleToSlice;
1353     import std.traits: isIterable;
1354 
1355     /++
1356     Params:
1357         s = slice-like
1358         w = weights
1359     +/
1360     @fmamath sumType!F wsum(SliceA, SliceB)(SliceA s, SliceB w)
1361         if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB)
1362     {
1363         import core.lifetime: move;
1364 
1365         alias H = typeof(return);
1366         WSummator!(H, ResolveSummationType!(summation, SliceA, H), G) wsum;
1367         wsum.put(s.move, w.move);
1368         return wsum.wsum;
1369     }
1370 
1371     /++
1372     Params:
1373         r = range, must be finite iterable
1374     +/
1375     @fmamath sumType!F wsum(Range)(Range r)
1376         if (isIterable!Range)
1377     {
1378         import core.lifetime: move;
1379 
1380         alias H = typeof(return);
1381         WSummator!(H, ResolveSummationType!(summation, Range, H), G) wsum;
1382         wsum.put(r.move);
1383         return wsum.wsum;
1384     }
1385 }
1386 
1387 /// ditto
1388 template wsum(Summation summation = Summation.appropriate)
1389 {
1390     import mir.math.common: fmamath;
1391     import mir.math.sum: sumType;
1392     import mir.ndslice.slice: isConvertibleToSlice;
1393     import std.traits: isIterable;
1394 
1395     /++
1396     Params:
1397         s = slice-like
1398         w = weights
1399     +/
1400     @fmamath sumType!SliceA wsum(SliceA, SliceB)(SliceA s, SliceB w)
1401         if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB)
1402     {
1403         import core.lifetime: move;
1404         import mir.math.sum: sumType;
1405 
1406         alias F = typeof(return);
1407         return .wsum!(F, summation, sumType!SliceB)(s.move, w.move);
1408     }
1409 
1410     /++
1411     Params:
1412         r = range, must be finite iterable
1413     +/
1414     @fmamath sumType!Range wsum(Range)(Range r)
1415         if (isIterable!Range)
1416     {
1417         import core.lifetime: move;
1418 
1419         alias F = typeof(return);
1420         return .wsum!(F, summation, F)(r.move);
1421     }
1422 }
1423 
1424 /// ditto
1425 template wsum(F, string summation, G = F)
1426 {
1427     mixin("alias wsum = .wsum!(F, Summation." ~ summation ~ ", G);");
1428 }
1429 
1430 /// ditto
1431 template wsum(string summation)
1432 {
1433     mixin("alias wsum = .wsum!(Summation." ~ summation ~ ");");
1434 }
1435 
1436 ///
1437 version(mir_stat_test)
1438 @safe pure nothrow
1439 unittest
1440 {
1441     import mir.complex;
1442     import mir.math.common: approxEqual;
1443     import mir.ndslice.slice: sliced;
1444     import mir.test: should;
1445     alias C = Complex!double;
1446 
1447     wsum([1, 2, 3], [1, 2, 3]).should == (1 + 4 + 9);
1448     wsum([C(1, 3), C(2), C(3)], [1, 2, 3]).should == C((1 + 4 + 9), 3);
1449 
1450     wsum!float([0, 1, 2, 3, 4, 5].sliced(3, 2), [1, 2, 3, 4, 5, 6].sliced(3, 2)).should == 70;
1451 
1452     static assert(is(typeof(wmean!float([1, 2, 3], [1, 2, 3])) == float));
1453 }
1454 
1455 /// If weights are not provided, then behaves like sum
1456 version(mir_stat_test)
1457 @safe pure nothrow
1458 unittest
1459 {
1460     import mir.complex;
1461     import mir.ndslice.slice: sliced;
1462     import mir.test: should;
1463     alias C = Complex!double;
1464 
1465     wsum([1.0, 2, 3]).should == 6;
1466     wsum([C(1, 3), C(2), C(3)]).should == C(6, 3);
1467 
1468     wsum!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).should == 15;
1469 
1470     static assert(is(typeof(wsum!float([1, 2, 3])) == float));
1471 }
1472 
1473 /// Weighted sum of vector
1474 version(mir_stat_test)
1475 @safe pure nothrow
1476 unittest
1477 {
1478     import mir.ndslice.slice: sliced;
1479     import mir.ndslice.topology: iota, map;
1480     import mir.test: should;
1481 
1482     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1483               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
1484     auto w = iota([12], 1);
1485 
1486     x.wsum.should == 29.25;
1487     x.wsum(w).should == 203;
1488 }
1489 
1490 /// Weighted sum of matrix
1491 version(mir_stat_test)
1492 @safe pure
1493 unittest
1494 {
1495     import mir.ndslice.fuse: fuse;
1496     import mir.ndslice.topology: iota;
1497     import mir.test: should;
1498 
1499     auto x = [
1500         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
1501         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
1502     ].fuse;
1503     auto w = iota([2, 6], 1);
1504 
1505     x.wsum.should == 29.25;
1506     x.wsum(w).should == 203;
1507 }
1508 
1509 /// Column sum of matrix
1510 version(mir_stat_test)
1511 @safe pure
1512 unittest
1513 {
1514     import mir.algorithm.iteration: all;
1515     import mir.math.common: approxEqual;
1516     import mir.ndslice.fuse: fuse;
1517     import mir.ndslice.topology: alongDim, byDim, iota, map, universal;
1518 
1519     auto x = [
1520         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
1521         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
1522     ].fuse;
1523     auto w = iota([2], 1).universal;
1524     auto result = [4, 16, 11.5, 4, 6.5, 4.25];
1525 
1526     // Use byDim or alongDim with map to compute sum of row/column.
1527     assert(x.byDim!1.map!(a => a.wsum(w)).all!approxEqual(result));
1528     assert(x.alongDim!0.map!(a => a.wsum(w)).all!approxEqual(result));
1529 
1530     // FIXME
1531     // Without using map, computes the sum of the whole slice
1532     // assert(x.byDim!1.wsum(w) == x.sliced.wsum);
1533     // assert(x.alongDim!0.wsum(w) == x.sliced.wsum);
1534 }
1535 
1536 /// Can also set algorithm or output type
1537 version(mir_stat_test)
1538 @safe pure nothrow
1539 unittest
1540 {
1541     import mir.ndslice.slice: sliced;
1542     import mir.ndslice.topology: repeat, universal;
1543     import mir.test: should;
1544 
1545     //Set sum algorithm (also for weights) or output type
1546 
1547     auto a = [1, 1e100, 1, -1e100].sliced;
1548 
1549     auto x = a * 10_000;
1550     auto w1 = [1, 1, 1, 1].sliced;
1551     auto w2 = [0.25, 0.25, 0.25, 0.25].sliced;
1552 
1553     x.wsum!"kbn"(w1).should == 20_000;
1554     x.wsum!"kbn"(w2).should == 20_000 / 4;
1555     x.wsum!"kb2"(w1).should == 20_000;
1556     x.wsum!"precise"(w1).should == 20_000;
1557     x.wsum!(double, "precise")(w1).should == 20_000;
1558 
1559     auto y = uint.max.repeat(3);
1560     y.wsum!ulong([1, 1, 1].sliced.universal).should == 12884901885;
1561 }
1562 
1563 /++
1564 wsum works for complex numbers and other user-defined types
1565 +/
1566 version(mir_test_weighted)
1567 @safe pure nothrow
1568 unittest
1569 {
1570     import mir.complex;
1571     import mir.ndslice.slice: sliced;
1572     import mir.test: should;
1573     alias C = Complex!double;
1574 
1575     auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
1576     auto w = [1, 2, 3, 4].sliced;
1577     x.wsum(w).should == C(30, 40);
1578 }
1579 
1580 /// Compute weighted sum tensors along specified dimention of tensors
1581 version(mir_stat_test)
1582 @safe pure
1583 unittest
1584 {
1585     import mir.ndslice.fuse: fuse;
1586     import mir.ndslice.slice: sliced;
1587     import mir.ndslice.topology: alongDim, as, iota, map, universal;
1588     /++
1589       [[0,1,2],
1590        [3,4,5]]
1591      +/
1592     auto x = [
1593         [0, 1, 2],
1594         [3, 4, 5]
1595     ].fuse.as!double;
1596     auto w = [
1597         [1, 2, 3],
1598         [4, 5, 6]
1599     ].fuse;
1600     auto w1 = [1, 2].sliced.universal;
1601     auto w2 = [1, 2, 3].sliced;
1602 
1603     assert(x.wsum(w) == 70);
1604 
1605     auto m0 = [(0 + 6), (1 + 8), (2 + 10)];
1606     assert(x.alongDim!0.map!(a => a.wsum(w1)) == m0);
1607     assert(x.alongDim!(-2).map!(a => a.wsum(w1)) == m0);
1608 
1609     auto m1 = [(0 + 2 + 6), (3 + 8 + 15)];
1610     assert(x.alongDim!1.map!(a => a.wsum(w2)) == m1);
1611     assert(x.alongDim!(-1).map!(a => a.wsum(w2)) == m1);
1612 }
1613 
1614 // test chaining
1615 version(mir_stat_test)
1616 @safe pure nothrow
1617 unittest
1618 {
1619     import mir.test: should;
1620     [1.0, 2, 3, 4].wsum.should == 10;
1621 }
1622 
1623 // additional alongDim tests
1624 version(mir_stat_test)
1625 @safe pure nothrow
1626 unittest
1627 {
1628     import mir.math.sum: sumType;
1629     import mir.ndslice.topology: iota, alongDim, map;
1630 
1631     auto x = iota([2, 2], 1);
1632     auto w = iota([2], 2);
1633     auto y = x.alongDim!1.map!(a => a.wsum(w));
1634     static assert(is(sumType!(typeof(y)) == long));
1635 }
1636 
1637 // @nogc test
1638 version(mir_stat_test)
1639 @safe pure nothrow @nogc
1640 unittest
1641 {
1642     import mir.test: should;
1643 
1644     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1645                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
1646     static immutable w = [1.0, 2, 3,  4,  5,  6,
1647                             7, 8, 9, 10, 11, 12];
1648 
1649     x.wsum.should == 29.25;
1650     x.wsum(w).should == 203.0;
1651 }