diff --git a/src/arraymancer/tensor/display.nim b/src/arraymancer/tensor/display.nim index 0d35f765..77c7114d 100644 --- a/src/arraymancer/tensor/display.nim +++ b/src/arraymancer/tensor/display.nim @@ -13,22 +13,168 @@ # limitations under the License. import ./private/p_display, - ./data_structure, - typetraits + ./data_structure +import std / typetraits -proc pretty*[T](t: Tensor[T], precision = -1): string = - ## Pretty-print a Tensor with the option to set a custom `precision` - ## for float values. - var desc = t.type.name & " of shape \"" & $t.shape & "\" on backend \"" & "Cpu" & "\"" - if t.storage.isNil: # return useful message for uninit'd tensors instead of crashing - return "Uninitialized " & $desc - if t.size() == 0: - return desc & "\n [] (empty)" - elif t.rank == 1: # for rank 1 we want an indentation, because we have no `|` - return desc & "\n " & t.prettyImpl(precision = precision) - else: - return desc & "\n" & t.prettyImpl(precision = precision) +proc pretty*[T](t: Tensor[T], precision: int = -1, showHeader: static bool = true): string = + ## Pretty-print a Tensor as a "table" with a given precision and optional header + ## + ## Pretty-print a Tensor with options to set a custom `precision` for float + ## values and to show or hide a header describing the tensor type and shape. + ## + ## Inputs: + ## - Input Tensor. + ## - precision: The number of decimals printed (for float tensors), + ## _including_ the decimal point. + ## - showHeader: If true (the default) show a description header + ## indicating the tensor type and shape. + ## Result: + ## - A string containing a "pretty-print" representation of the Tensor. + ## + ## Examples: + ## ```nim + ## let t = arange(-2.0, 4.0).reshape(2, 3) + ## + ## echo t.pretty() + ## # Tensor[system.float] of shape "[2, 3]" on backend "Cpu" + ## # |-2 -1 0| + ## # |1 2 3| + ## + ## # Note that the precision counts + ## echo t.pretty(2) + ## # Tensor[system.float] of shape "[2, 3]" on backend "Cpu" + ## # |-2.0 -1.0 0.0| + ## # |1.0 2.0 3.0| + ## ``` + const specifier = if showHeader: "" else: "||" + t.prettyImpl(precision = precision, specifier = specifier) + +proc pretty*[T](t: Tensor[T], specifier: static string = ""): string = + ## Pretty-print a Tensor with the option to set a custom format `specifier` + ## + ## The "format specifier" is similar to those used in format strings, with + ## the addition of a few, tensor specific modifiers (shown below). + ## + ## Inputs: + ## - Input Tensor + ## - specifier: A format specifier similar to those used in format strings, + ## which are used to control how the tensor and its elements + ## are displayed. + ## All of the standard format specifiers, such as "f", "g", + ## "+", etc. (and including, in nim 2.2 and above, the 'j' + ## specifier for complex tensors) can be used (check the + ## documentation of nim's `strformat` module for more info). + ## In addition to those standard format specifiers which + ## control how the elements of the tensor are displayed, you + ## can use a few, tensor specific modifiers which can be + ## combined to achieve different results: + ## - "[:]": Display the tensor as if it were a nim "array". + ## This makes it easy to use the representation of a + ## tensor in your own code. No header is shown. + ## - "<>[:]": Same as "[:]" but displays a header describing + ## the tensor type and shape. + ## - "[]": Same as "[:]" but displays the tensor in a single + ## line. No header is shown. + ## - "<>[:]": Same as "[]" but displays a header describing + ## the tensor type and shape. + ## - "||": "Pretty-print" the tensor _without_ a header. + ## - "<>||": Same as "||" but displays a header describing the + ## tensor type and shape. This is the default display + ## mode. + ## - "<>": When used on its own (i.e. not combined with "[:]", + ## "[]" and "||" as shown above) it is equivalent to + ## "<>[]" (i.e. single-line, nim-like representation + ## with a header). Cannot wrap the element specifier. + ## + ## Notes: + ## - The default format (i.e. when none of these tensor-specific tokens is + ## used) is pretty printing with a header (i.e. "<>||"). + ## - These tensor specific modifiers can placed at the start or at the end + ## of the format specifier (e.g. "<>[:]06.2f", or "06.2f<>[:]"), or they + ## can "wrap" the element specifier (e.g. "<>[:06.2f]"). + ## - When wrapping you cannot use the "compact forms" of these specifiers + ## (i.e. "<:>" and "<|>"). + ## - When using the wrapping form of "[:]", start with "[:" and end with + ## either "]" or ":]" (i.e. both "[:06.2f]" and "[:06.2f:]" are valid). + ## - This version of this function does not have a `showHeader` argument + ## because to disable the header you can simply select the right format + ## specifier. + ## + ## Examples: + ## ```nim + ## let t_int = arange(-2, 22, 4).reshape(2, 3) + ## + ## # You can specify a format for the elements in the tensor + ## # Note that the default is "pretty-printing" the tensor + ## # _and_ showing a header describing its type and shape + ## echo t_int.pretty("+05X") + ## # Tensor[system.int] of shape "[2, 3]" on backend "Cpu" + ## # |-0002 +0002 +0006| + ## # |+000A +000E +0012| + ## + ## # The header can be disabled by using "||" + ## echo t_int.pretty("+05X||") + ## # |-0002 +0002 +0006| + ## # |+000A +000E +0012| + ## + ## # Use the "[:]" format specifier to print the tensor as a + ## # "multi-line array" _without_ a header + ## echo t_int.pretty("[:]") + ## # [[-2, 2, 6], + ## # [10, 14, 18]] + ## + ## # Enable the header adding "<>" (i.e. "<>[:]") or the shorter "<:>" + ## echo t_int.pretty("<:>") + ## # Tensor[int]<2,3>: + ## # [[-2, 2, 6], + ## # [10, 14, 18]] + ## + ## # The "[]" specifier is similar to "[:]" but prints on a single line + ## echo t_int.pretty("[]") + ## # [[-2, 2, 6], [10, 14, 18]] + ## + ## # You can also enable the header using "<>" or "<>[]" + ## echo t_int.pretty("<>") + ## # Tensor[int]<2,3>:[[-2, 2, 6], [10, 14, 18]] + ## + ## # You can combine "[]", "[:]", "<>" and "<:>" with a regular format spec: + ## let t_float = arange(-2.0, 22.0, 4.0).reshape(2, 3) + ## + ## echo t_float.pretty("6.2f<:>") + ## # Tensor[int]<2,3>: + ## # [[ -2.00, 2.00, 6.00], + ## # [ 10.00, 14.00, 18.00]] + ## + ## # The equivalent to the wrapping form is: + ## echo t_float.pretty("<>[:6.2f]") + ## # Tensor[int]<2,3>: + ## # [[ -2.00, 2.00, 6.00], + ## # [ 10.00, 14.00, 18.00]] + ## ``` + t.prettyImpl(precision = -1, specifier = specifier) proc `$`*[T](t: Tensor[T]): string = ## Pretty-print a tensor (when using ``echo`` for example) t.pretty() + +proc `$$`*[T](t: Tensor[T]): string = + ## Print the "elements" of a tensor as a multi-line array + t.pretty(specifier = "<:>") + +proc `$<>`*[T](t: Tensor[T]): string = + ## Print the "elements" of a tensor as a single-line array + t.pretty(specifier = "<>") + +proc formatValue*[T](result: var string, t: Tensor[T], specifier: static string) = + ## Standard format implementation for `Tensor` + ## + ## Note that it makes little sense to call this directly, but it is required + ## to exist by the `&` macro. + ## + ## See the documentation of the `pretty` procedure for more information on the + ## supported format specifiers (which include a number of tensor specific + ## tokens). + if specifier.len == 0: + result.add $t + else: + result.add t.pretty(specifier = specifier) diff --git a/src/arraymancer/tensor/private/p_display.nim b/src/arraymancer/tensor/private/p_display.nim index 7995b8ec..5118b5ba 100644 --- a/src/arraymancer/tensor/private/p_display.nim +++ b/src/arraymancer/tensor/private/p_display.nim @@ -14,15 +14,140 @@ import ../../private/functional, ../higher_order_applymap, ../shapeshifting, ../data_structure, - ../accessors, - sequtils, strutils + ../accessors +import std / [sequtils, strutils, strformat, typetraits] + +type tensorDispMode = enum table, multi_line_array, single_line_array + +func isWrappedBy(s: string, prefix: string, suffix: string): bool = + ## Check if a string starts or ends with a given prefix and suffix + return s.startsWith(prefix) and s.endsWith(suffix) + +func startsOrEndsWith(s: string, token: string): bool = + ## Check if a string starts or ends with a given prefix or suffix + return s.startsWith(token) or s.endsWith(token) + +func parseTensorFormatSpecifier(specifier: static string): (tensorDispMode, bool, string) = + ## Parse the tensor format specifier + ## + ## This custom made parser takes a "tensor format specifier" and returs the + ## display mode, a boolean indicating whether a header must be shown, and the + ## element format specifier + when specifier == "" or ("[" notin specifier and "<" notin specifier and "|" notin specifier and ":" notin specifier): + # Fast path for the most common, default case, which is getting an empty or + # an element format specifier without any special tensor markers + return (table, true, specifier) + # Handle the "shortcut" markers, which can appear either at the start or the + # end of the specifier, first + elif "[:]" in specifier or "<:>" in specifier or "[]" in specifier or "||" in specifier or "<|>" in specifier: + const element_specifier = specifier.multiReplace( + ("[:]", ""), ("<:>", ""), ("[]", ""), ("||", ""), ("<|>", "")) + const has_header = "<>" in specifier or "<:>" in specifier or "<|>" in specifier + when specifier.startsOrEndsWith("[:]") or specifier.startsOrEndsWith("<>[:]") or specifier.startsOrEndsWith("<:>"): + return (multi_line_array, has_header, element_specifier) + elif specifier.startsOrEndsWith("[]") or specifier.startsOrEndsWith("<>[]"): + return (single_line_array, has_header, element_specifier) + elif specifier.startsOrEndsWith("||") or specifier.startsOrEndsWith("<>||") or specifier.startsOrEndsWith("<|>"): + return (table, has_header, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "[:], [], ||, <:>, <|>, <>[:], <>[] and <>|| must be located at the " & + "start or the end of the format specifier." + .} + # Then handle the "wrapper" markers, which have separate opening and closing + # tokens which must appear that the start and the end of the specifier + # Start with the "<>" markers (i.e. those that request displaying a header) + elif "<>[:" in specifier: + # Multi-line with header + when specifier.isWrappedBy("<>[:", ":]"): + const element_specifier = specifier[4 ..< ^2] + return (multi_line_array, true, element_specifier) + elif specifier.isWrappedBy("<>[:", "]"): + # Support skipping the : on the closing bracket + const element_specifier = specifier[4 ..< ^1] + return (multi_line_array, true, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "<>[: and ] must wrap the element format specifier." + .} + elif "<>[" in specifier: + when specifier.isWrappedBy("<>[", "]"): + const element_specifier = specifier[3 ..< ^1] + return (single_line_array, true, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "<>[ and ] wrap the element format specifier." + .} + elif "<>|" in specifier: + when specifier.isWrappedBy("<>|", "|"): + const element_specifier = specifier[3 ..< ^1] + return (table, true, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "<>| and | must wrap the element format specifier." + .} + # Then handle the "<>-less" wrapper markers + elif "[:" in specifier: + when specifier.isWrappedBy("[:", ":]"): + const element_specifier = specifier[2 ..< ^2] + return (multi_line_array, false, element_specifier) + elif specifier.isWrappedBy("[:", "]"): + # Support skipping the : on the closing bracket + const element_specifier = specifier[2 ..< ^1] + return (multi_line_array, false, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "[: and ] must wrap the element format specifier." + .} + elif "[" in specifier: + when specifier.isWrappedBy("[", "]"): + const element_specifier = specifier[1 ..< ^1] + return (single_line_array, false, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "[ and ] must wrap the element format specifier." + .} + elif "|" in specifier: + when specifier.isWrappedBy("|", "|"): + const element_specifier = specifier[1 ..< ^1] + return (table, false, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "| and | must wrap the element format specifier." + .} + # At this point "<>" must appear on its own (meaning the same as "<>[]") + elif "<>" in specifier: + when specifier.startsOrEndsWith("<>"): + const element_specifier = specifier.replace("<>") + return (single_line_array, true, element_specifier) + else: + {. error: + "Invalid tensor format specifier (" & specifier & "): " & + "<> must be located at the start or the end of the format specifier." + .} + else: + # This should never happen, but if it did, use the default display format + return (table, true, specifier) + +func removeTensorFormatSpecifiers(specifier: static string): string = + ## Remove the special "tensor specifier tokens" + const (dispMode {.used.}, showHeader {.used.}, elementSpecifier) + = parseTensorFormatSpecifier(specifier) + return elementSpecifier func bounds_display(t: Tensor, idx_data: tuple[val: string, idx: int], alignBy, alignSpacing: int ): string = ## Internal routine, compare an index with the strides of a Tensor - ## to check beginning and end of lines + ## to check beginning and end of lines ## Add the delimiter "|" and line breaks at beginning and end of lines ## ## `alignBy` is the total fill for each "column" in a 2D print. `alignSpacing` @@ -78,17 +203,24 @@ func bounds_display(t: Tensor, # let b = toSeq(1..72).toTensor.reshape(2,3,3,4) # echo b +func dispElement*[T](value: T, precision = -1, specifier: static string = ""): string = + ## Display a single element with the selected precision _or_ specifier format + when specifier.len == 0: + when T is SomeFloat: + result = formatBiggestFloat(value, precision = precision) + else: + result = $value + else: + formatValue(result, value, specifier = specifier) + func disp2d*[T](t: Tensor[T], alignBy = 6, alignSpacing = 3, - precision = -1): string = - ## Display a 2D-tensor + precision = -1, specifier: static string = ""): string = + ## Display a 2D-tensor (only used for "table", i.e. non array, printing) # Add a position index to each value in the Tensor. var indexed_data: seq[(string,int)] = @[] for i, value in t.enumerate: - when T is SomeFloat: - let val = formatBiggestFloat(value, precision = precision) - else: - let val = $value + let val = dispElement(value, precision = precision, specifier = specifier) indexed_data.add((val, i+1)) # TODO Note: the $conversion is unstable if the whole test suite is done. # it fails at the test_load_openmp. # if done alone there is no issue @@ -152,17 +284,15 @@ func genLeftIdx(axIdx: string, s: string): string = if i < tmp.high - 1: result.add "\n" -proc determineLargestElement[T](t: Tensor[T], precision: int): int = +proc determineLargestElement[T](t: Tensor[T], precision: int, specifier: static string = ""): int = ## Determines the length of the "largest" element in the tensor after ## string conversion. This is to align our output table nicely. - when T is SomeFloat: - result = t.map_inline((x.formatBiggestFloat(precision = precision)).len).max - else: - result = t.map_inline(($x).len).max + result = t.map_inline(x.dispElement(precision = precision, specifier = specifier).len).max -proc prettyImpl*[T](t: Tensor[T], - inputRank = 0, alignBy = 0, alignSpacing = 4, - precision = -1): string = +proc dispTensorAsTable*[T](t: Tensor[T], + inputRank = 0, alignBy = 0, alignSpacing = 4, + precision = -1, + specifier: static string = ""): string = ## Pretty printing implementation that aligns N dimensional tensors as a ## table. Odd dimensions are stacked horizontally and even dimensions ## vertically. @@ -174,11 +304,12 @@ proc prettyImpl*[T](t: Tensor[T], ## and all others right aligned). ## ## `precision` sets the floating point precision. + const elementSpecifier = removeTensorFormatSpecifiers(specifier) var alignBy = alignBy var inputRank = inputRank if inputRank == 0: inputRank = t.rank - let largestElement = t.determineLargestElement(precision) + let largestElement = t.determineLargestElement(precision, elementSpecifier) alignBy = max(6, largestElement + alignSpacing) # for tensors of rank larger 2, walk axis 0 and stack vertically (even dim) # or stack horizontally (odd dim) @@ -189,8 +320,11 @@ proc prettyImpl*[T](t: Tensor[T], for ax in axis(t, 0): if oddRank: # 1. get next "column" - var toZip = prettyImpl(ax.squeeze, inputRank, alignBy = alignBy, - precision = precision) + var toZip = dispTensorAsTable(ax.squeeze, + inputRank, + alignBy = alignBy, + precision = precision, + specifier = elementSpecifier) # 2. center current "column" index to width of `toZip`, put on top toZip = center($axIdx, toZip.splitLines[0].len) & "\n" & toZip # 3. generate separator of "columns" and zip together @@ -198,8 +332,11 @@ proc prettyImpl*[T](t: Tensor[T], res = res.zipStrings(toZip, sep = sep, allowEmpty = false) else: # 1. get next "row" - var toStack = prettyImpl(ax.squeeze, inputRank, alignBy = alignBy, - precision = precision) + var toStack = dispTensorAsTable(ax.squeeze, + inputRank, + alignBy = alignBy, + precision = precision, + specifier = elementSpecifier) # 2. center current "row" index to height of `toStack` let leftIdx = genLeftIdx($axIdx, toStack) # 3. zip index and "row" @@ -215,4 +352,134 @@ proc prettyImpl*[T](t: Tensor[T], else: result = t.disp2d(alignBy = alignBy, alignSpacing = alignSpacing, - precision = precision).strip + precision = precision, + specifier = elementSpecifier).strip + +proc disp1dAsArray[T](t: Tensor[T], + sep = ", ", + precision = -1, specifier: static string = ""): string = + ## Display a 1D-tensor (only used for "array-style", i.e. non-table, printing) + if t.len == 0: + return "[]" + result = "[" + for value in t: + result &= dispElement(value, precision = precision, specifier = specifier) + result &= sep + # Remove the separator from the last element + result = result[0..^(1+sep.len)] & "]" + when T is Complex and "j" in specifier: + result = result.replace("(").replace(")") + +proc compactTensorDescription[T](t: Tensor[T]): string = + ## Describe the tensor in terms of its shape and type (in a "compact" way) + ## Only used for array-style printing + # Most if not all tensors element types are part of the system or complex + # modules so we can remove them from the type without much information loss + let compactType = t.type.name().replace("system.", "").replace("complex.", "") + let compactShape = ($t.shape)[1 ..< ^1].replace(", ", ",") + result = compactType & "<" & compactShape & ">" + +proc squeezeTopDimension[T](t: Tensor[T]): Tensor[T] = + ## Remove the top most dimension if its size is 1 + if t.shape.len == 0 or t.shape[0] > 1: + return t + var new_shape = t.shape + new_shape.delete(0) + result = t.reshape(new_shape) + +proc dispTensorAsSingleLineArrayImp[T](t: Tensor[T], + precision = -1, + specifier: static string = "", + indentSpacing = 0, + sep = ", ", rowSep = "" + ): string = + ## Implementation of the "array-style" tensor printing + result = "[" + if t.rank <= 1: + result = disp1dAsArray(t, sep = sep, precision = precision, specifier = specifier) + else: + var n = 0 + for ax in axis(t, 0): + var axRepr = dispTensorAsSingleLineArrayImp(ax.squeezeTopDimension(), + precision = precision, + specifier = specifier, + indentSpacing = indentSpacing, + sep = sep, rowSep = rowSep) + result &= axRepr + n += 1 + if n < t.shape[0]: + result &= sep & rowSep + result &= "]" + +proc dispTensorAsSingleLineArray*[T](t: Tensor[T], + precision = -1, + indentSpacing = 0, + specifier: static string = "", + sep = ", ", rowSep = "", + showHeader = true + ): string = + ## Display a tensor as a single line "array" + # Remove the non-standard specifier flags + const elementSpecifier = removeTensorFormatSpecifiers(specifier) + if showHeader: + result = t.compactTensorDescription & ":" & rowSep + result &= dispTensorAsSingleLineArrayImp(t, precision, specifier = elementSpecifier, rowSep = rowSep) + if t.storage.isNil: + # Return a useful message for uninit'd tensors instead of crashing + # Note that this should only happen when displaying tensors created + # by just declaring their type (e.g. `var t: Tensor[int]`), given that + # even tensors created by calling `newTensorUninit` have their storage + # initialized (with garbage) + result &= " (uninitialized)" + +proc indentTensorReprRows(s: string, indent: int): string = + ## Indent the lines of a multi-line "array-style" tensor representation + ## so that the right-most opening braces align vertically + if indent <= 0: + return s + for line in s.splitLines(): + var numBrackets = 0 + for c in line: + if c != '[': + break + numBrackets += 1 + result &= line.indent(indent - numBrackets) & "\n" + +proc dispTensorAsArray*[T](t: Tensor[T], + precision = -1, + specifier: static string = "", + showHeader = true): string = + ## Display a tensor as a multi-line "array" + result = t.dispTensorAsSingleLineArray( + precision = precision, specifier = specifier, rowSep="\n", showHeader = false) + result = indentTensorReprRows(result, t.rank).strip(leading=false) + if showHeader: + result = t.compactTensorDescription() & ":\n" & result + +proc prettyImpl*[T]( + t: Tensor[T], precision: int, specifier: static string): string = + ## Non public implementation of the pretty function + ## Three modes are supported: table, multi-line array and single-line array + const (dispMode, showHeader, elementSpecifier) = parseTensorFormatSpecifier(specifier) + if dispMode == single_line_array: + return t.dispTensorAsSingleLineArray( + precision = precision, specifier = elementSpecifier, showHeader = showHeader) + elif dispMode == multi_line_array: + return t.dispTensorAsArray( + precision = precision, specifier = elementSpecifier, showHeader = showHeader) + # Represent the tensor as a "pretty" table + var desc = t.type.name & " of shape \"" & $t.shape & "\" on backend \"" & "Cpu" & "\"" + if t.storage.isNil: # return useful message for uninit'd tensors instead of crashing + return "Uninitialized " & $desc + if showHeader: + desc &= "\n" + else: + desc = "" + if t.size() == 0: + return desc & " [] (empty)" + elif t.rank == 1: # for rank 1 we want an indentation, because we have no `|` + return desc & " " & t.dispTensorAsTable( + precision = precision, specifier = elementSpecifier) + else: + return desc & t.dispTensorAsTable( + precision = precision, specifier = elementSpecifier) diff --git a/tests/tensor/test_display.nim b/tests/tensor/test_display.nim index a1e029ed..e9b51371 100644 --- a/tests/tensor/test_display.nim +++ b/tests/tensor/test_display.nim @@ -13,7 +13,7 @@ # limitations under the License. import ../../src/arraymancer -import std / [unittest, sequtils, strutils] +import std / [unittest, sequtils, strutils, strformat] template compareStrings(t1, t2: string) = let t1S = t1.splitLines @@ -26,9 +26,15 @@ proc main() = suite "Displaying tensors": test "Display invalid tensor": var t: Tensor[int] - let exp = """ + block: + let exp = """ Uninitialized Tensor[system.int] of shape "[]" on backend "Cpu"""" - check $t == exp + check $t == exp + block: + let exp = """ +Tensor[int]<>: +[] (uninitialized)""" + check t.pretty("<:>") == exp test "Display 1D tensor": block: @@ -36,14 +42,24 @@ Uninitialized Tensor[system.int] of shape "[]" on backend "Cpu"""" compareStrings($t, """ Tensor[system.float] of shape "[2]" on backend "Cpu" 0.953293 0.129458""") + compareStrings(t.pretty("<:>"), """ +Tensor[float]<2>: +[0.953293, 0.129458]""") + block: let t = [1, 2, 3, 4].toTensor compareStrings($t, """Tensor[system.int] of shape "[4]" on backend "Cpu" 1 2 3 4""") + compareStrings(t.pretty("<:>"), """ +Tensor[int]<4>: +[1, 2, 3, 4]""") block: let t = ["foo", "bar", "hello world", "baz"].toTensor compareStrings($t, """Tensor[system.string] of shape "[4]" on backend "Cpu" foo bar hello world baz""") + compareStrings(t.pretty("<:>"), """ +Tensor[string]<4>: +[foo, bar, hello world, baz]""") block: # sequence of tensors still look a bit funky var ts = newSeq[Tensor[float]]() @@ -63,6 +79,9 @@ Tensor[system.float] of shape "[2]" on backend "Cpu" compareStrings($t_single_row, """ Tensor[system.int] of shape "[1, 5]" on backend "Cpu" |1 2 3 4 5|""") + compareStrings(t_single_row.pretty("<:>"), """ +Tensor[int]<1,5>: +[[1, 2, 3, 4, 5]]""") let t_single_column = t_single_row.transpose() compareStrings($t_single_column, """ Tensor[system.int] of shape "[5, 1]" on backend "Cpu" @@ -71,6 +90,13 @@ Tensor[system.int] of shape "[5, 1]" on backend "Cpu" | 3| | 4| | 5|""") + compareStrings(t_single_column.pretty("<:>"), """ +Tensor[int]<5,1>: +[[1], + [2], + [3], + [4], + [5]]""") test "Display 2D tensor (multi-column)": const @@ -102,6 +128,13 @@ Tensor[system.int] of shape "[5, 5]" on backend "Cpu" |3 9 27 81 243| |4 16 64 256 1024| |5 25 125 625 3125|""") + compareStrings(t_van.pretty("<:>"), """ +Tensor[int]<5,5>: +[[1, 1, 1, 1, 1], + [2, 4, 8, 16, 32], + [3, 9, 27, 81, 243], + [4, 16, 64, 256, 1024], + [5, 25, 125, 625, 3125]]""") test "Disp3d + Concat + SlicerMut bug with empty tensors": let a = [4, 3, 2, 1, 8, 7, 6, 5].toTensor.reshape(2, 1, 4) @@ -116,6 +149,14 @@ Tensor[system.int] of shape "[2, 3, 4]" on backend "Cpu" |5 6 7 8| |17 18 19 20| |9 10 11 12| |21 22 23 24| """) + compareStrings(t.pretty("<:>"), """ +Tensor[int]<2,3,4>: +[[[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12]], + [[13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24]]]""") test "Display 4D tensor": let t = toSeq(1..72).toTensor().reshape(2,3,4,3) @@ -134,6 +175,32 @@ Tensor[system.int] of shape "[2, 3, 4, 3]" on backend "Cpu" |46 47 48| |58 59 60| |70 71 72| -------------------------------------------------- """) + compareStrings(t.pretty("<:>"), """ +Tensor[int]<2,3,4,3>: +[[[[1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12]], + [[13, 14, 15], + [16, 17, 18], + [19, 20, 21], + [22, 23, 24]], + [[25, 26, 27], + [28, 29, 30], + [31, 32, 33], + [34, 35, 36]]], + [[[37, 38, 39], + [40, 41, 42], + [43, 44, 45], + [46, 47, 48]], + [[49, 50, 51], + [52, 53, 54], + [55, 56, 57], + [58, 59, 60]], + [[61, 62, 63], + [64, 65, 66], + [67, 68, 69], + [70, 71, 72]]]]""") test "Display 4D tensor with float values": let t = linspace(0.0, 100.0, 72).reshape(2,3,4,3) @@ -154,6 +221,32 @@ Tensor[system.float] of shape "[2, 3, 4, 3]" on backend "Cpu" |63.3803 64.7887 66.1972| |80.2817 81.6901 83.0986| |97.1831 98.5915 100| ----------------------------------------------------------------------------------------------- """) + compareStrings(t.pretty("<:>"), """ +Tensor[float]<2,3,4,3>: +[[[[0, 1.40845, 2.8169], + [4.22535, 5.6338, 7.04225], + [8.4507, 9.85915, 11.2676], + [12.6761, 14.0845, 15.493]], + [[16.9014, 18.3099, 19.7183], + [21.1268, 22.5352, 23.9437], + [25.3521, 26.7606, 28.169], + [29.5775, 30.9859, 32.3944]], + [[33.8028, 35.2113, 36.6197], + [38.0282, 39.4366, 40.8451], + [42.2535, 43.662, 45.0704], + [46.4789, 47.8873, 49.2958]]], + [[[50.7042, 52.1127, 53.5211], + [54.9296, 56.338, 57.7465], + [59.1549, 60.5634, 61.9718], + [63.3803, 64.7887, 66.1972]], + [[67.6056, 69.0141, 70.4225], + [71.831, 73.2394, 74.6479], + [76.0563, 77.4648, 78.8732], + [80.2817, 81.6901, 83.0986]], + [[84.507, 85.9155, 87.3239], + [88.7324, 90.1408, 91.5493], + [92.9577, 94.3662, 95.7746], + [97.1831, 98.5915, 100]]]]""") test "Display 4D tensor with float values and custom precision": let t = linspace(0.0, 100.0, 72).reshape(2,3,4,3) @@ -173,6 +266,32 @@ Tensor[system.float] of shape "[2, 3, 4, 3]" on backend "Cpu" |63.38 64.79 66.20| |80.28 81.69 83.10| |97.18 98.59 100.0| ----------------------------------------------------------------------------- """) + compareStrings(t.pretty("<:>"), """ +Tensor[float]<2,3,4,3>: +[[[[0, 1.40845, 2.8169], + [4.22535, 5.6338, 7.04225], + [8.4507, 9.85915, 11.2676], + [12.6761, 14.0845, 15.493]], + [[16.9014, 18.3099, 19.7183], + [21.1268, 22.5352, 23.9437], + [25.3521, 26.7606, 28.169], + [29.5775, 30.9859, 32.3944]], + [[33.8028, 35.2113, 36.6197], + [38.0282, 39.4366, 40.8451], + [42.2535, 43.662, 45.0704], + [46.4789, 47.8873, 49.2958]]], + [[[50.7042, 52.1127, 53.5211], + [54.9296, 56.338, 57.7465], + [59.1549, 60.5634, 61.9718], + [63.3803, 64.7887, 66.1972]], + [[67.6056, 69.0141, 70.4225], + [71.831, 73.2394, 74.6479], + [76.0563, 77.4648, 78.8732], + [80.2817, 81.6901, 83.0986]], + [[84.507, 85.9155, 87.3239], + [88.7324, 90.1408, 91.5493], + [92.9577, 94.3662, 95.7746], + [97.1831, 98.5915, 100]]]]""") test "Display 5D tensor": let t1 = toSeq(1..144).toTensor().reshape(2,3,4,3,2) @@ -195,6 +314,20 @@ Tensor[system.int] of shape "[2, 3, 4, 3, 2]" on backend "Cpu" |53 54| |59 60| |65 66| |71 72| | |125 126| |131 132| |137 138| |143 144| --------------------------------------------------- | --------------------------------------------------- """) + compareStrings(t1.reshape(2,2,1,3,12).pretty("<:>"), """ +Tensor[int]<2,2,1,3,12>: +[[[[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]]], + [[[37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48], + [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60], + [61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72]]]], + [[[[73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84], + [85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96], + [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108]]], + [[[109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120], + [121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132], + [133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144]]]]]""") let t2 = toSeq(1..72*3).toTensor().reshape(3,3,4,3,2) compareStrings($t2, """ @@ -238,6 +371,44 @@ Tensor[system.int] of shape "[3, 3, 4, 3, 2]" on backend "Cpu" |1000000052 1000000053| |1000000058 1000000059| |1000000064 1000000065| |1000000070 1000000071| | |1000000124 1000000125| |1000000130 1000000131| |1000000136 1000000137| |1000000142 1000000143| | |1000000196 1000000197| |1000000202 1000000203| |1000000208 1000000209| |1000000214 1000000215| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- """) + compareStrings(t.reshape(2,3,1,6,6).pretty("<:>"), """ +Tensor[int]<2,3,1,6,6>: +[[[[[1000000000, 1000000001, 1000000002, 1000000003, 1000000004, 1000000005], + [1000000006, 1000000007, 1000000008, 1000000009, 1000000010, 1000000011], + [1000000012, 1000000013, 1000000014, 1000000015, 1000000016, 1000000017], + [1000000018, 1000000019, 1000000020, 1000000021, 1000000022, 1000000023], + [1000000024, 1000000025, 1000000026, 1000000027, 1000000028, 1000000029], + [1000000030, 1000000031, 1000000032, 1000000033, 1000000034, 1000000035]]], + [[[1000000036, 1000000037, 1000000038, 1000000039, 1000000040, 1000000041], + [1000000042, 1000000043, 1000000044, 1000000045, 1000000046, 1000000047], + [1000000048, 1000000049, 1000000050, 1000000051, 1000000052, 1000000053], + [1000000054, 1000000055, 1000000056, 1000000057, 1000000058, 1000000059], + [1000000060, 1000000061, 1000000062, 1000000063, 1000000064, 1000000065], + [1000000066, 1000000067, 1000000068, 1000000069, 1000000070, 1000000071]]], + [[[1000000072, 1000000073, 1000000074, 1000000075, 1000000076, 1000000077], + [1000000078, 1000000079, 1000000080, 1000000081, 1000000082, 1000000083], + [1000000084, 1000000085, 1000000086, 1000000087, 1000000088, 1000000089], + [1000000090, 1000000091, 1000000092, 1000000093, 1000000094, 1000000095], + [1000000096, 1000000097, 1000000098, 1000000099, 1000000100, 1000000101], + [1000000102, 1000000103, 1000000104, 1000000105, 1000000106, 1000000107]]]], + [[[[1000000108, 1000000109, 1000000110, 1000000111, 1000000112, 1000000113], + [1000000114, 1000000115, 1000000116, 1000000117, 1000000118, 1000000119], + [1000000120, 1000000121, 1000000122, 1000000123, 1000000124, 1000000125], + [1000000126, 1000000127, 1000000128, 1000000129, 1000000130, 1000000131], + [1000000132, 1000000133, 1000000134, 1000000135, 1000000136, 1000000137], + [1000000138, 1000000139, 1000000140, 1000000141, 1000000142, 1000000143]]], + [[[1000000144, 1000000145, 1000000146, 1000000147, 1000000148, 1000000149], + [1000000150, 1000000151, 1000000152, 1000000153, 1000000154, 1000000155], + [1000000156, 1000000157, 1000000158, 1000000159, 1000000160, 1000000161], + [1000000162, 1000000163, 1000000164, 1000000165, 1000000166, 1000000167], + [1000000168, 1000000169, 1000000170, 1000000171, 1000000172, 1000000173], + [1000000174, 1000000175, 1000000176, 1000000177, 1000000178, 1000000179]]], + [[[1000000180, 1000000181, 1000000182, 1000000183, 1000000184, 1000000185], + [1000000186, 1000000187, 1000000188, 1000000189, 1000000190, 1000000191], + [1000000192, 1000000193, 1000000194, 1000000195, 1000000196, 1000000197], + [1000000198, 1000000199, 1000000200, 1000000201, 1000000202, 1000000203], + [1000000204, 1000000205, 1000000206, 1000000207, 1000000208, 1000000209], + [1000000210, 1000000211, 1000000212, 1000000213, 1000000214, 1000000215]]]]]""") test "Display 5D tensor with string elements": let t = toSeq(1..72).mapIt("Value: " & $it).toTensor.reshape(2,3,3,4) @@ -254,12 +425,121 @@ Tensor[system.string] of shape "[2, 3, 3, 4]" on backend "Cpu" |Value: 45 Value: 46 Value: 47 Value: 48| |Value: 57 Value: 58 Value: 59 Value: 60| |Value: 69 Value: 70 Value: 71 Value: 72| -------------------------------------------------------------------------------------------------------------------------------------------------------- """) + compareStrings(t.pretty("<:>"), """ +Tensor[string]<2,3,3,4>: +[[[[Value: 1, Value: 2, Value: 3, Value: 4], + [Value: 5, Value: 6, Value: 7, Value: 8], + [Value: 9, Value: 10, Value: 11, Value: 12]], + [[Value: 13, Value: 14, Value: 15, Value: 16], + [Value: 17, Value: 18, Value: 19, Value: 20], + [Value: 21, Value: 22, Value: 23, Value: 24]], + [[Value: 25, Value: 26, Value: 27, Value: 28], + [Value: 29, Value: 30, Value: 31, Value: 32], + [Value: 33, Value: 34, Value: 35, Value: 36]]], + [[[Value: 37, Value: 38, Value: 39, Value: 40], + [Value: 41, Value: 42, Value: 43, Value: 44], + [Value: 45, Value: 46, Value: 47, Value: 48]], + [[Value: 49, Value: 50, Value: 51, Value: 52], + [Value: 53, Value: 54, Value: 55, Value: 56], + [Value: 57, Value: 58, Value: 59, Value: 60]], + [[Value: 61, Value: 62, Value: 63, Value: 64], + [Value: 65, Value: 66, Value: 67, Value: 68], + [Value: 69, Value: 70, Value: 71, Value: 72]]]]""") + + + test "Format-strings (1D tensor)": + let t = arange(-2, 10) + + # Table-style tensor format strings + compareStrings(&"{t}", """ +Tensor[system.int] of shape "[12]" on backend "Cpu" + -2 -1 0 1 2 3 4 5 6 7 8 9""") + check &"{t}" == &"{t:<>||}" + check &"{t:||}" == " -2 -1 0 1 2 3 4 5 6 7 8 9" + + # Single-line array-style format strings + check &"{t:[]}" == "[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + check &"{t:<>}" == "Tensor[int]<12>:[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + check &"{t:<>}" == &"{t:<>[]}" + check &"{t:<>}" == t.pretty("<>") + + # Multi-line array-style format strings + check &"{t:[:]}" == "[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + check &"{t:<:>}" == "Tensor[int]<12>:\n[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + check &"{t:<:>}" == &"{t:<>[:]}" + + + test "Format-strings (3D tensor)": + let t = arange(-2, 10).reshape(2, 2, 3) + + # Table-style tensor format strings + compareStrings(&"{t}", """ +Tensor[system.int] of shape "[2, 2, 3]" on backend "Cpu" + 0 1 +|-2 -1 0| |4 5 6| +|1 2 3| |7 8 9| +""") + check &"{t}" == &"{t:<>||}" + check &"{t:<|>}" == &"{t:<>||}" + compareStrings(&"{t:||}", """ + 0 1 +|-2 -1 0| |4 5 6| +|1 2 3| |7 8 9| +""") + + # Single-line array-style format strings + check &"{t:[]}" == "[[[-2, -1, 0], [1, 2, 3]], [[4, 5, 6], [7, 8, 9]]]" + check &"{t:<>}" == "Tensor[int]<2,2,3>:[[[-2, -1, 0], [1, 2, 3]], [[4, 5, 6], [7, 8, 9]]]" + check &"{t:<>}" == &"{t:<>[]}" + check &"{t:<>}" == t.pretty("<>") + + # Multi-line array-style format strings + compareStrings(&"{t:[:]}", """ +[[[-2, -1, 0], + [1, 2, 3]], + [[4, 5, 6], + [7, 8, 9]]]""") + compareStrings(&"{t:<:>}", """ +Tensor[int]<2,2,3>: +[[[-2, -1, 0], + [1, 2, 3]], + [[4, 5, 6], + [7, 8, 9]]]""") + check &"{t:<:>}" == &"{t:<>[:]}" + + test "Complex format specifiers": + let t_int = arange(-2, 22, 4).reshape(2, 3) + compareStrings(&"{t_int:X<>}", """ +Tensor[int]<2,3>:[[-2, 2, 6], [A, E, 12]]""") + + let t_float = arange(-2.0, 22.0, 4.0).reshape(2, 3) + compareStrings(&"{t_float:+06.2f[:]}", """ +[[-02.00, +02.00, +06.00], + [+10.00, +14.00, +18.00]]""") + check &"{t_float:+06.2f[:]}" == &"{t_float:[:]+06.2f}" + check &"{t_float:+06.2f[]}" == &"{t_float:[+06.2f]}" + check &"{t_float:+06.2f[:]}" == &"{t_float:[:+06.2f:]}" + check &"{t_float:+06.2f[:]}" == &"{t_float:[:+06.2f]}" + + test "Invalid tensor formats detected": + when compiles(&"{t_float:+[]06.2f}"): check false + when compiles(&"{t_float:+<>[]06.2f}"): check false + when compiles(&"{t_float:<>[+06.2f|}"): check false + when compiles(&"{t_float:+<>06.2f}"): check false + when compiles(&"{t_float:+[:]06.2f}"): check false + when compiles(&"{t_float:[+06.2f:]}"): check false + when compiles(&"{t_float:+<>[:]06.2f}"): check false + when compiles(&"{t_float:+<:>06.2f}"): check false + when compiles(&"{t_float:+<>||06.2f}"): check false + when compiles(&"{t_float:+<|>06.2f}"): check false test "Displaying of unininitialized tensors works": template checkTypes(typ: untyped): untyped = var x: Tensor[typ] var exp = "Uninitialized Tensor[system." & astToStr(typ) & """] of shape "[]" on backend "Cpu"""" + var exp_array = "Tensor[" & astToStr(typ) & "]<>:[] (uninitialized)" check $x == exp + check x.pretty("<>") == exp_array checkTypes(int) checkTypes(char) checkTypes(float)