1 /**
2     Contains functions for creating variable nodes and subsequently manipulating their shapes.
3 
4     Authors: Henry Gouk
5 */
6 module dopt.core.ops.basic;
7 
8 import dopt.core.ops;
9 import dopt.core.types;
10 
11 import std.algorithm;
12 import std.array;
13 import std.exception;
14 import std.functional;
15 import std.range;
16 import std.variant;
17 
18 package
19 {
20     void initialize()
21     {
22         registerOperation("slice", OpDef(toDelegate(&verifySlice), toDelegate(&judgeSlice)));
23         registerOperation("pad", OpDef(toDelegate(&verifyPad), toDelegate(&judgePad)));
24         registerOperation("reshape", OpDef(toDelegate(&verifyReshape), toDelegate(&judgeReshape)));
25         registerOperation("transpose", OpDef(toDelegate(&verifyTranspose), toDelegate(&judgeTranspose)));
26         registerOperation("repeat", OpDef(toDelegate(&verifyRepeat), toDelegate(&judgeRepeat)));
27         registerOperation("variable", OpDef(toDelegate(&verifyVariable), toDelegate(&judgeVariable)));
28         registerOperation("constant", OpDef(toDelegate(&verifyVariable), toDelegate(&judgeVariable)));
29     }
30 }
31 
32 private
33 {
34     bool verifySlice(Operation op)
35     {
36         if(("start" in op.attributes) is null || ("stop" in op.attributes) is null)
37         {
38             return false;
39         }
40 
41         auto startVar = op.attributes["start"];
42         auto stopVar = op.attributes["stop"];
43 
44         if(startVar.peek!(size_t[]) is null || stopVar.peek!(size_t[]) is null)
45         {
46             return false;
47         }
48 
49         auto start = startVar.get!(size_t[]);
50         auto stop = stopVar.get!(size_t[]);
51 
52         return op.deps.length == 1
53             && start.length == stop.length
54             && start.length == op.deps[0].outputType.rank
55             && zip(start, op.deps[0].outputType.shape).all!(x => x[0] < x[1])
56             && zip(stop, op.deps[0].outputType.shape).all!(x => x[0] <= x[1])
57             && zip(start, stop).all!(x => x[0] < x[1]);
58     }
59 
60     TensorType judgeSlice(Operation op)
61     {
62         auto start = op
63                     .attributes["start"]
64                     .get!(size_t[]);
65 
66         auto stop = op
67                    .attributes["stop"]
68                    .get!(size_t[]);
69 
70         auto shape = zip(start, stop)
71                     .map!(x => x[1] - x[0])
72                     .array();
73 
74         return TensorType(op.deps[0].outputType.elementType, shape);
75     }
76 
77     bool verifyPad(Operation op)
78     {
79         if(("before" in op.attributes) is null || ("after" in op.attributes) is null)
80         {
81             return false;
82         }
83 
84         auto beforeVar = op.attributes["before"];
85         auto afterVar = op.attributes["after"];
86 
87         if(beforeVar.peek!(size_t[]) is null || afterVar.peek!(size_t[]) is null)
88         {
89             return false;
90         }
91 
92         auto before = beforeVar.get!(size_t[]);
93         auto after = afterVar.get!(size_t[]);
94 
95         return op.deps.length == 1
96             && before.length == after.length
97             && before.length == op.deps[0].outputType.rank;
98     }
99 
100     TensorType judgePad(Operation op)
101     {
102         auto before = op
103                      .attributes["before"]
104                      .get!(size_t[]);
105 
106         auto after = op
107                     .attributes["after"]
108                     .get!(size_t[]);
109 
110         auto shape = zip(before, after, op.deps[0].outputType.shape)
111                     .map!(x => x[0] + x[1] + x[2])
112                     .array();
113 
114         return TensorType(op.deps[0].outputType.elementType, shape);
115     }
116 
117     bool verifyReshape(Operation op)
118     {
119         auto newShape = "shape" in op.attributes;
120 
121         return op.deps.length == 1
122             && newShape !is null
123             && newShape.peek!(size_t[]) !is null
124             && newShape.get!(size_t[]).fold!((a, b) => a * b)(cast(size_t)1) == op.deps[0].outputType.volume;
125     }
126 
127     TensorType judgeReshape(Operation op)
128     {
129         return TensorType(op.deps[0].outputType.elementType, op.attributes["shape"].get!(size_t[]));
130     }
131 
132     bool verifyTranspose(Operation op)
133     {
134         auto newOrder = "order" in op.attributes;
135 
136         return op.deps.length == 1
137             && newOrder !is null
138             && newOrder.peek!(size_t[]) !is null
139             && newOrder.get!(size_t[]).dup.sort().equal(iota(0, op.deps[0].outputType.rank));
140     }
141 
142     TensorType judgeTranspose(Operation op)
143     {
144         auto order = op
145                     .attributes["order"]
146                     .get!(size_t[]);
147 
148         auto newShape = order
149                        .map!(x => op.deps[0].outputType.shape[x])
150                        .array();
151 
152         return TensorType(op.deps[0].outputType.elementType, newShape);
153     }
154 
155     bool verifyRepeat(Operation op)
156     {
157         if(("repetitions" in op.attributes) is null)
158         {
159             return false;
160         }
161 
162         auto reps = op.attributes["repetitions"].get!(size_t[]);
163 
164         return op.deps.length == 1
165             && reps.length == op.deps[0].rank
166             && reps.all!(x => x > 0);
167     }
168 
169     TensorType judgeRepeat(Operation op)
170     {
171         auto reps = op.attributes["repetitions"].get!(size_t[]);
172         auto shape = op.deps[0].shape.dup;
173         shape[] *= reps[];
174 
175         return TensorType(op.deps[0].elementType, shape);
176     }
177 
178     bool verifyVariable(Operation op)
179     {
180         return op.deps.length == 0
181             && ("type" in op.attributes) !is null
182             && op.attributes["type"].peek!TensorType !is null;
183     }
184 
185     TensorType judgeVariable(Operation op)
186     {
187         return op.attributes["type"].get!TensorType;
188     }
189 }
190 
191 public
192 {
193     /**
194         Slices the result of an operation.
195 
196         Params:
197             input = The operation that should be sliced.
198             start = The starting indices for each dimension.
199             stop = The stopping indices for each dimension.
200 
201         Returns:
202             The new $(D Operation).
203     */
204     Operation slice(Operation input, size_t[] start, size_t[] stop,
205         string mod = __MODULE__, size_t line = __LINE__)
206     {
207         return createOperation("slice", [input], ["start": Variant(start), "stop": Variant(stop)], mod, line);
208     }
209 
210     ///
211     unittest
212     {
213         import dopt.core : evaluate;
214 
215         auto s1 = int32([3, 3], [
216             1, 2, 3,
217             4, 5, 6,
218             7, 8, 9
219         ]).slice([1, 1], [3, 3]);
220 
221         assert(s1.evaluate().as!int == [
222             5, 6,
223             8, 9
224         ]);
225     }
226 
227     /**
228         Pads the result of an operation with zeros in each dimension.
229 
230         Params:
231             input = The operation that should be padded.
232             before = The amount of padding that should be prepended for each dimension.
233             after = The amount of padding that should be appended for each dimension.
234 
235         Returns:
236             The new $(D Operation).
237     */
238     Operation pad(Operation input, size_t[] before, size_t[] after,
239         string mod = __MODULE__, size_t line = __LINE__)
240     {
241         return createOperation("pad", [input], ["before": Variant(before), "after": Variant(after)], mod, line);
242     }
243 
244     ///
245     unittest
246     {
247         import dopt.core : evaluate;
248 
249         auto p1 = int32([1, 1], [3]).pad([2, 1], [3, 3]);
250 
251         assert(p1.evaluate().as!int == [
252             0, 0, 0, 0, 0,
253             0, 0, 0, 0, 0,
254             0, 3, 0, 0, 0,
255             0, 0, 0, 0, 0,
256             0, 0, 0, 0, 0,
257             0, 0, 0, 0, 0
258         ]);
259     }
260 
261     /**
262         Allows one to cast an operation to a different shape with the same volume.
263 
264         Params:
265             input = The operation to be reshaped.
266             shape = The new shape.
267 
268         Returns:
269             The new $(D Operation).
270     */
271     Operation reshape(Operation input, size_t[] shape, string mod = __MODULE__, size_t line = __LINE__)
272     {
273         return createOperation("reshape", [input], ["shape": Variant(shape)], mod, line);
274     }
275 
276     ///
277     unittest
278     {
279         import dopt.core : evaluate;
280 
281         auto r1 = float32([2, 2], [1.0f, 2.0f, 3.0f, 4.0f]).reshape([1, 4]);
282 
283         assert(r1.shape == [1, 4]);
284         assert(r1.evaluate().as!float == [1.0f, 2.0f, 3.0f, 4.0f]);
285     }
286 
287     /**
288         Reorders the dimensions of output of an operation.
289 
290         Params:
291             input = The operation that should have its dimensions reordered.
292             order = Determines how the dimensions are permuted.
293 
294         Notes:
295             Currently only implemented for rank 2 tensors.
296 
297         Returns:
298             The new $(D Operation).
299     */
300     Operation transpose(Operation input, size_t[] order, string mod = __MODULE__, size_t line = __LINE__)
301     {
302         return createOperation("transpose", [input], ["order": Variant(order)], mod, line);
303     }
304 
305     ///
306     unittest
307     {
308         import dopt.core : evaluate;
309 
310         auto t1 = float32([2, 2], [1.0f, 2.0f, 3.0f, 4.0f]).transpose([1, 0]);
311 
312         assert(t1.evaluate().as!float == [1.0f, 3.0f, 2.0f, 4.0f]);
313     }
314 
315     /**
316         Repeats the output of an operation along each axis the given number of times.
317 
318         Params:
319             input = The operation to have its output repeated.
320             repetitions = The number of repetitions to perform along each axis.
321 
322         Return:
323             The new $(D Operation).
324     */
325     Operation repeat(Operation input, size_t[] repetitions, string mod = __MODULE__,
326         size_t line = __LINE__)
327     {
328         enforce(repetitions.length == input.rank,
329             "The length of repetitions must be the same as the rank of the input.");
330         
331         return createOperation("repeat", [input], ["repetitions": Variant(repetitions)], mod, line);
332     }
333 
334     ///
335     unittest
336     {
337         import dopt.core : evaluate;
338         
339         auto r1 = float32([1, 1], [3.0f]).repeat([2, 3]);
340         auto r2 = float32([2, 2], [1.0f, 2.0f, 3.0f, 4.0f]).repeat([3, 2]);
341 
342         assert(r1.evaluate().as!float == [
343             3.0f, 3.0f, 3.0f,
344             3.0f, 3.0f, 3.0f
345         ]);
346 
347         assert(r2.evaluate().as!float == [
348             1.0f, 2.0f, 1.0f, 2.0f,
349             3.0f, 4.0f, 3.0f, 4.0f,
350             1.0f, 2.0f, 1.0f, 2.0f,
351             3.0f, 4.0f, 3.0f, 4.0f,
352             1.0f, 2.0f, 1.0f, 2.0f,
353             3.0f, 4.0f, 3.0f, 4.0f
354         ]);
355     }
356 
357     /**
358         Repeats the output of an operation the given number of times.
359 
360         A new dimension is added, allowing one to index each of these repetitions.
361 
362         Params:
363             input = The operation to have its output repeated.
364             repetitions = The number of repetitions to perform.
365         
366         Return:
367             The new $(D Operation).
368     */
369     Operation repeat(Operation input, size_t repetitions, string mod = __MODULE__, size_t line = __LINE__)
370     {
371         auto flat = input.reshape([input.volume]);
372         auto r = flat.repeat([repetitions], mod, line);
373         
374         return r.reshape([repetitions] ~ input.shape, mod, line);
375     }
376 
377     ///
378     unittest
379     {
380         import dopt.core : evaluate;
381 
382         auto r1 = float32([2], [1.0f, 2.0f]).repeat(3);
383 
384         assert(r1.evaluate().as!float == [
385             1.0f, 2.0f,
386             1.0f, 2.0f,
387             1.0f, 2.0f
388         ]);
389     }
390 
391     /**
392         Creates a variable with the given type.
393 
394         If no default value is provided, then the variable will have a default value of all zeros. The default value is
395         stored in the attributes["default"] field of the returned operation.
396 
397         Params:
398             type = The type of the variable
399             defaultVal = The default value of the variable. The array should store the elements in row major order.
400 
401         Returns:
402             The newly created variable
403     */
404     Operation variable(TensorType type, void[] defaultVal = null, string mod = __MODULE__, size_t line = __LINE__)
405     {
406         auto bufSize = type.volume * sizeOf(type.elementType);
407 
408         if(defaultVal is null)
409         {
410             defaultVal = new ubyte[bufSize];
411         }
412         else
413         {
414             enforce(defaultVal.length == bufSize, "The length of defaultVal does not match type.volume.");
415         }
416 
417         return createOperation("variable", [], ["type": Variant(type), "default": Variant(Buffer(defaultVal))], mod, line);
418     }
419 
420     /**
421         Creates a variable with the given shape and float32 elements.
422 
423         If no default value is provided, then the variable will have a default value of all zeros. The default value is
424         stored in the attributes["default"] field of the returned operation.
425 
426         Params:
427             size = The shape of the variable
428             defaultVal = The default value of the variable. The array should store the elements in row major order.
429 
430         Returns:
431             The newly created variable
432     */
433     Operation float32(size_t[] size = [], float[] defaultVal = null, string mod = __MODULE__, size_t line = __LINE__)
434     {
435         return variable(TensorType(DataType.float32, size), defaultVal, mod, line);
436     }
437 
438     /**
439         Creates a variable with the given shape and int32 elements.
440 
441         If no default value is provided, then the variable will have a default value of all zeros. The default value is
442         stored in the attributes["default"] field of the returned operation.
443 
444         Params:
445             size = The shape of the variable
446             defaultVal = The default value of the variable. The array should store the elements in row major order.
447 
448         Returns:
449             The newly created variable
450     */
451     Operation int32(size_t[] size = [], int[] defaultVal = null, string mod = __MODULE__, size_t line = __LINE__)
452     {
453         return variable(TensorType(DataType.int32, size), defaultVal, mod, line);
454     }
455 
456     /**
457         Creates a constant with the given type.
458 
459         Params:
460             type = The type of the constant
461             val = The value of the constant. The array should store the elements in row major order.
462 
463         Returns:
464             The newly created constant
465     */
466     Operation constant(TensorType type, void[] val, string mod = __MODULE__, size_t line = __LINE__)
467     {
468         auto bufSize = type.volume * sizeOf(type.elementType);
469 
470         if(val is null)
471         {
472             val = new ubyte[bufSize];
473         }
474         else
475         {
476             enforce(val.length == bufSize, "The length of val does not match type.volume.");
477         }
478 
479         return createOperation("constant", [], ["type": Variant(type), "default": Variant(Buffer(val))], mod, line);
480     }
481 
482     /**
483         Creates a constant with the given shape and float32 values.
484 
485         Params:
486             size = The shape of the constant
487             val = The value of the constant. The array should store the elements in row major order.
488 
489         Returns:
490             The newly created constant
491     */
492     Operation float32Constant(size_t[] size, float[] val, string mod = __MODULE__, size_t line = __LINE__)
493     {
494         return constant(TensorType(DataType.float32, size), val, mod, line);
495     }
496 
497     /**
498         Creates a constant with the given shape and int32 values.
499 
500         Params:
501             size = The shape of the constant
502             val = The value of the constant. The array should store the elements in row major order.
503 
504         Returns:
505             The newly created constant
506     */
507     Operation int32Constant(size_t[] size, int[] val, string mod = __MODULE__, size_t line = __LINE__)
508     {
509         return constant(TensorType(DataType.int32, size), val, mod, line);
510     }
511 }