From cd02bf70b39dc7a8fc613be02a5a1d8725821255 Mon Sep 17 00:00:00 2001 From: Angel Ezquerra Date: Fri, 11 Oct 2024 10:49:28 +0200 Subject: [PATCH] Add "format string" support for tensors (#655) This comes in 2 forms: 1. Add a version of the pretty function that takes a format string as its input instead of a precision value. Also add a new "showHeader" argument to the original pretty function. - This new version of pretty lets you specify the format string that must be used to display each element. It also adds some new tensor-specific format string "tokens" that are used to control how tensors are displayed (beyond the format of each element). See below for more details. 2. Add a formatValue procedure that takes a tensor as its first input. This makes it possible to control how tensors are displayed in format strings, in the exact same way as if you were using the new pretty(specifier) procedure. This new formatValue procedure takes 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) which are used to configure of each tensor element is displayed. In addition to those standard format specifiers 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. Fixes after PR comments More fixes --- src/arraymancer/tensor/display.nim | 174 ++++++++++- src/arraymancer/tensor/private/p_display.nim | 313 +++++++++++++++++-- tests/tensor/test_display.nim | 286 ++++++++++++++++- 3 files changed, 733 insertions(+), 40 deletions(-) diff --git a/src/arraymancer/tensor/display.nim b/src/arraymancer/tensor/display.nim index 0d35f7654..77c7114d6 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 7995b8ecd..5118b5ba9 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 a1e029ed7..e9b513712 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)