1 /++
2 This is a submodule of $(MREF mir,ndslice).
3 
4 The module contains $(LREF _chunks) routine.
5 $(LREF Chunks) structure is multidimensional random access range with slicing.
6 
7 $(SUBREF slice, slicedField), $(SUBREF slice, slicedNdField) can be used to construct ndslice view on top of $(LREF Chunks).
8 
9 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
10 Copyright: 2020 Ilya Yaroshenko, Kaleidic Associates Advisory Limited, Symmetry Investments
11 Authors: Ilya Yaroshenko
12 
13 Macros:
14 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, ndslice, $1)$(NBSP)
15 T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
16 +/
17 module mir.ndslice.chunks;
18 
19 import mir.internal.utility;
20 import mir.math.common: optmath;
21 import mir.ndslice.internal;
22 import mir.ndslice.iterator: IotaIterator;
23 import mir.ndslice.slice;
24 
25 import std.meta;
26 import std.traits;
27 
28 /++
29 Creates $(LREF Chunks).
30 
31 Params:
32     Dimensions = compile time list of dimensions to chunk
33 
34 See_also: $(SUBREF topology, blocks) $(SUBREF fuse, fuseCells)
35 +/
36 template chunks(Dimensions...)
37     if (Dimensions.length)
38 {
39     static if (allSatisfy!(isSize_t, Dimensions))
40     /++
41     Params:
42         slice = Slice to chunk.
43         chunkLengths = Chunk shape. It must not have a zero length.
44     Returns: $(LREF Chunks).
45     +/
46     Chunks!([Dimensions], Iterator, N, kind == Contiguous && [Dimensions] != [0] ? Canonical : kind)
47     chunks(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice, size_t[Dimensions.length] chunkLengths...)
48     {
49         static if (kindOf!(typeof(typeof(return).init._slice)) != kind)
50         {
51             import mir.ndslice.topology: canonical;
52             auto p = slice.canonical;
53         }
54         else
55         {
56             alias p = slice;
57         }
58         auto ret = typeof(return)(chunkLengths, p);
59         foreach (i; Iota!(Dimensions.length))
60             ret._norm!i;
61         return ret;
62     }
63     else
64         alias chunks = .chunks!(staticMap!(toSize_t, Dimensions));
65 }
66 
67 /// ditto
68 Chunks!([0], Iterator, N, kind) chunks(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice, size_t chunkLength)
69 {
70     return .chunks!0(slice, chunkLength);
71 }
72 
73 
74 /// 1Dx1D
75 @safe pure nothrow @nogc version(mir_test) unittest
76 {
77     import mir.ndslice.chunks: chunks, isChunks;
78     import mir.ndslice.topology: iota;
79 
80     // 0 1 2 3 4 5 6 7 8 9 10
81     auto sl = iota(11);
82     // 0 1 2 | 3 4 5 | 6 7 8 | 9 10
83     auto ch = sl.chunks(3);
84 
85     static assert(isChunks!(typeof(ch)) == [0]); // isChunks returns dimension indices
86 
87     assert(ch.length == 4);
88     assert(ch.shape == cast(size_t[1])[4]);
89 
90     // 0 1 2
91     assert(ch.front == iota([3], 0));
92     ch.popFront;
93 
94     // 3 4 5
95     assert(ch.front == iota([3], 3));
96     assert(ch.length == 3);
97 
98     // 9 10
99     assert(ch[$ - 1] == ch.back);
100     assert(ch.back == iota([2], 9));
101 
102     ch.popBack;
103     assert(ch.back == iota([3], 6));
104 
105     assert(ch[$ - 1 .. $].length == 1);
106     assert(ch[$ .. $].length == 0);
107     assert(ch[0 ..  0].empty);
108 
109     import std.range.primitives: isRandomAccessRange;
110     static assert(isRandomAccessRange!(typeof(ch)));
111 }
112 
113 /// 2Dx2D
114 @safe pure nothrow
115 version(mir_test) unittest
116 {
117     import mir.ndslice.chunks: chunks, isChunks;
118     import mir.ndslice.topology: iota;
119 
120     //   0   1   2   3   4   5   6   7   8   9
121     //  10  11  12  13  14  15  16  17  18  19
122     //  20  21  22  23  24  25  26  27  28  29
123     //  30  31  32  33  34  35  36  37  38  39
124     //  40  41  42  43  44  45  46  47  48  49
125     //  50  51  52  53  54  55  56  57  58  59
126     //  60  61  62  63  64  65  66  67  68  69
127     //  70  71  72  73  74  75  76  77  78  79
128     //  80  81  82  83  84  85  86  87  88  89
129     //  90  91  92  93  94  95  96  97  98  99
130     // 100 101 102 103 104 105 106 107 108 109
131     auto sl = iota(11, 10); // [0, 1, .. 10]
132 
133     //   ----------------   ----------------   --------
134     //  |  0   1   2   3 | |  4   5   6   7 | |  8   9 |
135     //  | 10  11  12  13 | | 14  15  16  17 | | 18  19 |
136     //  | 20  21  22  23 | | 24  25  26  27 | | 28  29 |
137     //  |----------------| |----------------| |--------|
138     //  | 30  31  32  33 | | 34  35  36  37 | | 38  39 |
139     //  | 40  41  42  43 | | 44  45  46  47 | | 48  49 |
140     //  | 50  51  52  53 | | 54  55  56  57 | | 58  59 |
141     //  |----------------| |----------------| |--------|
142     //  | 60  61  62  63 | | 64  65  66  67 | | 68  69 |
143     //  | 70  71  72  73 | | 74  75  76  77 | | 78  79 |
144     //  | 80  81  82  83 | | 84  85  86  87 | | 88  89 |
145     //  |----------------| |----------------| |--------|
146     //  | 90  91  92  93 | | 94  95  96  97 | | 98  99 |
147     //  |100 101 102 103 | |104 105 106 107 | |108 109 |
148     //   ----------------   ----------------   --------
149     // Chunk columns first, then blocks rows.
150     auto ch = sl.chunks!(1, 0)(4, 3);
151 
152     assert(ch.shape == [3, 4]);
153     assert(ch.slice == sl);
154     assert(ch.front.slice == sl[0 .. $, 0 .. 4]);
155 
156     assert(ch.front.front == sl[0 .. 3, 0 .. 4]);
157 
158     assert(ch.front!0[1] == sl[3 .. 6, 0 .. 4]);
159     assert(ch.front!1[1] == sl[0 .. 3, 4 .. 8]);
160 
161     assert (ch[$ - 1, $ - 1] == [[98, 99], [108, 109]]);
162 
163     static assert(isChunks!(typeof(ch)) == [1, 0]); // isChunks returns dimension indices
164 
165     assert(ch.length == 3);
166     assert(ch.length!1 == 4);
167 
168     ch.popFront;
169     assert(ch.front.front == sl[0 .. 3, 4 .. 8]);
170     ch.popFront!1;
171     assert(ch.front.front == sl[3 .. 6, 4 .. 8]);
172 
173     assert(ch.back.slice == sl[3 .. $, 8 .. $]);
174     ch.popBack;
175     assert(ch.back.slice == sl[3 .. $, 4 .. 8]);
176 
177     import std.range.primitives: isRandomAccessRange;
178     static assert(isRandomAccessRange!(typeof(ch)));
179 }
180 
181 /// 1Dx2D
182 version(mir_test) unittest
183 {
184     import mir.ndslice.chunks: chunks, isChunks;
185     import mir.ndslice.topology: iota;
186 
187     //   0   1   2   3   4   5   6   7   8   9
188     //  10  11  12  13  14  15  16  17  18  19
189     //  20  21  22  23  24  25  26  27  28  29
190     //  30  31  32  33  34  35  36  37  38  39
191     auto sl = iota(4, 10); // [0, 1, .. 10]
192 
193     //   ----------------   ----------------   --------
194     //  |  0   1   2   3 | |  4   5   6   7 | |  8   9 |
195     //  | 10  11  12  13 | | 14  15  16  17 | | 18  19 |
196     //  | 20  21  22  23 | | 24  25  26  27 | | 28  29 |
197     //  | 30  31  32  33 | | 34  35  36  37 | | 38  39 |
198     //   ----------------   ----------------   --------
199     // Chunk columns
200     auto ch = sl.chunks!1(4);
201 
202     assert(ch.slice == sl);
203     assert(ch.front == sl[0 .. $, 0 .. 4]);
204 
205     assert(ch.back == sl[0 .. $, 8 .. $]);
206 
207     import std.range.primitives: isRandomAccessRange;
208     static assert(isRandomAccessRange!(typeof(ch)));
209 }
210 
211 // conversion to ndslice
212 version(mir_test) unittest
213 {
214     import mir.ndslice.slice : slicedField;
215     import mir.ndslice.chunks: chunks;
216     import mir.ndslice.topology: iota, map;
217     import mir.math.sum: sum;
218 
219     // 0 1 2 3 4 5 6 7 8 9 10
220     auto sl = iota(11);
221     // 0 1 2 | 3 4 5 | 6 7 8 | 9 10
222     auto ch = sl.chunks(3);
223     // 3 | 12 | 21 | 19
224     auto s = ch.slicedField.map!sum;
225     assert(s == [3, 12, 21, 19]);
226 }
227 
228 /++
229 +/
230 struct Chunks(size_t[] dimensions, Iterator, size_t N = 1, SliceKind kind = Contiguous)
231 {
232 @optmath:
233 
234     /++
235     Chunk shape.
236     +/
237     size_t[dimensions.length] chunkLengths()() @property { return _chunkLengths; }
238     /// ditto
239     size_t[dimensions.length] _chunkLengths;
240 
241     ///
242     auto lightConst()() const @property
243     {
244         import mir.qualifier;
245         return Chunks!(dimensions, LightConstOf!Iterator, N, kind)(_chunkLengths, _slice.lightConst);
246     }
247 
248     ///
249     auto lightImmutable()() immutable @property
250     {
251         import mir.qualifier;
252         return Chunks!(dimensions, LightImmutableOf!Iterator, N, kind)(_chunkLengths, _slice.lightImmutable);
253     }
254 
255     alias DeepElement = Slice!(Iterator, N, kind);
256 
257     /++
258     Underlying ndslice.
259     It always correspond to current chunks state.
260     Its shape equal to the concatenation of the all chunks.
261     +/
262     Slice!(Iterator, N, kind) slice()() @property { return _slice; }
263     ///
264     Slice!(Iterator, N, kind) _slice;
265 
266     private auto _norm(size_t dimensionIndex = 0)() @property
267     {
268         assert(_chunkLengths[dimensionIndex]);
269         enum dimension = dimensions[dimensionIndex];
270         if (_expect(_slice._lengths[dimension] < _chunkLengths[dimensionIndex], false) && _slice._lengths[dimension])
271             _chunkLengths[dimensionIndex] = _slice._lengths[dimension];
272     }
273 
274     private auto _wrap(size_t dimensionIndex, S)(ref S ret)
275     {
276         static if (dimensions.length == 1)
277         {
278             return ret;
279         }
280         else
281         {
282             size_t[dimensions.length - 1] rcl;
283             foreach (i, j; AliasSeq!(Iota!dimensionIndex, Iota!(dimensionIndex + 1, dimensions.length)))
284                 rcl[i] = _chunkLengths[j];
285             enum newDims = dimensions[0 .. dimensionIndex] ~ dimensions[dimensionIndex + 1 .. $];
286             return .Chunks!(newDims, Iterator, N, typeof(ret).kind)(rcl, ret);
287         }
288     }
289 
290     private ref size_t sliceLength(size_t dimensionIndex)() @property
291     {
292         enum dimension = dimensions[dimensionIndex];
293         return _slice._lengths[dimension];
294     }
295 
296     /// ndslice-like primitives
297     bool empty(size_t dimensionIndex = 0)() const @property
298         if (dimensionIndex < dimensions.length)
299     {
300         enum dimension = dimensions[dimensionIndex];
301         return _slice.empty!(dimension);
302     }
303 
304     ///
305     size_t[dimensions.length] shape()() const @property
306     {
307         typeof(return) ret;
308         foreach(dimensionIndex; Iota!(ret.length))
309         {
310             enum dimension = dimensions[dimensionIndex];
311             auto l = _slice._lengths[dimension];
312             auto n = _chunkLengths[dimensionIndex];
313             ret[dimensionIndex] = l / n + (l % n != 0);
314         }
315         return ret;
316     }
317 
318     /// ditto
319     size_t length(size_t dimensionIndex = 0)() const @property
320         if (dimensionIndex < dimensions.length)
321     {
322         enum dimension = dimensions[dimensionIndex];
323         auto l = _slice._lengths[dimension];
324         auto n = _chunkLengths[dimensionIndex];
325         return l / n + (l % n != 0);
326     }
327 
328     /// ditto
329     auto front(size_t dimensionIndex = 0)() @property
330         if (dimensionIndex < dimensions.length)
331     {
332         enum dimension = dimensions[dimensionIndex];
333         assert(_chunkLengths[dimensionIndex] <= _slice._lengths[dimension]);
334         auto ret = _slice.selectFront!dimension(_chunkLengths[dimensionIndex]);
335         return _wrap!dimensionIndex(ret);
336     }
337 
338     ///
339     auto back(size_t dimensionIndex = 0)() @property
340         if (dimensionIndex < dimensions.length)
341     {
342         assert(!empty!dimensionIndex);
343         enum dimension = dimensions[dimensionIndex];
344         auto l = _slice._lengths[dimension];
345         auto n = _chunkLengths[dimensionIndex];
346         auto rshift = l % n;
347         rshift = !rshift ? n : rshift;
348         auto len = _slice._lengths[dimension];
349         auto ret = _slice.select!dimension(len - rshift, len);
350         return _wrap!dimensionIndex(ret);
351     }
352 
353     /// ditto
354     void popFront(size_t dimensionIndex = 0)()
355         if (dimensionIndex < dimensions.length)
356     {
357         enum dimension = dimensions[dimensionIndex];
358         assert(!empty!dimensionIndex);
359         _slice.popFrontExactly!dimension(_chunkLengths[dimensionIndex]);
360         _norm!dimensionIndex;
361     }
362 
363     /// ditto
364     void popBack(size_t dimensionIndex = 0)()
365         if (dimensionIndex < dimensions.length)
366     {
367         assert(!empty!dimensionIndex);
368         enum dimension = dimensions[dimensionIndex];
369         auto l = _slice._lengths[dimension];
370         auto n = _chunkLengths[dimensionIndex];
371         auto rshift = l % n;
372         rshift = !rshift ? n : rshift;
373         _slice.popBackExactly!dimension(rshift);
374         _norm!dimensionIndex;
375    }
376 
377     /// ditto
378     Slice!(IotaIterator!size_t) opSlice(size_t dimensionIndex)(size_t i, size_t j) const
379         if (dimensionIndex < dimensions.length)
380     in
381     {
382         assert(i <= j,
383             "Chunks.opSlice!" ~ dimensionIndex.stringof ~ ": the left opSlice boundary must be less than or equal to the right bound.");
384         enum errorMsg = ": the right opSlice boundary must be less than or equal to the length of the given dimensionIndex.";
385         assert(j <= length!dimensionIndex,
386               "Chunks.opSlice!" ~ dimensionIndex.stringof ~ errorMsg);
387     }
388     do
389     {
390         return typeof(return)(j - i, typeof(return).Iterator(i));
391     }
392 
393     /// ditto
394     ChunksSlice!() opSlice(size_t dimensionIndex)(size_t i, ChunksDollar!() j) const
395         if (dimensionIndex < dimensions.length)
396     in
397     {
398         assert(i <= j,
399             "Chunks.opSlice!" ~ dimensionIndex.stringof ~ ": the left opSlice boundary must be less than or equal to the right bound.");
400         enum errorMsg = ": the right opSlice boundary must be less than or equal to the length of the given dimensionIndex.";
401         assert(j <= length!dimensionIndex,
402               "Chunks.opSlice!" ~ dimensionIndex.stringof ~ errorMsg);
403     }
404     do
405     {
406         return typeof(return)(i, j);
407     }
408 
409     /// ditto
410     ChunksDollar!() opDollar(size_t dimensionIndex)() @property
411     {
412         enum dimension = dimensions[dimensionIndex];
413         return ChunksDollar!()(_slice._lengths[dimension], _chunkLengths[dimensionIndex]);
414     }
415 
416     /// ditto
417     auto opIndex(Slices...)(Slices slices)
418         if (Slices.length <= dimensions.length)
419     {
420         static if (slices.length == 0)
421         {
422             return this;
423         }
424         else
425         {
426             alias slice = slices[0];
427             alias S = Slices[0];
428             static if (isIndex!S)
429             {
430                 auto next = this.select!0(slice);
431             }
432             else
433             static if (is_Slice!S)
434             {
435                 auto i = slice._iterator._index;
436                 auto j = i + slice._lengths[0];
437                 auto next = this.select!0(i, j);
438             }
439             else
440             {
441                 auto next = this.select!0(slice.i, slice.j);
442             }
443             static if (slices.length > 1)
444             {
445                 return next[slices[1 .. $]];
446             }
447             else
448             {
449                 return next;
450             }
451         }
452     }
453 
454     /// ditto
455     auto opIndex()(size_t[dimensions.length] index)
456     {
457         auto next = this.select!0(index[0]);
458         static if (dimensions.length == 1)
459         {
460             return next;
461         }
462         else
463         {
464             return next[index[1 .. $]];
465         }
466     }
467 
468     /// ditto
469     auto save()() @property
470     {
471         return this;
472     }
473 
474     ///
475     auto select(size_t dimensionIndex = 0)(size_t index) @property
476         if (dimensionIndex < dimensions.length)
477     {
478         enum dimension = dimensions[dimensionIndex];
479         auto chl = _chunkLengths[dimensionIndex];
480         auto shiftL = chl * index;
481         assert(shiftL <= _slice._lengths[dimension]);
482         auto shiftR = shiftL + chl;
483         if (_expect(shiftR > _slice._lengths[dimension], false))
484         {
485             shiftR = _slice._lengths[dimension];
486         }
487         auto ret = _slice.select!dimension(shiftL, shiftR);
488         return _wrap!dimensionIndex(ret);
489     }
490 
491     /// ditto
492     auto select(size_t dimensionIndex = 0)(size_t i, size_t j) @property
493         if (dimensionIndex < dimensions.length)
494     {
495         assert(i <= j);
496         enum dimension = dimensions[dimensionIndex];
497         auto chl = _chunkLengths[dimensionIndex];
498         auto shiftL = chl * i;
499         auto shiftR = chl * j;
500         assert(shiftL <= _slice._lengths[dimension]);
501         assert(shiftR <= _slice._lengths[dimension]);
502         if (_expect(shiftR > _slice._lengths[dimension], false))
503         {
504             shiftR = _slice._lengths[dimension];
505             if (_expect(shiftL > _slice._lengths[dimension], false))
506             {
507                 shiftL = _slice._lengths[dimension];
508             }
509         }
510         auto ret = _slice.select!dimension(shiftL, shiftR);
511         import std.meta: aliasSeqOf;
512         return ret.chunks!(aliasSeqOf!dimensions)(_chunkLengths);
513     }
514 
515     // undocumented
516     auto select(size_t dimensionIndex = 0)(ChunksSlice!() sl) @property
517         if (dimensionIndex < dimensions.length)
518     {
519         assert(sl.i <= _slice._lengths[dimension]);
520         assert(sl.chunkLength == _chunkLengths[dimensionIndex]);
521         assert(sl.length == _slice._lengths[dimension]);
522 
523         enum dimension = dimensions[dimensionIndex];
524         auto chl = _chunkLengths[dimensionIndex];
525         auto len = sl.i * chl;
526         assert(len <= _slice._lengths[dimension]);
527         if (_expect(len > _slice._lengths[dimension], false))
528             len = _slice._lengths[dimension];
529         auto ret = _slice.selectBack!dimension(len);
530         import std.meta: aliasSeqOf;
531         return ret.chunks!(aliasSeqOf!dimensions)(_chunkLengths);
532     }
533 }
534 
535 // undocumented
536 struct ChunksSlice()
537 {
538     size_t i;
539     ChunksDollar!() j;
540 }
541 
542 // undocumented
543 struct ChunksDollar()
544 {
545     size_t length;
546     size_t chunkLength;
547     size_t value()() @property
548     {
549         return length / chunkLength + (length % chunkLength != 0);
550     }
551     alias value this;
552 }
553 
554 /++
555 Checks if T is $(LREF Chunks) type.
556 Returns:
557     array of dimension indices.
558 +/
559 template isChunks(T)
560 {
561     static if (is(T : Chunks!(dimensions, Iterator, N, kind), size_t[] dimensions, Iterator, size_t N, SliceKind kind))
562         enum isChunks = dimensions;
563     else
564         enum isChunks = size_t[].init;
565 }
566 
567 ///
568 version(mir_test) unittest
569 {
570     import mir.ndslice.chunks: chunks, isChunks;
571     import mir.ndslice.topology: iota;
572 
573     static assert(isChunks!int == null);
574     static assert(isChunks!(typeof(iota(20, 30).chunks!(1, 0)(3, 7))) == [1, 0]);
575 }
576 
577 /++
578 Evaluates `popFront!dimmensionIndex` for multiple $(LREF Chunks) structures at once.
579 All chunks structures must have for the appropriate dimension the same chunk lengths and the same underlying slice lengths.
580 
581 Params:
582     dimmensionIndex = dimensionIndex
583     master = the fist chunks structure
584     followers = following chunks structures
585 +/
586 void popFrontTuple(size_t dimmensionIndex = 0, Master, Followers...)(ref Master master, ref Followers followers)
587     if (isChunks!Master && allSatisfy!(isChunks, Followers))
588 in
589 {
590     foreach (ref follower; followers)
591     {
592         assert(follower.sliceLength!dimmensionIndex == master.sliceLength!dimmensionIndex);
593         assert(follower._chunkLengths[dimmensionIndex] == master._chunkLengths[dimmensionIndex]);
594     }
595 }
596 do
597 {
598     master._slice.popFrontExactly!(isChunks!Master[dimmensionIndex])(master._chunkLengths[dimmensionIndex]);
599     foreach (i, ref follower; followers)
600     {
601         follower._slice.popFrontExactly!(isChunks!(Followers[i])[dimmensionIndex])(master._chunkLengths[dimmensionIndex]);
602         // hint for optimizer
603         follower.sliceLength!dimmensionIndex = master.sliceLength!dimmensionIndex;
604     }
605     if (_expect(master.sliceLength!dimmensionIndex < master._chunkLengths[dimmensionIndex], false) && master.sliceLength!dimmensionIndex)
606     {
607         master._chunkLengths[dimmensionIndex] = master.sliceLength!dimmensionIndex;
608         foreach(ref follower; followers)
609         {
610             follower._chunkLengths[dimmensionIndex] = master._chunkLengths[dimmensionIndex];
611         }
612     }
613 }
614 
615 ///
616 version(mir_test) unittest
617 {
618     import mir.ndslice.chunks: chunks;
619     import mir.ndslice.topology: iota;
620 
621     auto a = iota(10, 20).chunks!(0, 1)(3, 7);
622     auto b = iota(20, 10).chunks!(1, 0)(3, 7);
623 
624     auto as = a;
625     auto bs = b;
626 
627     as.popFront;
628     bs.popFront;
629 
630     popFrontTuple(a, b);
631 
632     assert(as.slice == a.slice);
633     assert(bs.slice == b.slice);
634 
635     assert(as.chunkLengths == a.chunkLengths);
636     assert(bs.chunkLengths == b.chunkLengths);
637 }