diff --git a/src/arraymancer/tensor/display.nim b/src/arraymancer/tensor/display.nim index 0d35f765..9ed9b013 100644 --- a/src/arraymancer/tensor/display.nim +++ b/src/arraymancer/tensor/display.nim @@ -13,22 +13,160 @@ # 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 dscription 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, + ## with the addition of 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 the tensor in a single + ## line. No header is shown. + ## - "<>": Combined with the 2 above (i.e. "<>[:]" or "<>[]") + ## adds a header with basic tensor info (type and + ## shape). "<:>" can be used as a shortcut for "<>[:]" + ## while "<>" on its own is equivalent to "<>[]". + ## Can also be combined with "<>||" (see below). + ## - "||": "Pretty-print" the tensor _without_ a header. This + ## can also be combined with "<>" (i.e. "<>||") to + ## explicitly enable the default mode, which is pretty + ## printing with a header. + ## Notes: + ## - Note that in addition to these we support 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). For a list of supported + ## format specifiers please check the documentation of nim's `strformat` + ## module. + ## - This version of this function does not have a `showHeader` argument + ## because to disable the header you must add a "n" to the 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("[]") + ## # Tensor[float]<2,3>:[[-2.00, -1.00, +0.00], [+1.00, +2.00, +3.00]] + ## + ## # You can also enable the header using "<>" or "<>[]" + ## echo t_int.pretty("<>") + ## # [[-2.00, -1.00, +0.00], [+1.00, +2.00, +3.00]] + ## + ## # 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]] + ## ``` + 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`. It makes little + ## sense to call this directly, but it is required to exist + ## by the `&` macro. + ## + ## For Tensors, we add some additional specifiers 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 the tensor in a single + ## line. No header is shown. + ## - "<>": Combined with the 2 above (i.e. "<>[:]" or "<>[]") + ## adds a header with basic tensor info (type and + ## shape). "<:>" can be used as a shortcut for "<>[:]" + ## while "<>" on its own is equivalent to "<>[]". + ## Can also be combined with "<>||" (see below). + ## - "||": "Pretty-print" the tensor _without_ a header. This + ## can also be combined with "<>" (i.e. "<>||") to + ## explicitly enable the default mode, which is pretty + ## printing with a header. + ## - 'j': Formats complex values as (A+Bj) like in mathematics. + ## Ignored for non Complex tensors + 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..cbab161f 100644 --- a/src/arraymancer/tensor/private/p_display.nim +++ b/src/arraymancer/tensor/private/p_display.nim @@ -14,8 +14,64 @@ import ../../private/functional, ../higher_order_applymap, ../shapeshifting, ../data_structure, - ../accessors, - sequtils, strutils + ../accessors +import std / [sequtils, strutils, strformat, typetraits] + +# We use different specifier string "tokens" to set the tensor display mode: +# - || or <>||: forces "table mode" (without or with header) +# - [:] or <>[:] (or the equivalent <:>): multi line array mode (w/o or w/ header) +# - [] or <>[] (or the equivalent <>): single line array mode (w/o or w/ header) +# If none of those tokens is found use table mode +# +func isTensorHeaderEnabled(specifier: string): bool = + ## Return true when a header must be shown based on the format specifier + # Show a header when "<>" (i.e. the explicit header marker) or "<:>" (i.e. the + # combined <>[:] shortcut) is found + # Otherwise do not show a header if "||" or "[]" or "[:]" is found + # Show a header in all other cases + # if "<>" in specifier or "<:>" in specifier: + if "<" in specifier and ">" in specifier: + # Note that this also covers the "<>[]" and "<>||" cases + return true + elif specifier.count('|') == 2 or ("[" in specifier and "]" in specifier): + return false + return true + +type tensorDispMode = enum table, multi_line_array, single_line_array + +func getTensorDispMode(specifier: string): tensorDispMode = + ## Get the display mode based on the special "tensor specifier tokens" + let brackets = "[" in specifier and "]" in specifier + let colon_brackets = specifier.count(':') == 2 + let logic_brackets = "<" in specifier and ">" in specifier + let flat_brackets = specifier.count('|') == 2 + if flat_brackets: + # Note that "<>||" also implies "table" mode! + table + elif colon_brackets or + (":" in specifier and (brackets or logic_brackets)): + # <:> is a shortcut for <>[:] + multi_line_array + elif brackets or logic_brackets: + # <> is a shortcut for <>[] _when_ not combined with ||, ::, [:] or [::] + single_line_array + else: + # In all other cases, default to "table" mode! + table + +func removeTensorFormatSpecifiers(specifier: string): string = + ## Remove the special "tensor specifier tokens" + ## + ## This must be called _after_ getting the display mode (using + ## `getTensorDispMode`) and the "show header flat" (using + ## `isTensorHeaderEnabled`), to avoid an error when the specifier + ## is finally passed to the tensor element `formatValue` functions + result = specifier.replace("::") + .replace("[:]") + .replace("<:>") + .replace("<>") + .replace("[]") + .replace("||") func bounds_display(t: Tensor, idx_data: tuple[val: string, idx: int], @@ -78,17 +134,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 +215,19 @@ 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 + # 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 +239,12 @@ proc prettyImpl*[T](t: Tensor[T], ## and all others right aligned). ## ## `precision` sets the floating point precision. + const specifier = removeTensorFormatSpecifiers(specifier) var alignBy = alignBy var inputRank = inputRank if inputRank == 0: inputRank = t.rank - let largestElement = t.determineLargestElement(precision) + let largestElement = t.determineLargestElement(precision, specifier) 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 +255,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 = specifier) # 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 +267,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 = specifier) # 2. center current "row" index to height of `toStack` let leftIdx = genLeftIdx($axIdx, toStack) # 3. zip index and "row" @@ -215,4 +287,136 @@ proc prettyImpl*[T](t: Tensor[T], else: result = t.disp2d(alignBy = alignBy, alignSpacing = alignSpacing, - precision = precision).strip + precision = precision, + specifier = specifier).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 must 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 (n, a and s) + const specifier = removeTensorFormatSpecifiers(specifier) + if showHeader: + result = t.compactTensorDescription & ":" & rowSep + result &= dispTensorAsSingleLineArrayImp(t, precision, specifier = specifier, 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 + let showHeader = isTensorHeaderEnabled(specifier) + let dispMode = getTensorDispMode(specifier) + const specifier = removeTensorFormatSpecifiers(specifier) + if dispMode == single_line_array: + return t.dispTensorAsSingleLineArray( + precision = precision, specifier = specifier, showHeader = showHeader) + elif dispMode == multi_line_array: + return t.dispTensorAsArray( + precision = precision, specifier = specifier, 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 = specifier) + else: + return desc & t.dispTensorAsTable( + precision = precision, specifier = specifier) diff --git a/tests/tensor/test_display.nim b/tests/tensor/test_display.nim index a1e029ed..17e179a5 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,104 @@ 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:<>||}" + 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]]""") 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)