diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index d4ac8cfe4..32be8a89a 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -1,7 +1,14 @@ -# This file imports pandas, so it should only be imported when formatting +# The rich repr and _repr_html_ are hand-rendered here to reproduce pandas' +# DataFrame text/HTML output byte-for-byte without importing pandas. When pandas +# is installed we still read its display.* options so a user's option_context is +# honored; when it is absent we fall back to pandas' documented defaults. +import math +import re +import shutil + import numpy as np -from .. import backend, config, monoid, unary +from .. import backend, config, unary from ..dtypes import BOOL from ..exceptions import OutOfMemory from .infix import _is_literal @@ -15,6 +22,35 @@ except ImportError: # pragma: no cover (import) has_pandas = False +# pandas display.* defaults, used verbatim when pandas is not installed so the +# hand renderer produces the same output it would with a freshly imported pandas. +_DISPLAY_DEFAULTS = { + "max_rows": 60, + "min_rows": 10, + "max_columns": 0, + "width": 80, + "expand_frame_repr": True, + "precision": 6, + "max_colwidth": 50, + "chop_threshold": None, + "colheader_justify": "right", + "float_format": None, + "html.border": 1, + "html.use_mathjax": True, +} + + +def _display_option(name): + """Read a pandas display. option, or fall back to its default. + + Reading from pandas keeps a user's ``pd.option_context`` honored while the + rendering logic itself stays pandas-free. + """ + if has_pandas: + return pd.get_option(f"display.{name}") + return _DISPLAY_DEFAULTS[name] + + # This was written by a complete novice at CSS. # If you can help make it better, please do! CSS_STYLE = """ @@ -139,22 +175,15 @@ def _update_matrix_array(arr, matrix, rows, row_offset, columns, column_offset, if type(matrix) is TransposedMatrix: parent = matrix._matrix submatrix = Matrix(parent.dtype, parent._nrows, parent._ncols, name="") - if parent._nvals > 0: # pragma: no branch - # Get val to support iso-valued matrices - val = parent.reduce_scalar(monoid.any, allow_empty=False).new(name="") - submatrix(parent.S)[columns, rows] = val - submatrix(submatrix.S)[...] = parent + submatrix[columns, rows] = parent[columns, rows].new(name="") if row_offset is not None or column_offset is not None: submatrix = submatrix[column_offset:, row_offset:].new(name="") submatrix = submatrix.T else: if mask is None: + # Extract and assign need no operator, which a UDT lacks without Numba submatrix = Matrix(matrix.dtype, matrix._nrows, matrix._ncols, name="") - # Get val to support iso-valued matrices - if matrix._nvals > 0: - val = matrix.reduce_scalar(monoid.any).new(name="") - submatrix(matrix.S)[rows, columns] = val - submatrix(submatrix.S)[...] = matrix + submatrix[rows, columns] = matrix[rows, columns].new(name="") else: submatrix = Matrix("UINT8", matrix._nrows, matrix._ncols, name="") if mask.structure: @@ -191,12 +220,9 @@ def _update_vector_array(arr, vector, columns, column_offset, *, mask=None): subvector(vector.V)[...] = 0 if mask.complement else 1 else: if mask is None: + # Extract and assign need no operator, which a UDT lacks without Numba subvector = Vector(vector.dtype, vector._size, name="") - # Get val to support iso-valued vectors - if vector._nvals > 0: - val = vector.reduce(monoid.any).new(name="") - subvector(vector.S)[columns] = val - subvector(subvector.S)[...] = vector + subvector[columns] = vector[columns].new(name="") else: subvector = Vector("UINT8", vector._size, name="") if mask.structure: @@ -219,13 +245,53 @@ def _update_vector_array(arr, vector, columns, column_offset, *, mask=None): arr[0, cols[np.isnan(vals)]] = "nan" -def _get_max_columns(): - max_columns = pd.options.display.max_columns - if max_columns == 0: - # We are probably in a terminal and pandas will automatically size the data correctly. - # In this case, let's get a sufficiently large amount of data to show and defer to pandas. - max_columns = 150 - return max_columns +# The most rows or columns a repr reads when the display options set no limit: a max_columns of +# 0 (as many as fit the terminal, which the renderer then cuts to its width) or None. pandas +# takes None as no limit at all, but a repr cannot read every row of a Matrix with 2**60. +_NO_LIMIT = 150 + + +def _display_limits(): + """display.max_rows, min_rows and max_columns as the renderer uses them, None capped. + + A max_rows of None reads as ``_NO_LIMIT + 1`` with a min_rows of ``_NO_LIMIT``, whatever + min_rows is (pandas ignores it then): a longer frame shows its first and last + ``_NO_LIMIT // 2`` rows, and a COO form its first ``_NO_LIMIT`` entries and the "..." row, + which a max_rows of ``_NO_LIMIT`` would cut again. A min_rows of None is max_rows, as in + pandas, and a max_columns of None is ``_NO_LIMIT``. + """ + max_rows = _display_option("max_rows") + min_rows = _display_option("min_rows") + max_columns = _display_option("max_columns") + if max_rows is None: + max_rows = _NO_LIMIT + 1 + min_rows = _NO_LIMIT + elif min_rows is None: + min_rows = max_rows + if max_columns is None: + max_columns = _NO_LIMIT + return max_rows, min_rows, max_columns + + +def _get_limits(max_rows, min_rows, max_columns): + """The limits of the rows and columns to read: the display options for any not given.""" + display_max_rows, display_min_rows, display_max_columns = _display_limits() + if max_rows is None: # pragma: no branch + max_rows = display_max_rows + if min_rows is None: # pragma: no branch + min_rows = display_min_rows + if max_columns is None: # pragma: no branch + max_columns = display_max_columns + if max_columns == 0: + # We are probably in a terminal and the renderer will size the data to fit it. + # In this case, let's get a sufficiently large amount of data to show. + max_columns = _NO_LIMIT + return max_rows, min_rows, max_columns + + +def _terminal_rows(): + # rows available for data: terminal height minus dots + prompt + header + return shutil.get_terminal_size()[1] - 3 def _get_chunk(length, min_length, max_length): @@ -233,25 +299,670 @@ def _get_chunk(length, min_length, max_length): chunk = list(range(length)) chunk_groups = [(None, None)] else: - half = min_length // 2 + # The renderer cuts a frame longer than max_length to the first and last half of + # min(min_length, max_length) items, or of max_length when min_length is 0. Only + # those are read: the frame is one item longer than max_length, and the items + # between them are blank and cut. + shown = min(min_length, max_length) if min_length else max_length + half = shown // 2 + chunk = list(range(max_length + 1)) + if half == 0: + # With room for one item the renderer shows the first max_length items + # instead of a head and a tail. + return chunk, [(list(range(max_length)), None)] first_chunk = list(range(half)) second_chunk = list(range(length - half, length)) - chunk = list(range(max_length + 1)) chunk[-half:] = second_chunk chunk_groups = [(first_chunk, None), (second_chunk, length - len(chunk))] return chunk, chunk_groups +class _Column: + """One rendered column: a label, its raw cell values, and how to format them. + + ``kind`` selects the pandas array formatter this column would have used: + "object" (GenericArrayFormatter), "float" (FloatArrayFormatter, also complex), + or "int" (IntArrayFormatter). ``numeric`` mirrors pandas ``is_numeric_dtype`` + and controls whether the column header gets a leading space. + """ + + __slots__ = ("label", "values", "kind", "numeric") + + def __init__(self, label, values, kind, numeric): + self.label = label + self.values = values + self.kind = kind + self.numeric = numeric + + +class _GBFrame: + """A minimal object/typed-column table, standing in for a pandas DataFrame. + + ``col_name`` is the columns' index name (pandas ``df.columns.name``); when set + it appears in the top-left corner cell, as vector reprs rely on. + """ + + __slots__ = ("columns", "index", "col_name") + + def __init__(self, columns, index, col_name=None): + self.columns = columns + self.index = index + self.col_name = col_name + + @property + def ncols(self): + return len(self.columns) + + @property + def nrows(self): + return len(self.index) + + def slice_cols(self, idx): + return _GBFrame([self.columns[i] for i in idx], self.index, self.col_name) + + def slice_rows(self, idx): + columns = [ + _Column(c.label, [c.values[i] for i in idx], c.kind, c.numeric) for c in self.columns + ] + return _GBFrame(columns, [self.index[i] for i in idx], self.col_name) + + +def _isna_cell(x): + # Gaps in the dense grid are float NaN; present values (including displayed + # "nan"/"inf") are never a bare float NaN, so this only flags the gaps. + return x is None or (isinstance(x, float) and math.isnan(x)) + + +def _count_present(arr): + return sum(1 for x in arr.flat if not _isna_cell(x)) + + +def _dtype_kind_numeric(dtype): + kind = dtype.kind + if kind in "fc": + return "float", True + if kind in "iu": + return "int", True + if kind == "b": + # bool renders via the generic formatter, but is_numeric_dtype(bool) is True + return "object", True + return "object", False + + +def _make_dense_frame(arr, columns, index): + nrows = len(index) + out = [] + for j, label in enumerate(columns): + vals = [("" if _isna_cell(arr[i, j]) else arr[i, j]) for i in range(nrows)] + out.append(_Column(label, vals, "object", False)) + return _GBFrame(out, list(index)) + + +def _make_coo_frame(label_arrays, add_dots): + n = len(label_arrays[0][1]) + index = list(range(n)) + out = [] + for label, values in label_arrays: + values = np.asarray(values) + if add_dots: + out.append(_Column(label, [*values.tolist(), "..."], "object", False)) + elif values.ndim > 1: + # Array UDT values: a list per entry, as the dense grid shows them + out.append(_Column(label, values.tolist(), "object", False)) + else: + kind, numeric = _dtype_kind_numeric(values.dtype) + out.append(_Column(label, values.tolist(), kind, numeric)) + if add_dots: + index.append("...") + return _GBFrame(out, index) + + +# --- cell formatting (reproduces pandas array formatters for object/int/float) --- + +_NUMBER_RE = re.compile(r"^\s*[\+-]?[0-9]+\.[0-9]*$") + + +def _is_float_scalar(v): + # Matches pandas.lib.is_float: python/numpy floats, but not bool/int/complex. + return isinstance(v, (float, np.floating)) + + +def _pprint(v): + # Reproduces pandas printing.pprint_thing for our cell types (escape_chars for + # tab/cr/nl, quote_strings=False): scalars -> str, sequences recurse. + if isinstance(v, (list, tuple)): + body = ", ".join(_pprint(e) for e in v) + if isinstance(v, tuple) and len(v) == 1: + body += "," + return f"[{body}]" if isinstance(v, list) else f"({body})" + s = str(v) + return s.replace("\t", r"\t").replace("\r", r"\r").replace("\n", r"\n") + + +def _trim_zeros_single_float(s): + s = s.rstrip("0") + if s.endswith("."): + s += "0" + return s + + +def _trim_zeros_float(str_floats): + trimmed = list(str_floats) + + def is_number_with_decimal(x): + return _NUMBER_RE.match(x) is not None + + def should_trim(values): + numbers = [x for x in values if is_number_with_decimal(x)] + return len(numbers) > 0 and all(x.endswith("0") for x in numbers) + + while should_trim(trimmed): + trimmed = [x[:-1] if is_number_with_decimal(x) else x for x in trimmed] + return [x + "0" if is_number_with_decimal(x) and x.endswith(".") else x for x in trimmed] + + +def _trim_zeros_complex(str_complexes): + real_part, imag_part = [], [] + for x in str_complexes: + trimmed = re.split(r"(?{padded_length}}" + "j" + for real_pt, imag_pt in zip(padded_parts[:n], padded_parts[n:], strict=True) + ] + + +def _value_formatter(fmt_str, threshold): + def base(v): + return fmt_str.format(value=v) + + if threshold is None: + return base + + def formatter(v): + return base(v) if abs(v) > threshold else base(0.0) + + return formatter + + +def _format_reals_with_na(values, formatter, na_rep): + return [na_rep if (v != v) else formatter(v) for v in values] + + +def _format_complex_with_na(values, formatter, na_rep): + out = [] + for val in values: + re_v, im_v = val.real, val.imag + re_na, im_na = re_v != re_v, im_v != im_v + if not re_na and not im_na: + out.append(formatter(val)) + elif not re_na: + out.append(f"{formatter(re_v)}+{na_rep}j") + elif not im_na: + imag_formatted = formatter(im_v).strip() + if imag_formatted.startswith("-"): + out.append(f"{na_rep}{imag_formatted}j") + else: + out.append(f"{na_rep}+{imag_formatted}j") + else: + out.append(f"{na_rep}+{na_rep}j") + return out + + +def _format_float_column(values, digits): + # Reproduces FloatArrayFormatter for fixed_width, leading_space=True, na_rep="NaN". + arr = np.asarray(values) + is_complex = np.iscomplexobj(arr) + na_rep = "NaN" + if (float_format := _display_option("float_format")) is not None: + # A user display.float_format callable makes FloatArrayFormatter drop + # fixed_width: each value (real or complex) is just float_format(value), + # with no trailing-zero trim and no scientific switchover. Iterate the + # numpy array (not .tolist()) so the callable receives numpy scalars, as + # pandas does; e.g. a "%f"-style callable then casts complex the same way. + return [na_rep if (v != v) else float_format(v) for v in arr] + seq = arr.tolist() + threshold = _display_option("chop_threshold") + + def format_with(fmt_str): + formatter = _value_formatter(fmt_str, threshold) + if is_complex: + return _trim_zeros_complex(_format_complex_with_na(seq, formatter, na_rep)) + return _trim_zeros_float(_format_reals_with_na(seq, formatter, na_rep)) + + result = format_with(f"{{value: .{digits:d}f}}") + too_long = bool(result) and max(len(x) for x in result) > digits + 6 + abs_vals = np.abs(arr) + has_large = bool((abs_vals > 1e6).any()) + has_small = bool(((abs_vals < 10.0 ** (-digits)) & (abs_vals > 0)).any()) + if has_small or (too_long and has_large): + result = format_with(f"{{value: .{digits:d}e}}") + return list(result) + + +def _justify(strings, width, mode="right"): + if mode == "left": + return [x.ljust(width) for x in strings] + if mode == "center": + return [x.center(width) for x in strings] + return [x.rjust(width) for x in strings] + + +def _make_fixed_width(strings, justify="right", minimum=None): + if not strings: + return list(strings) + max_len = max(len(x) for x in strings) + if minimum is not None: + max_len = max(minimum, max_len) + conf_max = _display_option("max_colwidth") + if conf_max is not None and max_len > conf_max: + max_len = conf_max + + def just(x): + if conf_max is not None and conf_max > 3 and len(x) > max_len: + x = x[: max_len - 3] + "..." + return x + + return _justify([just(x) for x in strings], max_len, justify) + + +def _format_labels(labels): + # Reproduce pandas Index._format_flat(include_name=False) for our label types: + # integer labels get a sign column and are made one width, left-justified and cut + # short at display.max_colwidth, then the leading spaces they all share are + # trimmed; string labels are left as-is. + if labels and all(isinstance(x, (int, np.integer)) and not isinstance(x, bool) for x in labels): + strs = _make_fixed_width([f"{x: d}" for x in labels], "left") + while all(s.startswith(" ") for s in strs): + strs = [s[1:] for s in strs] + return strs + return [str(x) for x in labels] + + +def _adjoin(space, lists): + # Port of pandas printing.adjoin (ascii len/ljust); glues columns with `space`. + lengths = [max(map(len, x)) + space for x in lists[:-1]] + lengths.append(max(map(len, lists[-1]))) + max_len = max(map(len, lists)) + padded = [] + for i, lst in enumerate(lists): + nl = [x.ljust(lengths[i]) for x in lst] + nl = [" " * lengths[i]] * (max_len - len(lst)) + nl + padded.append(nl) + return "\n".join("".join(parts) for parts in zip(*padded, strict=True)) + + +def _binify(cols, line_width): + adjoin_width = 1 + bins = [] + curr_width = 0 + i_last = len(cols) - 1 + for i, w in enumerate(cols): + w_adjoined = w + adjoin_width + curr_width += w_adjoined + if i_last == i: + wrap = curr_width + 1 > line_width and i > 0 + else: + wrap = curr_width + 2 > line_width and i > 0 + if wrap: + bins.append(i) + curr_width = w_adjoined + bins.append(len(cols)) + return bins + + +def _console_width(): + # pandas repr sets the wrap width from console.get_console_size(); reuse it + # when present so the wrap decision is identical. Without pandas (or if that + # private module moved) fall back to display.width. + if has_pandas: + try: + from pandas.io.formats.console import get_console_size + + return get_console_size()[0] + except Exception: # pragma: no cover (defensive across pandas versions) + pass + return _display_option("width") + + +class _TextFormatter: + """Reproduces pandas DataFrameFormatter + StringFormatter for text repr.""" + + def __init__(self, frame, max_rows, min_rows, max_cols): + self.frame = frame + self.max_rows = max_rows + self.min_rows = min_rows + self.max_cols = max_cols + self.justify = _display_option("colheader_justify") + self.tr_frame = frame + self.tr_col_num = None + self.tr_row_num = None + self.max_cols_fitted = self._calc_max_cols_fitted() + self.max_rows_fitted = self._calc_max_rows_fitted() + self.truncate() + + def _is_in_terminal(self): + return self.max_cols == 0 or self.max_rows == 0 + + def _calc_max_cols_fitted(self): + if not self._is_in_terminal(): + return self.max_cols + width = shutil.get_terminal_size()[0] + if self.max_cols == 0 and self.frame.ncols > width: + return width + return self.max_cols + + def _calc_max_rows_fitted(self): + if self._is_in_terminal() and self.max_rows == 0: + return _terminal_rows() + max_rows = self.max_rows + if max_rows and self.frame.nrows > max_rows and self.min_rows: + max_rows = min(self.min_rows, max_rows) + return max_rows + + @property + def is_truncated_horizontally(self): + return bool(self.max_cols_fitted and self.frame.ncols > self.max_cols_fitted) + + @property + def is_truncated_vertically(self): + return bool(self.max_rows_fitted and self.frame.nrows > self.max_rows_fitted) + + @property + def is_truncated(self): + return self.is_truncated_horizontally or self.is_truncated_vertically + + def truncate(self): + if self.is_truncated_horizontally: + self._truncate_horizontally() + if self.is_truncated_vertically: + self._truncate_vertically() + + def _truncate_horizontally(self): + col_num = self.max_cols_fitted // 2 + if col_num >= 1: + _len = self.tr_frame.ncols + self.tr_frame = self.tr_frame.slice_cols( + [*range(col_num), *range(_len - col_num, _len)] + ) + else: + col_num = self.max_cols + self.tr_frame = self.tr_frame.slice_cols(list(range(col_num))) + self.tr_col_num = col_num + + def _truncate_vertically(self): + row_num = self.max_rows_fitted // 2 + if row_num >= 1: + _len = self.tr_frame.nrows + self.tr_frame = self.tr_frame.slice_rows( + [*range(row_num), *range(_len - row_num, _len)] + ) + else: + row_num = self.max_rows + self.tr_frame = self.tr_frame.slice_rows(list(range(row_num))) + self.tr_row_num = row_num + + def _format_col_raw(self, col): + if col.kind == "int": + return [f"{x: d}" for x in col.values] + if col.kind == "float": + return _format_float_column(col.values, _display_option("precision")) + precision = _display_option("precision") + float_format = _display_option("float_format") + out = [] + for v in col.values: + # A float NaN is excluded from pandas' float-format branch (it uses + # is_float(v) & notna(v)) and rendered as the na_rep "NaN" instead. + if _is_float_scalar(v) and not math.isnan(v): + if float_format is not None: + # A user display.float_format callable replaces the default + # precision render (and adds no sign-space of its own). + out.append(float_format(v)) + else: + out.append(_trim_zeros_single_float(f"{v: .{precision}f}")) + elif v is None: # pragma: no cover (defensive) + # Cells come from numpy values or the dense grid, which blanks None + out.append(" None") + elif _is_float_scalar(v): + out.append(" NaN") + else: + out.append(f" {_pprint(v)}") + return out + + def _get_body_strcols(self): + # Column labels are formatted together (integer labels padded to a uniform + # width) the way pandas Index._format_flat does, not per column. + labels = _format_labels([col.label for col in self.tr_frame.columns]) + strcols = [] + for col, label in zip(self.tr_frame.columns, labels, strict=True): + header = f" {label}" if col.numeric else label + header_colwidth = len(header) + # pandas fixes width twice: format_array right-justifies to the cell + # content width, then the body pass re-justifies with colheader_justify + # (which only matters when the header is wider, or when it is "left"). + fmt_values = _make_fixed_width(self._format_col_raw(col), "right") + fmt_values = _make_fixed_width(fmt_values, self.justify, minimum=header_colwidth) + max_len = max(max((len(x) for x in fmt_values), default=0), header_colwidth) + cheader = _justify([header], max_len, self.justify) + strcols.append(cheader + fmt_values) + return strcols + + def _get_index_strcol(self): + idx = _make_fixed_width(_format_labels(self.tr_frame.index), justify="left") + width = max(map(len, idx), default=0) + idx = [s.ljust(width) for s in idx] + corner = "" if self.frame.col_name is None else str(self.frame.col_name) + return [corner, *idx] + + def get_strcols(self): + strcols = self._get_body_strcols() + strcols.insert(0, self._get_index_strcol()) + return strcols + + @property + def _adjusted_tr_col_num(self): + return self.tr_col_num + 1 # index column is always shown + + def _insert_dot_separators(self, strcols): + index_length = len(self._get_index_strcol()) + if self.is_truncated_horizontally: + strcols.insert(self._adjusted_tr_col_num, [" ..."] * index_length) + if self.is_truncated_vertically: + self._insert_dots_vertical(strcols, index_length) + return strcols + + def _insert_dots_vertical(self, strcols, index_length): + n_header_rows = index_length - self.tr_frame.nrows + row_num = self.tr_row_num + for ix, col in enumerate(strcols): + cwidth = len(col[row_num]) + is_dot_col = self.is_truncated_horizontally and ix == self._adjusted_tr_col_num + dots = "..." if (cwidth > 3 or is_dot_col) else ".." + if ix == 0: + dot_mode = "left" + elif is_dot_col: + cwidth = 4 + dot_mode = "right" + else: + dot_mode = "right" + col.insert(row_num + n_header_rows, _justify([dots], cwidth, dot_mode)[0]) + + def _get_strcols(self): + strcols = self.get_strcols() + if self.is_truncated: + strcols = self._insert_dot_separators(strcols) + return strcols + + def _fit_to_terminal(self, strcols): + lines = _adjoin(1, strcols).split("\n") + max_len = max(len(x) for x in lines) + width = shutil.get_terminal_size()[0] + adj_dif = max_len - width + 1 # +1 to avoid too-wide repr (pandas GH #17023) + col_lens = [max((len(x) for x in col), default=0) for col in strcols] + n_cols = len(col_lens) + while adj_dif > 0 and n_cols > 1: + mid = round(n_cols / 2) + adj_dif -= col_lens.pop(mid) + 1 + n_cols = len(col_lens) + max_cols_fitted = max(n_cols - 1, 2) # minus index column; show at least two + self.max_cols_fitted = max_cols_fitted + self.truncate() + return _adjoin(1, self._get_strcols()) + + def _join_multiline(self, strcols, line_width): + adjoin_width = 1 + strcols = list(strcols) + idx = strcols.pop(0) + line_width -= max(len(x) for x in idx) + adjoin_width + col_widths = [max((len(x) for x in col), default=0) for col in strcols] + col_bins = _binify(col_widths, line_width) + nbins = len(col_bins) + blocks = [] + start = 0 + for i, end in enumerate(col_bins): + row = strcols[start:end] + row.insert(0, idx) + if nbins > 1: + nrows = len(row[-1]) + if end <= len(strcols) and i < nbins - 1: + row.append([" \\", *[" "] * (nrows - 1)]) + else: + row.append([" "] * nrows) + blocks.append(_adjoin(adjoin_width, row)) + start = end + return "\n\n".join(blocks) + + def to_string(self, line_width): + strcols = self._get_strcols() + if line_width is None: + return _adjoin(1, strcols) + if self.max_cols > 0: + return self._join_multiline(strcols, line_width) + return self._fit_to_terminal(strcols) + + +def _render_text(frame): + line_width = _console_width() if _display_option("expand_frame_repr") else None + fmt = _TextFormatter(frame, *_display_limits()) + return fmt.to_string(line_width) + + +# The scoped style block pandas' NotebookFormatter emits ahead of the table. +_HTML_STYLE = ( + "" +) + + +def _html_escape(s): + return s.replace("&", "&").replace("<", "<").replace(">", ">") + + +class _HtmlBuilder: + """Reproduces pandas NotebookFormatter (DataFrame._repr_html_) markup.""" + + indent_delta = 2 + + def __init__(self, fmt): + self.fmt = fmt + self.tr = fmt.tr_frame + self.ncols = self.tr.ncols + self.th = fmt.is_truncated_horizontally + self.tv = fmt.is_truncated_vertically + self.row_levels = 1 # single-level index, always shown + self.elements = [] + + def write(self, s, indent=0): + self.elements.append(" " * indent + s) + + def _cell(self, s, kind, indent): + rs = _html_escape(str(s)).strip().replace(" ", "  ") + self.write(f"<{kind}>{rs}", indent) + + def write_tr(self, line, indent, header=False, align=None, nindex_levels=0): + self.write("" if align is None else f'', indent) + inner = indent + self.indent_delta + for i, s in enumerate(line): + self._cell(s, "th" if (header or i < nindex_levels) else "td", inner) + self.write("", indent) + + def _col_header(self, indent): + row = ["" if self.tr.col_name is None else str(self.tr.col_name)] + row.extend(_format_labels([col.label for col in self.tr.columns])) + if self.th: + row.insert(self.row_levels + self.fmt.tr_col_num, "...") + self.write_tr(row, indent, header=True, align=self.fmt.justify) + + def _body(self, indent): + index_labels = _format_labels(list(self.tr.index)) + col_cells = [ + _make_fixed_width(self.fmt._format_col_raw(col), "right") for col in self.tr.columns + ] + row = [] + for i in range(self.tr.nrows): + if self.tv and i == self.fmt.tr_row_num: + self.write_tr(["..."] * len(row), indent, nindex_levels=self.row_levels) + row = [index_labels[i], *(col_cells[j][i] for j in range(self.ncols))] + if self.th: + row.insert(self.fmt.tr_col_num + self.row_levels, "...") + self.write_tr(row, indent, nindex_levels=self.row_levels) + + def _table(self, indent=0): + classes = "dataframe" + if not _display_option("html.use_mathjax"): + classes = "dataframe tex2jax_ignore mathjax_ignore" + # pandas keeps the attribute for any non-None border, including 0. + border = _display_option("html.border") + border_attr = "" if border is None else f' border="{border}"' + self.write(f'', indent) + self.write("", indent + self.indent_delta) + self._col_header(indent + 2 * self.indent_delta) + self.write("", indent + self.indent_delta) + self.write("", indent + self.indent_delta) + self._body(indent + 2 * self.indent_delta) + self.write("", indent + self.indent_delta) + self.write("", indent) + + def render(self): + self.write("
") + self.write(_HTML_STYLE) + self._table(0) + self.write("
") + return "\n".join(self.elements) + + +def _render_html(frame): + fmt = _TextFormatter(frame, *_display_limits()) + return _HtmlBuilder(fmt).render() + + def _get_matrix_dataframe(matrix, max_rows, min_rows, max_columns, *, mask=None): - if not has_pandas: - return - if max_rows is None: # pragma: no branch - max_rows = pd.options.display.max_rows - if min_rows is None: # pragma: no branch - min_rows = pd.options.display.min_rows - if max_columns is None: # pragma: no branch - max_columns = _get_max_columns() - rows, row_groups = _get_chunk(matrix._nrows, min_rows, max_rows) + max_rows, min_rows, max_columns = _get_limits(max_rows, min_rows, max_columns) + if max_rows == 0: + # The renderer fits the rows to the terminal; read at least the first row + fit_rows = max(_terminal_rows(), 1) + rows, row_groups = _get_chunk(matrix._nrows, fit_rows, fit_rows) + else: + fit_rows = max_rows + rows, row_groups = _get_chunk(matrix._nrows, min_rows, max_rows) columns, column_groups = _get_chunk(matrix._ncols, max_columns, max_columns) arr = np.full((len(rows), len(columns)), np.nan, dtype=object) for row_group, row_offset in row_groups: @@ -265,12 +976,15 @@ def _get_matrix_dataframe(matrix, max_rows, min_rows, max_columns, *, mask=None) column_offset, mask=mask, ) - df = pd.DataFrame(arr, columns=columns, index=rows) + present = _count_present(arr) + truncated = (len(rows), len(columns)) != matrix.shape + # Weigh the COO form by the entries it would show: the renderer cuts it to the rows it + # shows, as it does the grid + coo_rows = matrix._nvals if matrix._nvals <= max_rows else min(min_rows, fit_rows) if ( (mask is None or mask.structure) - and df.shape != matrix.shape - and min(matrix._nvals, max_rows if matrix._nvals <= max_rows else min_rows) - > 2 * df.count().sum() + and truncated + and min(matrix._nvals, coo_rows) > 2 * present ): # The data is sparse and it's better to show in COO format. # SS, SuiteSparse-specific: head @@ -284,47 +998,38 @@ def _get_matrix_dataframe(matrix, max_rows, min_rows, max_columns, *, mask=None) vals = np.zeros(vals.size, dtype=np.uint8) else: vals = np.ones(vals.size, dtype=np.uint8) - df = pd.DataFrame({"row": rows, "col": cols, "val": vals}) - if num_rows < matrix._nvals: - df.loc["..."] = ["..."] * 3 - return df - if mask is not None and not mask.structure and df.shape != matrix.shape: + return _make_coo_frame( + [("row", rows), ("col", cols), ("val", vals)], num_rows < matrix._nvals + ) + if mask is not None and not mask.structure and truncated: # This performs more calculation and uses more memory than I would prefer. # Perhaps we could use the efficient "constant vector or matrix" trick. nonzero = matrix.apply(unary.one["UINT8"]).new(mask=matrix.V, name="") num_rows = matrix._nvals if matrix._nvals <= max_rows else min_rows - if min(nonzero._nvals, num_rows) > 2 * df.count().sum(): + if min(nonzero._nvals, coo_rows) > 2 * present: rows, cols, vals = nonzero.ss.head(num_rows, sort=True) if mask.complement: if not vals.flags.writeable: # pragma: no cover (safety) vals = vals.copy() vals[:] = 0 - df = pd.DataFrame({"row": rows, "col": cols, "val": vals}) - if num_rows < nonzero._nvals: - df.loc["..."] = ["..."] * 3 - return df - return df.where(pd.notna(df), "") + return _make_coo_frame( + [("row", rows), ("col", cols), ("val", vals)], num_rows < nonzero._nvals + ) + return _make_dense_frame(arr, columns, rows) def _get_vector_dataframe(vector, max_rows, min_rows, max_columns, *, mask=None): - if not has_pandas: - return - if max_rows is None: # pragma: no branch - max_rows = pd.options.display.max_rows - if min_rows is None: # pragma: no branch - min_rows = pd.options.display.min_rows - if max_columns is None: # pragma: no branch - max_columns = _get_max_columns() + max_rows, min_rows, max_columns = _get_limits(max_rows, min_rows, max_columns) columns, column_groups = _get_chunk(vector._size, max_columns, max_columns) arr = np.full((1, len(columns)), np.nan, dtype=object) for column_group, column_offset in column_groups: _update_vector_array(arr, vector, column_group, column_offset, mask=mask) - df = pd.DataFrame(arr, columns=columns, index=[""]) + present = _count_present(arr) + truncated = len(columns) != vector._size if ( (mask is None or mask.structure) - and df.size != vector._size - and min(vector._nvals, max_rows if vector._nvals <= max_rows else min_rows) - > 2 * df.count().sum() + and truncated + and min(vector._nvals, max_rows if vector._nvals <= max_rows else min_rows) > 2 * present ): # The data is sparse and it's better to show in COO format. # SS, SuiteSparse-specific: head @@ -335,26 +1040,20 @@ def _get_vector_dataframe(vector, max_rows, min_rows, max_columns, *, mask=None) vals = np.zeros(vals.size, dtype=np.uint8) else: vals = np.ones(vals.size, dtype=np.uint8) - df = pd.DataFrame({"index": indices, "val": vals}) - if num_rows < vector._nvals: - df.loc["..."] = ["..."] * 2 - return df - if mask is not None and not mask.structure and df.size != vector._size: + return _make_coo_frame([("index", indices), ("val", vals)], num_rows < vector._nvals) + if mask is not None and not mask.structure and truncated: # This performs more calculation and uses more memory than I would prefer. # Perhaps we could use the efficient "constant vector or matrix" trick. nonzero = vector.apply(unary.one["UINT8"]).new(mask=vector.V, name="") num_rows = vector._nvals if vector._nvals <= max_rows else min_rows - if min(nonzero._nvals, num_rows) > 2 * df.count().sum(): + if min(nonzero._nvals, num_rows) > 2 * present: indices, vals = nonzero.ss.head(num_rows, sort=True) if mask.complement: if not vals.flags.writeable: # pragma: no cover (safety) vals = vals.copy() vals[:] = 0 - df = pd.DataFrame({"index": indices, "val": vals}) - if num_rows < nonzero._nvals: - df.loc["..."] = ["..."] * 2 - return df - return df.where(pd.notna(df), "") + return _make_coo_frame([("index", indices), ("val", vals)], num_rows < nonzero._nvals) + return _make_dense_frame(arr, columns, [""]) def get_format(x, is_transposed=False): @@ -437,14 +1136,9 @@ def vector_expression_header_html(matrix, expr): return create_header_html(name, keys, vals) -def _format_html(name, header, df, collapse): - if has_pandas: - state = "" if collapse else " open" - with pd.option_context("display.show_dimensions", False, "display.large_repr", "truncate"): - details = df._repr_html_() - else: - state = "" - details = "(Install pandas to see a preview of the data)" +def _format_html(name, header, frame, collapse): + state = "" if collapse else " open" + details = _render_html(frame) return ( "
" f"{CSS_STYLE}" @@ -668,17 +1362,12 @@ def format_matrix(matrix, *, max_rows=None, min_rows=None, max_columns=None, mas name, keys, vals, - lower_border=has_pandas, + lower_border=True, name=matrix.name if mask is None else mask.name, ) - if has_pandas: - df = _get_matrix_dataframe(matrix, max_rows, min_rows, max_columns, mask=mask) - if 0 not in matrix.shape: - with pd.option_context( - "display.show_dimensions", False, "display.large_repr", "truncate" - ): - df_repr = df.__repr__() - return f"{header}\n{df_repr}" + if 0 not in matrix.shape: + frame = _get_matrix_dataframe(matrix, max_rows, min_rows, max_columns, mask=mask) + return f"{header}\n{_render_text(frame)}" return header @@ -688,20 +1377,16 @@ def format_vector(vector, *, max_rows=None, min_rows=None, max_columns=None, mas name, keys, vals, - lower_border=has_pandas, + lower_border=True, name=vector.name if mask is None else mask.name, ) - if has_pandas: - df = _get_vector_dataframe(vector, max_rows, min_rows, max_columns, mask=mask) - if vector._size > 0: - if df.columns[0] != "index": - df.columns.name = "index" - df.index = ["value"] - with pd.option_context( - "display.show_dimensions", False, "display.large_repr", "truncate" - ): - df_repr = df.__repr__() - return f"{header}\n{df_repr}" + if vector._size > 0: + frame = _get_vector_dataframe(vector, max_rows, min_rows, max_columns, mask=mask) + if frame.columns[0].label != "index": + # Dense vectors label the corner "index" and the single row "value". + frame.col_name = "index" + frame.index = ["value"] + return f"{header}\n{_render_text(frame)}" return header @@ -848,7 +1533,7 @@ def format_matrix_infix_expression_html(expr): expr_html = expr._format_expr_html() header = create_header_html( expr_html, - ["nrows", "ncols", "right_dtype", "left_dtype"], + ["nrows", "ncols", "left_dtype", "right_dtype"], [expr._nrows, expr._ncols, expr.left.dtype, expr.right.dtype], ) return _format_infix_expression(expr, header, expr_html) diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index d39eb9145..9ad43721a 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -18,6 +18,7 @@ from ..base import call from ..operator import get_typed_op from ..scalar import Scalar, _as_scalar, _scalar_index +from ..udt import _element_value from ..utils import ( _CArray, _distinct_buffers, @@ -49,6 +50,19 @@ def njit(func=None, **kwargs): ffi_new = ffi.new +def _iterator_value_getter(dtype): + """The function returning the value of the entry an iterator is on.""" + if not dtype._is_udt: + return getattr(lib, f"GxB_Iterator_get_{dtype.name}") + buf = ffi_new("char[]", dtype.np_type.itemsize) + + def get_udt(it): + lib.GxB_Iterator_get_UDT(it, buf) + return _element_value(dtype, buf) + + return get_udt + + def head(matrix, n=10, dtype=None, *, sort=False): """Like ``matrix.to_coo()``, but only returns the first n elements. @@ -67,7 +81,11 @@ def head(matrix, n=10, dtype=None, *, sort=False): else: dtype = lookup_dtype(dtype) rows, cols, vals = zip(*itertools.islice(matrix.ss.iteritems(), n), strict=True) - return np.array(rows, np.uint64), np.array(cols, np.uint64), np.array(vals, dtype.np_type) + # Filled from a list: numpy reads a tuple of records as one record, and an array + # UDT's dtype would add its shape to that of each value. + values = np.empty(len(vals), dtype.np_type) + values[...] = list(vals) + return np.array(rows, np.uint64), np.array(cols, np.uint64), values def _concat_mn(tiles, *, is_matrix=None): @@ -499,7 +517,7 @@ def itervalues(self, seek=0): return it = it_ptr[0] info = success = lib.GrB_SUCCESS - val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + val_func = _iterator_value_getter(self._parent.dtype) next_func = lib.GxB_Matrix_Iterator_next try: while info == success: @@ -534,7 +552,7 @@ def iteritems(self, seek=0): it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Matrix_Iterator_getIndex - val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + val_func = _iterator_value_getter(self._parent.dtype) next_func = lib.GxB_Matrix_Iterator_next row_ptr = ffi_new("GrB_Index*") col_ptr = ffi_new("GrB_Index*") diff --git a/graphblas/core/ss/vector.py b/graphblas/core/ss/vector.py index 4ad1c3eb9..608dcacbb 100644 --- a/graphblas/core/ss/vector.py +++ b/graphblas/core/ss/vector.py @@ -25,7 +25,7 @@ from ._utils import _pack_securely, _resolve_serialized_dtype, _unclaim_taken from .config import BaseConfig from .descriptor import get_descriptor -from .matrix import _concat_mn, njit +from .matrix import _concat_mn, _iterator_value_getter, njit from .prefix_scan import prefix_scan ffi_new = ffi.new @@ -46,7 +46,11 @@ def head(vector, n=10, dtype=None, *, sort=False): else: dtype = lookup_dtype(dtype) indices, vals = zip(*itertools.islice(vector.ss.iteritems(), n), strict=True) - return np.array(indices, np.uint64), np.array(vals, dtype.np_type) + # Filled from a list: numpy reads a tuple of records as one record, and an array + # UDT's dtype would add its shape to that of each value. + values = np.empty(len(vals), dtype.np_type) + values[...] = list(vals) + return np.array(indices, np.uint64), values class VectorConfig(BaseConfig): @@ -370,7 +374,7 @@ def itervalues(self, seek=0): return it = it_ptr[0] info = success = lib.GrB_SUCCESS - val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + val_func = _iterator_value_getter(self._parent.dtype) next_func = lib.GxB_Vector_Iterator_next try: while info == success: @@ -405,7 +409,7 @@ def iteritems(self, seek=0): it = it_ptr[0] info = success = lib.GrB_SUCCESS key_func = lib.GxB_Vector_Iterator_getIndex - val_func = getattr(lib, f"GxB_Iterator_get_{self._parent.dtype.name}") + val_func = _iterator_value_getter(self._parent.dtype) next_func = lib.GxB_Vector_Iterator_next try: while info == success: diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index 75393afa0..3da1297d4 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -1,3 +1,5 @@ +import contextlib + import numpy as np import pytest @@ -81,6 +83,21 @@ def html_printer(x, name="", indent=4): return _printer(repr_html(x), name, "repr_html", indent) +@contextlib.contextmanager +def display_options(*args): + """Set display options as ``pd.option_context`` does, with or without pandas. + + The renderer reads pandas' display options when pandas is installed and its own + copy of their defaults otherwise. Setting both lets a test expect the same output + in either environment. + """ + with pytest.MonkeyPatch.context() as mp: + for key, val in zip(args[::2], args[1::2], strict=True): + mp.setitem(formatting._DISPLAY_DEFAULTS, key.removeprefix("display."), val) + with pd.option_context(*args) if pd is not None else contextlib.nullcontext(): + yield + + @pytest.fixture(autouse=True) def _stable_repr_width(monkeypatch): """Pin terminal width so pandas-rendered reprs are deterministic. @@ -146,41 +163,26 @@ def t(): def test_no_pandas_repr(A, C, v, w): - # This is a bit of a hack... + # The rich repr is hand-rendered, so it no longer depends on pandas: with + # pandas marked absent the output is byte-identical to the pandas-present + # output, data grid and all. (When pandas is genuinely absent both branches + # use the same pandas-free path, so this still exercises that code.) + objs = [A, A.T, C, C.S, ~C.V, v, v.S, ~w.V, w] + expected = [repr(x) for x in objs] has_pandas_prev = formatting.has_pandas formatting.has_pandas = False try: - repr_printer(A, "A", indent=8) - assert repr(A) == ( - '"A_1" nvals nrows ncols dtype format\n' - "gb.Matrix 3 1 5 INT64 bitmapr" - ) - repr_printer(A.T, "A.T", indent=8) - assert repr(A.T) == ( - '"A_1.T" nvals nrows ncols dtype format\n' - "gb.TransposedMatrix 3 5 1 INT64 bitmapc" - ) - repr_printer(C.S, "C.S", indent=8) - assert repr(C.S) == ( - '"C.S" nvals nrows ncols dtype format\n' - "StructuralMask\n" - "of gb.Matrix 8 70 77 INT64 hypercsr" - ) - repr_printer(v, "v", indent=8) - assert repr(v) == ( - '"v" nvals size dtype format\ngb.Vector 3 5 FP64 bitmap' - ) - repr_printer(~w.V, "~w.V", indent=8) - assert repr(~w.V) == ( - '"~w.V" nvals size dtype format\n' - "ComplementedValueMask\n" - "of gb.Vector 4 77 INT64 bitmap" - ) + actual = [repr(x) for x in objs] finally: formatting.has_pandas = has_pandas_prev + assert actual == expected + # The data grid is rendered, not just the header: the border line and the + # values are present. + lines = repr(A).split("\n") + assert lines[2].startswith("----") + assert lines[-1] == "0 0 1 2" -@pytest.mark.skipif("not pd") def test_matrix_repr_small(A, B): repr_printer(A, "A") assert repr(A) == ( @@ -212,7 +214,6 @@ def test_matrix_repr_small(A, B): ) -@pytest.mark.skipif("not pd") def test_matrix_mask_repr_small(A): repr_printer(A.S, "A.S") assert repr(A.S) == ( @@ -404,7 +405,6 @@ def test_matrix_mask_repr_large(C): ) -@pytest.mark.skipif("not pd") def test_vector_repr_small(v): repr_printer(v, "v") assert repr(v) == ( @@ -429,7 +429,6 @@ def test_vector_repr_large(w): ) -@pytest.mark.skipif("not pd") def test_vector_mask_repr_small(v): repr_printer(v.S, "v.S") assert repr(v.S) == ( @@ -517,140 +516,23 @@ def test_scalar_repr(s, t): def test_no_pandas_repr_html(A, C, v, w): - # This is a bit of a hack... + # _repr_html_ is hand-rendered too: marking pandas absent yields output that + # is byte-identical to the pandas-present output, data table included. + objs = [A, A.T, C, C.S, ~C.V, v, v.S, ~w.V, w] + expected = [repr_html(x) for x in objs] has_pandas_prev = formatting.has_pandas formatting.has_pandas = False try: - html_printer(A, "A", indent=8) - assert repr_html(A) == ( - "
" - f"{CSS_STYLE}" - '
A1
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
gb.Matrix
nvals
nrows
ncols
dtype
format
315INT64bitmapr
\n" - "
\n" - "
(Install pandas to see a preview of the data)
" - ) - html_printer(A.T, "A.T", indent=8) - assert repr_html(A.T) == ( - "
" - f"{CSS_STYLE}" - '
A1.T
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
gb.TransposedMatrix
nvals
nrows
ncols
dtype
format
351INT64bitmapc
\n" - "
\n" - "
(Install pandas to see a preview of the data)
" - ) - html_printer(C.S, "C.S", indent=8) - assert repr_html(C.S) == ( - "
" - f"{CSS_STYLE}" - '
C.S
\n' - '\n' - " \n" - ' \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
StructuralMask\n'
-            "of\n"
-            "gb.Matrix
nvals
nrows
ncols
dtype
format
87077INT64hypercsr
\n" - "
\n" - "
(Install pandas to see a preview of the data)
" - ) - html_printer(v, "v", indent=8) - assert repr_html(v) == ( - "
" - f"{CSS_STYLE}" - '
v
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
gb.Vector
nvals
size
dtype
format
35FP64bitmap
\n" - "
\n" - "
(Install pandas to see a preview of the data)
" - ) - html_printer(~w.V, "~w.V", indent=8) - assert repr_html(~w.V) == ( - "
" - f"{CSS_STYLE}" - '
~w.V
\n' - '\n' - " \n" - ' \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
ComplementedValueMask\n'
-            "of\n"
-            "gb.Vector
nvals
size
dtype
format
477INT64bitmap
\n" - "
\n" - "
(Install pandas to see a preview of the data)
" - ) + actual = [repr_html(x) for x in objs] finally: formatting.has_pandas = has_pandas_prev + assert actual == expected + # The data table is rendered, not the "install pandas" placeholder. + html = repr_html(A) + assert "install" not in html.lower() + assert '' in html -@pytest.mark.skipif("not pd") def test_matrix_repr_html_small(A, B): html_printer(A, "A") assert repr_html(A) == ( @@ -845,7 +727,6 @@ def test_matrix_repr_html_small(A, B): ) -@pytest.mark.skipif("not pd") def test_matrix_mask_repr_html_small(A): html_printer(A.S, "A.S") assert repr_html(A.S) == ( @@ -2121,7 +2002,6 @@ def test_matrix_mask_repr_html_large(C): ) -@pytest.mark.skipif("not pd") def test_vector_repr_html_small(v): html_printer(v, "v") assert repr_html(v) == ( @@ -2267,7 +2147,6 @@ def test_vector_repr_html_large(w): ) -@pytest.mark.skipif("not pd") def test_vector_mask_repr_html_small(v): html_printer(v.S, "v.S") assert repr_html(v.S) == ( @@ -2889,7 +2768,6 @@ def test_apply_repr(v): ) -@pytest.mark.skipif("not pd") def test_apply_repr_html(v): html_printer(v.apply(unary.one), "v.apply(unary.one)") assert repr_html(v.apply(unary.one)) == ( @@ -2922,7 +2800,6 @@ def test_mxm_repr(A, B): ) -@pytest.mark.skipif("not pd") def test_mxm_repr_html(A, B): html_printer(A.mxm(B), "A.mxm(B)") assert repr_html(A.mxm(B)) == ( @@ -2957,7 +2834,6 @@ def test_mxv_repr(A, v): ) -@pytest.mark.skipif("not pd") def test_mxv_repr_html(A, v): html_printer(A.mxv(v), "A.mxv(v)") assert repr_html(A.mxv(v)) == ( @@ -2980,7 +2856,6 @@ def test_mxv_repr_html(A, v): ) -@pytest.mark.skipif("not pd") def test_matrix_reduce_columns_repr_html(A): # This is implemented using the transpose of A, so make sure we're oriented correctly! html_printer(A.reduce_columnwise(), "A.reduce_columnwise()") @@ -3014,7 +2889,6 @@ def test_matrix_reduce_repr(C, v): ) -@pytest.mark.skipif("not pd") def test_matrix_reduce_repr_html(C, v): html_printer(C.reduce_scalar(), "C.reduce_scalar()", indent=8) assert repr_html(C.reduce_scalar()) == ( @@ -3035,7 +2909,6 @@ def test_matrix_reduce_repr_html(C, v): ) -@pytest.mark.skipif("not pd") def test_matrix_huge(): M = Matrix(int, nrows=2**60, ncols=2**60, name="M") repr_printer(M, "M") @@ -3061,7 +2934,6 @@ def test_matrix_huge(): assert M.isequal(M2) -@pytest.mark.skipif("not pd") def test_matrix_huge_html(): M = Matrix(int, nrows=2**60, ncols=2**60, name="M") html_printer(M, "M") @@ -3254,7 +3126,6 @@ def test_matrix_huge_html(): ) -@pytest.mark.skipif("not pd") def test_vector_huge(): v = Vector(int, size=2**60) repr_printer(v, "v") @@ -3269,7 +3140,6 @@ def test_vector_huge(): assert v2.isequal(v) -@pytest.mark.skipif("not pd") def test_vector_huge_html(): v = Vector(int, size=2**60) html_printer(v, "v") @@ -3407,7 +3277,6 @@ def test_vector_huge_html(): ) -@pytest.mark.skipif("not pd") def test_sparse_vector_repr(): v = Vector.from_coo([100 * i for i in range(100)], [10 * i for i in range(100)], name="v") repr_printer(v, "v") @@ -3561,7 +3430,6 @@ def test_sparse_vector_repr(): ) -@pytest.mark.skipif("not pd") def test_sparse_matrix_repr(): A = Matrix.from_coo( [100 * i for i in range(100)], [10 * i for i in range(100)], list(range(100)), name="A" @@ -3735,154 +3603,1749 @@ def test_sparse_matrix_repr(): ) -@pytest.mark.skipif("not pd") -def test_infix_expr_repr_html(A, B, v): - html_printer(v & v, "v & v") - assert repr_html(v & v) == ( - "
" - f"{CSS_STYLE}" - '
gb.VectorEwiseMultExpr ?Do op(expr) to create a VectorExpression for ewise_mult.
For example: times(v & v)
\n' - '
\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
v & v
size
left_dtype
right_dtype
5FP64FP64
\n" - "
\n" - "" +def test_sparse_float_repr(): + # A sparse float object whose nvals fit in display.max_rows is shown in COO form + # with a float column: every value gets the same number of decimals, less the + # trailing zeros they all share, and NaN shows as "NaN" where the dense grid shows + # "nan". + indices = [100, 161, 222, 283, 344, 405, 466] + values = [1.5, -2.25, 0.0, 100.0, np.nan, np.inf, -np.inf] + v = Vector.from_coo(indices, values, size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 7 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.50\n" + "1 161 -2.25\n" + "2 222 0.00\n" + "3 283 100.00\n" + "4 344 NaN\n" + "5 405 inf\n" + "6 466 -inf" ) - html_printer(v | v, "v | v") - assert repr_html(v | v) == ( + html_printer(v, "v") + assert repr_html(v) == ( "
" f"{CSS_STYLE}" - '
gb.VectorEwiseAddExpr ?Do op(expr) to create a VectorExpression for ewise_add.
For example: plus(v | v)
\n' + '
v
\n' '\n' " \n" - ' \n' + ' \n' + " \n" " \n" - " \n" - " \n" + " \n" + " \n" " \n" " \n" - " \n" - " \n" + " \n" + " \n" " \n" + " \n" " \n" "
v | v
gb.Vector
nvals
size
left_dtype
right_dtype
dtype
format
5FP6471000FP64sparse
\n" "
\n" - "
" - ) - html_printer(A @ v, "A @ v") - assert repr_html(A @ v) == ( - "
" - f"{CSS_STYLE}" - '
gb.VectorMatMulExpr ?Do op(expr) to create a VectorExpression for mxv.
For example: plus_times(A1 @ v)
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" + "
\n" + "\n" + '
A1 @ v
size
left_dtype
right_dtype
1INT64FP64
\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" "
indexval
01001.50
1161-2.25
22220.00
3283100.00
4344NaN
5405inf
6466-inf
\n" - "
\n" - "
" + "
" ) - html_printer(v @ A.T, "v @ A.T") + A = Matrix.from_coo( + [3, 104, 205, 306, 407, 508, 609], + [900, 811, 722, 633, 544, 455, 366], + values, + nrows=1000, + ncols=1000, + name="A", + ) + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 7 1000 1000 FP64 hypercsr\n" + "-----------------------------------------------\n" + " row col val\n" + "0 3 900 1.50\n" + "1 104 811 -2.25\n" + "2 205 722 0.00\n" + "3 306 633 100.00\n" + "4 407 544 NaN\n" + "5 508 455 inf\n" + "6 609 366 -inf" + ) + v = Vector.from_coo([100, 161, 222], [1.0, -2.0, 30.0], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.0\n" + "1 161 -2.0\n" + "2 222 30.0" + ) + v = Vector.from_coo( + [100, 161, 222, 283], [0.1, 1 / 3, -2.5, np.nan], dtype="FP32", size=1000, name="v" + ) + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 4 1000 FP32 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.100000\n" + "1 161 0.333333\n" + "2 222 -2.500000\n" + "3 283 NaN" + ) + + +def test_sparse_float_repr_scientific(): + # Scientific notation when a value is smaller than the precision shows, or when + # one is above 1e6 and the fixed-point column would be too wide. + v = Vector.from_coo([100, 161, 222], [1e-8, 2.5, -3.0], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.000000e-08\n" + "1 161 2.500000e+00\n" + "2 222 -3.000000e+00" + ) + v = Vector.from_coo([100, 161, 222], [1.5e10, 2.0, -0.125], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.500000e+10\n" + "1 161 2.000000e+00\n" + "2 222 -1.250000e-01" + ) + # Too wide for fixed point but not large: stays fixed point + v = Vector.from_coo([100, 161, 222], [123456.789, 0.5, -7.0], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 123456.789\n" + "1 161 0.500\n" + "2 222 -7.000" + ) + v = Vector.from_coo( + [100, 161, 222, 283], [1e-3, 12345.678, -9.5e5, np.nan], size=1000, name="v" + ) + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 4 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.001\n" + "1 161 12345.678\n" + "2 222 -950000.000\n" + "3 283 NaN" + ) + v = Vector.from_coo([100, 161, 222], [1.23456, -0.5, 1e-4], size=1000, name="v") + with display_options("display.precision", 3): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.235e+00\n" + "1 161 -5.000e-01\n" + "2 222 1.000e-04" + ) + + +@pytest.mark.skipif("not dtypes._supports_complex") +def test_sparse_complex_repr(): + values = [ + 1 + 2j, + -1.5 - 0.25j, + 3j, + complex(np.nan, 1), + complex(1, np.nan), + complex(np.nan, -2), + complex(np.nan, np.nan), + 1 + 100j, + ] + v = Vector.from_coo([100, 161, 222, 283, 344, 405, 466, 527], values, size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 8 1000 FC64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.00+ 2.00j\n" + "1 161 -1.50- 0.25j\n" + "2 222 0.00+ 3.00j\n" + "3 283 NaN+ 1.00j\n" + "4 344 1.00+ NaNj\n" + "5 405 NaN- 2.00j\n" + "6 466 NaN+ NaNj\n" + "7 527 1.00+100.00j" + ) + html_printer(v, "v") + assert repr_html(v) == ( + "
" + f"{CSS_STYLE}" + '
v
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
gb.Vector
nvals
size
dtype
format
81000FC64sparse
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
indexval
01001.00+  2.00j
1161-1.50-  0.25j
22220.00+  3.00j
3283NaN+  1.00j
43441.00+   NaNj
5405NaN-  2.00j
6466NaN+   NaNj
75271.00+100.00j
\n" + "
" + ) + v = Vector.from_coo([100, 161], [1e-8 + 1j, 2 - 3j], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FC64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.0+1.0j\n" + "1 161 2.0-3.0j" + ) + v = Vector.from_coo([100, 161], [0.1 + 0.2j, -1 - 1j], dtype="FC32", size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FC32 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.1+0.2j\n" + "1 161 -1.0-1.0j" + ) + + +def test_sparse_repr_chop_threshold(): + # A value no larger than display.chop_threshold shows as zero. Whether the column + # uses scientific notation is decided from the values before they are chopped. + v = Vector.from_coo([100, 161, 222, 283], [1e-8, 0.5, -1e-9, np.nan], size=1000, name="v") + with display_options("display.chop_threshold", 1e-6): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 4 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.000000e+00\n" + "1 161 5.000000e-01\n" + "2 222 0.000000e+00\n" + "3 283 NaN" + ) + v = Vector.from_coo([100, 161], [1.5, -0.25], size=1000, name="v") + with display_options("display.chop_threshold", 1e-6): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.50\n" + "1 161 -0.25" + ) + + +@pytest.mark.skipif("not dtypes._supports_complex") +def test_sparse_repr_chop_threshold_complex(): + # A complex value is compared by its magnitude, so a small part is not chopped + v = Vector.from_coo([100, 161], [1e-8 + 1j, 2 - 1e-9j], size=1000, name="v") + with display_options("display.chop_threshold", 1e-6): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FC64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.0+1.0j\n" + "1 161 2.0-0.0j" + ) + + +def test_repr_float_format(): + # A float_format callable replaces the fixed-point and scientific formatting. + # A missing value is "NaN" in COO form; the dense grid already holds it as "nan". + v = Vector.from_coo([100, 161, 222], [1234.5, np.nan, -0.001], size=1000, name="v") + with display_options("display.float_format", "{:,.2f}".format): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1,234.50\n" + "1 161 NaN\n" + "2 222 -0.00" + ) + v = Vector.from_coo([0, 1, 3], [1234.5, np.nan, -0.001], size=4, name="v") + with display_options("display.float_format", "{:,.2f}".format): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 4 FP64 bitmap\n" + "-------------------------------------\n" + "index 0 1 2 3\n" + "value 1,234.50 nan -0.00" + ) + + +@pytest.mark.skipif("not dtypes._supports_complex") +def test_repr_float_format_complex(): + v = Vector.from_coo([100, 161], [1 + 2j, complex(np.nan, 1)], size=1000, name="v") + with display_options("display.float_format", "{:.2f}".format): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FC64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.00+2.00j\n" + "1 161 NaN" + ) + + +def test_sparse_bool_repr(): + v = Vector.from_coo([100, 161, 222], [True, False, True], size=1000, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 BOOL sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 True\n" + "1 161 False\n" + "2 222 True" + ) + + +def test_sparse_float_repr_truncated(): + # Past display.max_rows the COO form ends in a "..." row, which makes every column + # an object column: each float is formatted on its own, and NaN shows as "NaN". + v = Vector.from_coo( + [100, 161, 222, 283, 344], [np.nan, 1.5, 2.0, 3.0, 4.0], size=1000, name="v" + ) + with display_options("display.max_rows", 4, "display.min_rows", 2): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 5 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 NaN\n" + "1 161 1.5\n" + "... ... ..." + ) + html_printer(v, "v") + assert repr_html(v) == ( + "
" + f"{CSS_STYLE}" + '
v
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
gb.Vector
nvals
size
dtype
format
51000FP64sparse
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 2 + " \n" + " \n" + "
indexval
0100NaN
11611.5
......
\n" + "
" + ) + + +def test_repr_max_colwidth(): + # Cells wider than display.max_colwidth are cut short with "..." + v = Vector.from_coo([100, 161], [1e10, -0.5], size=1000, name="v") + with display_options("display.max_colwidth", 8): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.00...\n" + "1 161 -5.00..." + ) + # With a max_colwidth of 3 or less, integer labels are narrowed but not cut + v = Vector.from_coo([0, 1, 11], [1, 2, 3], size=12, name="v") + with display_options("display.max_colwidth", 2): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 12 INT64 bitmap\n" + "-------------------------------------\n" + "index 0 1 2 3 4 5 6 7 8 9 10 11\n" + "value 1 2 3" + ) + # The row labels are still padded to one width, so the "..." label of truncated rows + # keeps all three dots + A = Matrix.from_coo( + [0, 9999, 9999], [9999, 1, 9999], [1, 2, 3], nrows=10000, ncols=10000, name="A" + ) + small = ("display.max_rows", 4, "display.min_rows", 4, "display.max_columns", 4) + with display_options("display.max_colwidth", 3, *small): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 3 10000 10000 INT64 hypercsr\n" + "-----------------------------------------------\n" + " 0 1 ... 9998 9999\n" + "0 ... 1 \n" + "1 ... \n" + "... .. .. ... .. .. \n" + "9998 ... \n" + "9999 2 ... 3 " + ) + # Longer integer labels are cut short as cells are, counting the sign column pandas + # gives them, in the text and the HTML + A = Matrix(int, 10**6, 10**6, name="A") + A[:, :] << 1 + with display_options("display.max_colwidth", 6, *small): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 1000000000000 1000000 1000000 INT64 fullr (iso)\n" + "--------------------------------------------------------------\n" + " 0 1 ... 99... 99...\n" + "0 1 1 ... 1 1\n" + "1 1 1 ... 1 1\n" + "... ... ... ... ... ...\n" + "99... 1 1 ... 1 1\n" + "99... 1 1 ... 1 1" + ) + html_printer(A, "A") + assert repr_html(A) == ( + "
" + f"{CSS_STYLE}" + '
A
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 2 + " \n" + " \n" + " \n" + "
gb.Matrix
nvals
nrows
ncols
dtype
format
10000000000001000000INT64fullr (iso)
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" * 2 + " \n" + " \n" + " \n" + " \n" + " \n" + + " \n" * 2 + + " \n" + + " \n" * 2 + + " \n" + " \n" + " \n" + + " \n" * 2 + + " \n" + + " \n" * 2 + + " \n" + " \n" + " \n" + " \n" * 5 + " \n" + " \n" + " \n" + + " \n" * 2 + + " \n" + + " \n" * 2 + + " \n" + " \n" + " \n" + + " \n" * 2 + + " \n" + + " \n" * 2 + + " \n" + " \n" + "
01...99...
01...1
11...1
......
99...1...1
99...1...1
\n" + "
" + ) + + +def test_repr_colheader_justify(): + v = Vector.from_coo([100, 161], [1.5, -2.25], size=1000, name="v") + with display_options("display.colheader_justify", "left"): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val \n" + "0 100 1.50\n" + "1 161 -2.25" + ) + with display_options("display.colheader_justify", "center"): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.50\n" + "1 161 -2.25" + ) + html_printer(v, "v") + assert repr_html(v) == ( + "
" + f"{CSS_STYLE}" + '
v
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
gb.Vector
nvals
size
dtype
format
21000FP64sparse
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
indexval
01001.50
1161-2.25
\n" + "
" + ) + + +def test_repr_html_no_mathjax(): + v = Vector.from_coo([100, 161], [1.5, -2.25], size=1000, name="v") + with display_options("display.html.use_mathjax", False): + html_printer(v, "v") + assert repr_html(v) == ( + "
" + f"{CSS_STYLE}" + '
v
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
gb.Vector
nvals
size
dtype
format
21000FP64sparse
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
indexval
01001.50
1161-2.25
\n" + "
" + ) + + +def test_repr_html_ignores_notebook_repr_html(v): + # Deliberately not pandas' output: with display.notebook_repr_html off, pandas' + # DataFrame._repr_html_ returns None, which the pandas-based renderer embedded as + # the text "None" where the table goes. The table is shown either way now. + expected = repr_html(v) + with display_options("display.notebook_repr_html", False): + assert repr_html(v) == expected + + +def test_repr_wraps_wide_frame(): + # A frame wider than display.width wraps into blocks, each but the last ending in a + # backslash column. With expand_frame_repr off it is never wrapped. + A = Matrix.from_coo( + [0, 0, 1, 1], [0, 11, 3, 7], [1.5, -2.0, 1e3, 0.25], nrows=2, ncols=12, name="A" + ) + with display_options("display.max_columns", 30, "display.width", 40): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 4 2 12 FP64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2 3 4 5 6 7 \\\n" + "0 1.5 \n" + "1 1000.0 0.25 \n" + "\n" + " 8 9 10 11 \n" + "0 -2.0 \n" + "1 " + ) + with display_options("display.max_columns", 30, "display.width", 20): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 4 2 12 FP64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2 \\\n" + "0 1.5 \n" + "1 \n" + "\n" + " 3 4 5 \\\n" + "0 \n" + "1 1000.0 \n" + "\n" + " 6 7 8 9 \\\n" + "0 \n" + "1 0.25 \n" + "\n" + " 10 11 \n" + "0 -2.0 \n" + "1 " + ) + with display_options("display.max_columns", 30, "display.expand_frame_repr", False): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 4 2 12 FP64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2 3 4 5 6 7 8 9 10 11\n" + "0 1.5 -2.0\n" + "1 1000.0 0.25 " + ) + + +def test_repr_max_rows_zero(monkeypatch): + # display.max_rows=0 fits the rows to the terminal's height (LINES), less three + # lines for the header, the dots and the prompt. + v = Vector.from_coo([100, 161, 222], [1.5, -2.25, np.nan], size=1000, name="v") + monkeypatch.setenv("LINES", "30") + with display_options("display.max_rows", 0): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 1.50\n" + "1 161 -2.25\n" + "2 222 NaN" + ) + v = Vector.from_coo( + [100 + 61 * i for i in range(14)], [1.5 * i for i in range(14)], size=1000, name="v" + ) + monkeypatch.setenv("LINES", "8") + with display_options("display.max_rows", 0): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 14 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.0\n" + "1 161 1.5\n" + ".. ... ...\n" + "9 649 13.5\n" + "... ... ..." + ) + # A terminal with room for one row leaves none to show. main's pandas renderer + # raised TypeError here (pandas fails on a frame truncated to no rows). + v = Vector.from_coo([100, 161, 222], [1.5, -1.0, 2.5], size=1000, name="v") + monkeypatch.setenv("LINES", "4") + with display_options("display.max_rows", 0): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 3 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + ".. ... ..." + ) + # A Matrix shown as a grid reads the rows that fit, not min_rows of them + rows = [0, 1, 2, 3, 36, 37, 38, 39] + A = Matrix.from_coo( + rows, [0, 1, 2, 0, 1, 2, 0, 1], [r * 10 + 1 for r in rows], nrows=40, ncols=3, name="A" + ) + monkeypatch.setenv("LINES", "8") + with display_options("display.max_rows", 0): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 8 40 3 INT64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2 \n" + "0 1 \n" + "1 11 \n" + ".. ... ... ..\n" + "38 381 \n" + "39 391 " + ) + + +def test_repr_max_rows_one(): + # With room for a single row, the first row is shown instead of a head and a tail + v = Vector.from_coo( + [100 + 61 * i for i in range(14)], [1.5 * i for i in range(14)], size=1000, name="v" + ) + with display_options("display.max_rows", 1, "display.min_rows", 1): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 14 1000 FP64 sparse\n" + "-------------------------------------\n" + " index val\n" + "0 100 0.0\n" + ".. ... ..." + ) + # display.min_rows=1 leaves room for one row too, so the first max_rows rows are shown + B = Matrix.from_coo( + [0, 1, 3, 20, 39], [0, 1, 2, 0, 2], [1, 2, 3, 4, 5], nrows=40, ncols=3, name="B" + ) + with display_options("display.max_rows", 5, "display.min_rows", 1): + repr_printer(B, "B") + assert repr(B) == ( + '"B" nvals nrows ncols dtype format\n' + "gb.Matrix 5 40 3 INT64 hypercsr\n" + "-----------------------------------------------\n" + " 0 1 2\n" + "0 1 \n" + "1 2 \n" + "2 \n" + "3 3\n" + "4 \n" + ".. .. .. .." + ) + + +def test_repr_max_rows_below_min_rows(): + # The rows shown are the first and last half of min(min_rows, max_rows), or of + # max_rows when min_rows is 0, so only those rows are read, each under its label + rows = [0, 1, 2, 3, 36, 37, 38, 39] + A = Matrix.from_coo( + rows, [0, 1, 2, 0, 1, 2, 0, 1], [r * 10 + 1 for r in rows], nrows=40, ncols=3, name="A" + ) + with display_options("display.max_rows", 4): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 8 40 3 INT64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2 \n" + "0 1 \n" + "1 11 \n" + ".. ... ... ..\n" + "38 381 \n" + "39 391 " + ) + with display_options("display.max_rows", 6, "display.min_rows", 0): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 8 40 3 INT64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2\n" + "0 1 \n" + "1 11 \n" + "2 21\n" + ".. ... ... ...\n" + "37 371\n" + "38 381 \n" + "39 391 " + ) + # Fewer rows than min_rows + C = Matrix.from_coo([0, 1, 2], [0, 1, 2], [1, 2, 3], nrows=3, ncols=3, name="C") + with display_options("display.max_rows", 2): + repr_printer(C, "C") + assert repr(C) == ( + '"C" nvals nrows ncols dtype format\n' + "gb.Matrix 3 3 3 INT64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2\n" + "0 1 \n" + ".. .. .. ..\n" + "2 3" + ) + # A dense Matrix stays a grid: the COO form would show no more rows than the grid. + # INT64 on Windows too, where numpy before 2 makes integers int32 by default. + vals = np.arange(5, dtype=np.int64)[:, None] * 1000 + np.arange(4)[None, :] + 1 + D = Matrix.from_dense(vals, name="D") + with display_options("display.max_rows", 3, "display.max_columns", 2): + repr_printer(D, "D") + assert repr(D) == ( + '"D" nvals nrows ncols dtype format\n' + "gb.Matrix 20 5 4 INT64 fullr\n" + "---------------------------------------------\n" + " 0 ... 3\n" + "0 1 ... 4\n" + ".. ... ... ...\n" + "4 4001 ... 4004" + ) + + +@pytest.mark.parametrize("use_pandas", [True, False]) +def test_repr_display_options_none(monkeypatch, use_pandas): + # pandas takes None for max_rows, min_rows and max_columns as no limit. A repr shows up + # to 150 rows or columns instead, and the first and last 75 of more: max_rows=None is + # max_rows=151 with min_rows=150, so the COO form can show 150 entries and its "..." row. + # A min_rows of None is max_rows, as in pandas. Before, each raised TypeError. + if use_pandas and pd is None: # pragma: no cover (no pandas) + pytest.skip("needs pandas") + if not use_pandas: + monkeypatch.setattr(formatting, "has_pandas", False) + + def lines(x): + return repr(x).split("\n")[3:] # skip the info header + + A = Matrix.from_coo([0, 1], [0, 1], [1, 2], nrows=100, ncols=3, name="A") + with display_options("display.max_rows", None): + assert len(lines(A)) == 1 + 100 # every row, though min_rows is 10 + # Past 151 rows, the rows shown are read, and the rest are not shown + A = Matrix.from_dense(np.arange(152 * 2).reshape(152, 2), name="A") + with display_options("display.max_rows", None): + rows = lines(A) + assert len(rows) == 1 + 75 + 1 + 75 + assert rows[1].split() == ["0", "0", "1"] + assert rows[75].split() == ["74", "148", "149"] + assert rows[76].split() == ["..", "...", "..."] + assert rows[77].split() == ["77", "154", "155"] + assert rows[-1].split() == ["151", "302", "303"] + text, html = repr(A), repr_html(A) + with display_options("display.max_rows", 151, "display.min_rows", 150): + assert repr(A) == text + assert repr_html(A) == html + with display_options("display.max_rows", None): + assert len(lines(A[:151, :].new(name="A"))) == 1 + 151 + # The COO form of more entries than 151 + v = Vector.from_coo(np.arange(0, 10000, 10), np.arange(1000), size=10000, name="v") + with display_options("display.max_rows", None): + rows = lines(v) + assert len(rows) == 1 + 150 + 1 + assert rows[-2].split() == ["149", "1490", "149"] + assert rows[-1].split() == ["...", "...", "..."] + + # The columns shown: 0 to 74 and 125 to 199 + expected = [*map(str, range(75)), "...", *map(str, range(125, 200))] + B = Matrix.from_dense(np.arange(2 * 200).reshape(2, 200), name="B") + with display_options("display.max_columns", None, "display.expand_frame_repr", False): + assert lines(B)[0].split() == expected + html = repr_html(B) + header = html[html.index('class="dataframe"') :].split("")[0] + assert header.count("") == 1 + 151 + text = repr(B) + with display_options("display.max_columns", 150, "display.expand_frame_repr", False): + assert repr(B) == text + # As with any max_columns, a wide frame wraps at display.width + with display_options("display.max_columns", None, "display.width", 80): + assert len(lines(B)) > 3 + + # min_rows=None is max_rows, as pandas has it, for the grid and the COO form + w = Vector.from_coo(np.arange(0, 10000, 100), np.arange(100), size=10000, name="w") + for x in [A, w]: + with display_options("display.max_rows", 6, "display.min_rows", None): + text, html = repr(x), repr_html(x) + with display_options("display.max_rows", 6, "display.min_rows", 6): + assert repr(x) == text + assert repr_html(x) == html + + # A Matrix too large to read whole + C = Matrix(int, 2**60, 2**60, name="C") + C[0, 0] = 1 + C[2**60 - 1, 2**60 - 1] = 2 + options = ("display.max_rows", None, "display.min_rows", None, "display.max_columns", None) + with display_options(*options, "display.expand_frame_repr", False): + rows = lines(C) + assert len(rows) == 1 + 75 + 1 + 75 + assert rows[0].split()[75] == "..." + assert rows[1].split()[:2] == ["0", "1"] + assert rows[-1].split()[-1] == "2" + + +def test_repr_max_columns_one(): + # With room for a single column, the first column is shown instead of both ends + A = Matrix.from_coo( + [100, 300, 500], [0, 0, 0], [1.5, -2.25, np.nan], nrows=1000, ncols=1, name="A" + ) + with display_options("display.max_columns", 1): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 3 1000 1 FP64 csc\n" + "---------------------------------------------\n" + " row ...\n" + "0 100 ...\n" + "1 300 ...\n" + "2 500 ..." + ) + html_printer(A, "A") + assert repr_html(A) == ( + "
" + f"{CSS_STYLE}" + '
A
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
gb.Matrix
nvals
nrows
ncols
dtype
format
310001FP64csc
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
row...
0100...
1300...
2500...
\n" + "
" + ) + # The same for a Matrix and a Vector shown as a grid + A = Matrix.from_coo( + [0, 1, 2, 0, 2], [0, 0, 0, 3, 4], [1, 2, 3, 4, 5], nrows=3, ncols=5, name="A" + ) + with display_options("display.max_columns", 1): + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 5 3 5 INT64 bitmapr\n" + "----------------------------------------------\n" + " 0 ...\n" + "0 1 ...\n" + "1 2 ...\n" + "2 3 ..." + ) + v = Vector.from_coo([0, 4], [1, 2], size=5, name="v") + with display_options("display.max_columns", 1): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 5 INT64 bitmap\n" + "-------------------------------------\n" + "index 0 ...\n" + "value 1 ..." + ) + + +def test_infix_expr_repr_html(A, B, v): + html_printer(v & v, "v & v") + assert repr_html(v & v) == ( + "
" + f"{CSS_STYLE}" + '
gb.VectorEwiseMultExpr ?Do op(expr) to create a VectorExpression for ewise_mult.
For example: times(v & v)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
v & v
size
left_dtype
right_dtype
5FP64FP64
\n" + "
\n" + "
" + ) + html_printer(v | v, "v | v") + assert repr_html(v | v) == ( + "
" + f"{CSS_STYLE}" + '
gb.VectorEwiseAddExpr ?Do op(expr) to create a VectorExpression for ewise_add.
For example: plus(v | v)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
v | v
size
left_dtype
right_dtype
5FP64FP64
\n" + "
\n" + "
" + ) + html_printer(A @ v, "A @ v") + assert repr_html(A @ v) == ( + "
" + f"{CSS_STYLE}" + '
gb.VectorMatMulExpr ?Do op(expr) to create a VectorExpression for mxv.
For example: plus_times(A1 @ v)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1 @ v
size
left_dtype
right_dtype
1INT64FP64
\n" + "
\n" + "
" + ) + html_printer(v @ A.T, "v @ A.T") assert repr_html(v @ A.T) == ( "
" f"{CSS_STYLE}" - '
gb.VectorMatMulExpr ?Do op(expr) to create a VectorExpression for vxm.
For example: plus_times(v @ A1.T)
\n' + '
gb.VectorMatMulExpr ?Do op(expr) to create a VectorExpression for vxm.
For example: plus_times(v @ A1.T)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
v @ A1.T
size
left_dtype
right_dtype
1FP64INT64
\n" + "
\n" + "
" + ) + html_printer(A & B.T, "A & B.T") + assert repr_html(A & B.T) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(A1 & B1.T)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1 & B1.T
nrows
ncols
left_dtype
right_dtype
15INT64INT64
\n" + "
\n" + "
" + ) + html_printer(A | A, "A | A") + assert repr_html(A | A) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixEwiseAddExpr ?Do op(expr) to create a MatrixExpression for ewise_add.
For example: plus(A1 | A1)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1 | A1
nrows
ncols
left_dtype
right_dtype
15INT64INT64
\n" + "
\n" + "
" + ) + # Operands of two dtypes, so each dtype must sit under its own side's label + F = Matrix.from_coo([0], [1], [2.5], nrows=1, ncols=5, name="F") + html_printer(A | F, "A | F") + assert repr_html(A | F) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixEwiseAddExpr ?Do op(expr) to create a MatrixExpression for ewise_add.
For example: plus(A1 | F)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1 | F
nrows
ncols
left_dtype
right_dtype
15INT64FP64
\n" + "
\n" + "
" + ) + html_printer(A @ B, "A @ B") + assert repr_html(A @ B) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixMatMulExpr ?Do op(expr) to create a MatrixExpression for mxm.
For example: plus_times(A1 @ B1)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1 @ B1
nrows
ncols
left_dtype
right_dtype
11INT64INT64
\n" + "
\n" + "
" + ) + html_printer(A.T @ B.T, "A.T @ B.T") + assert repr_html(A.T @ B.T) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixMatMulExpr ?Do op(expr) to create a MatrixExpression for mxm.
For example: plus_times(A1.T @ B1.T)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
A1.T @ B1.T
nrows
ncols
left_dtype
right_dtype
55INT64INT64
\n" + "
\n" + "
" + ) + html_printer(v @ v, "v @ v") + assert repr_html(v @ v) == ( + "
" + f"{CSS_STYLE}" + '
gb.ScalarMatMulExpr ?Do op(expr) to create a ScalarExpression for inner.
For example: plus_times(v @ v)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
v @ v
left_dtype
right_dtype
FP64FP64
\n" + "
\n" + "
" + ) + + +def test_infix_expr_repr(A, B, v): + repr_printer(v & v, "v & v") + assert repr(v & v) == ( + "gb.VectorEwiseMultExpr size left_dtype right_dtype\n" + "v & v 5 FP64 FP64\n" + "\n" + "Do op(expr) to create a VectorExpression for ewise_mult.\n" + "For example: times(v & v)" + ) + repr_printer(v | v, "v | v") + assert repr(v | v) == ( + "gb.VectorEwiseAddExpr size left_dtype right_dtype\n" + "v | v 5 FP64 FP64\n" + "\n" + "Do op(expr) to create a VectorExpression for ewise_add.\n" + "For example: plus(v | v)" + ) + repr_printer(A @ v, "A @ v") + assert repr(A @ v) == ( + "gb.VectorMatMulExpr size left_dtype right_dtype\n" + "A_1 @ v 1 INT64 FP64\n" + "\n" + "Do op(expr) to create a VectorExpression for mxv.\n" + "For example: plus_times(A_1 @ v)" + ) + repr_printer(v @ A.T, "v @ A.T") + assert repr(v @ A.T) == ( + "gb.VectorMatMulExpr size left_dtype right_dtype\n" + "v @ A_1.T 1 FP64 INT64\n" + "\n" + "Do op(expr) to create a VectorExpression for vxm.\n" + "For example: plus_times(v @ A_1.T)" + ) + repr_printer(A & B.T, "A & B.T") + assert repr(A & B.T) == ( + "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" + "A_1 & B_1.T 1 5 INT64 INT64\n" + "\n" + "Do op(expr) to create a MatrixExpression for ewise_mult.\n" + "For example: times(A_1 & B_1.T)" + ) + repr_printer(A | A, "A | A") + assert repr(A | A) == ( + "gb.MatrixEwiseAddExpr nrows ncols left_dtype right_dtype\n" + "A_1 | A_1 1 5 INT64 INT64\n" + "\n" + "Do op(expr) to create a MatrixExpression for ewise_add.\n" + "For example: plus(A_1 | A_1)" + ) + repr_printer(A @ B, "A @ B") + assert repr(A @ B) == ( + "gb.MatrixMatMulExpr nrows ncols left_dtype right_dtype\n" + "A_1 @ B_1 1 1 INT64 INT64\n" + "\n" + "Do op(expr) to create a MatrixExpression for mxm.\n" + "For example: plus_times(A_1 @ B_1)" + ) + repr_printer(A.T @ B.T, "A.T @ B.T") + assert repr(A.T @ B.T) == ( + "gb.MatrixMatMulExpr nrows ncols left_dtype right_dtype\n" + "A_1.T @ B_1.T 5 5 INT64 INT64\n" + "\n" + "Do op(expr) to create a MatrixExpression for mxm.\n" + "For example: plus_times(A_1.T @ B_1.T)" + ) + repr_printer(v @ v, "v @ v") + assert repr(v @ v) == ( + "gb.ScalarMatMulExpr left_dtype right_dtype\n" + "v @ v FP64 FP64\n" + "\n" + "Do op(expr) to create a ScalarExpression for inner.\n" + "For example: plus_times(v @ v)" + ) + + +def test_scalar_infix_expr_repr_literal(): + # A literal shows its value and its own type, as it has no dtype until an op types it + s = Scalar.from_value(5, dtype=dtypes.INT8, name="s_1") + repr_printer(s & 1, "s & 1") + assert repr(s & 1) == ( + "gb.ScalarEwiseMultExpr left_dtype right_dtype\n" + "s_1 & 1 INT8 int\n" + "\n" + "Do op(expr) to create a ScalarExpression for ewise_mult.\n" + "For example: times(s_1 & 1)" + ) + repr_printer(0.5 | s, "0.5 | s") + assert repr(0.5 | s) == ( + "gb.ScalarEwiseAddExpr left_dtype right_dtype\n" + "0.5 | s_1 float INT8\n" + "\n" + "Do op(expr) to create a ScalarExpression for ewise_add.\n" + "For example: plus(0.5 | s_1)" + ) + html = repr_html(s & 1) + assert "
s1 & 1
" in html + assert "INT8\n int" in html + + +@autocompute +def test_scalar_infix_expr_repr_auto(): + # As for Vector, only BOOL `x & y` and `x | y` compute without an op, to show the result + s = Scalar.from_value(5, dtype=dtypes.INT8, name="s_1") + repr_printer(s & s, "s & s") + assert repr(s & s) == ( + "gb.ScalarEwiseMultExpr left_dtype right_dtype\n" + "s_1 & s_1 INT8 INT8\n" + "\n" + "Do op(expr) to create a ScalarExpression for ewise_mult.\n" + "For example: times(s_1 & s_1)" + ) + assert "Result" not in repr(s | 1) + assert '
' not in repr_html(s & s) + t = Scalar.from_value(True, name="t") + repr_printer(t & True, "t & True") + assert repr(t & True) == ( + "gb.ScalarEwiseMultExpr left_dtype right_dtype\n" + "t & True BOOL bool\n" + "\n" + '"Result" value dtype\n' + "gb.Scalar True BOOL\n" + "\n" + "Do op(expr) to create a ScalarExpression for ewise_mult.\n" + "For example: times(t & True)" + ) + html = repr_html(False | t) + assert '
' in html + assert "True\n BOOL" in html + + +def test_inner_outer_repr_html(v): + html_printer(v.inner(v), "v.inner(v)") + assert repr_html(v.inner(v)) == ( + "
" + f"{CSS_STYLE}" + '
gb.ScalarExpression ?Do expr.new() or other << expr to calculate the expression.
\n' '\n' " \n" - ' \n' - " \n" - " \n" - " \n" + ' \n' + " \n" " \n" " \n" - " \n" " \n" - " \n" " \n" "
v @ A1.T
size
left_dtype
right_dtype
v.inner(v, op=semiring.plus_times[FP64])
dtype
1FP64INT64
\n" "
\n" "
" ) - html_printer(A & B.T, "A & B.T") - assert repr_html(A & B.T) == ( + html_printer(v.outer(v), "v.outer(v)") + assert repr_html(v.outer(v)) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(A1 & B1.T)
\n' + '
gb.MatrixExpression ?Do expr.new() or other << expr to calculate the expression.
\n' '\n' " \n" - ' \n' + ' \n' " \n" " \n" - " \n" - " \n" + " \n" " \n" " \n" - " \n" " \n" - " \n" - " \n" - " \n" - "
A1 & B1.T
v.outer(v, op=semiring.any_times[FP64])
nrows
ncols
right_dtype
left_dtype
dtype
15INT64INT64
\n" - "
\n" - "
" - ) - html_printer(A | A, "A | A") - assert repr_html(A | A) == ( - "
" - f"{CSS_STYLE}" - '
gb.MatrixEwiseAddExpr ?Do op(expr) to create a MatrixExpression for ewise_add.
For example: plus(A1 | A1)
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" " \n" - " \n" - " \n" + " \n" " \n" "
A1 | A1
nrows
ncols
right_dtype
left_dtype
15INT64INT64FP64
\n" "
\n" "
" ) - html_printer(A @ B, "A @ B") - assert repr_html(A @ B) == ( + + +def test_inner_outer_repr(v): + # XXX: hmm, having `(GrB_Matrix)` here isn't so pretty + repr_printer(v.inner(v), "v.inner(v)") + assert repr(v.inner(v)) == ( + "gb.ScalarExpression dtype\n" + "v.inner((GrB_Matrix)v, op=semiring.plus_times[FP64]) FP64\n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + repr_printer(v.outer(v), "v.outer(v)") + assert repr(v.outer(v)) == ( + "gb.MatrixExpression nrows ncols dtype\n" + "(GrB_Matrix)v.outer((GrB_Matrix)v, op=semiring.any_times[FP64]) 5 5 FP64\n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + + +@autocompute +def test_autocompute(A, B, v): + repr_printer(A & A, "A & A") + assert repr(A & A) == ( + "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" + "A_1 & A_1 1 5 INT64 INT64\n" + "\n" + "Do op(expr) to create a MatrixExpression for ewise_mult.\n" + "For example: times(A_1 & A_1)" + ) + repr_printer(A.ewise_add(A), "A.ewise_add(A)") + assert repr(A.ewise_add(A)) == ( + "gb.MatrixExpression nrows ncols dtype\n" + "A_1.ewise_add(A_1, op=monoid.plus[INT64]) 1 5 INT64\n" + "\n" + '"Result" nvals nrows ncols dtype\n' + "gb.Matrix 3 1 5 INT64\n" + "-------------------------------------\n" + " 0 1 2 3 4\n" + "0 0 2 4\n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + + BIG = Vector(int, size=2**55) + small = Vector(int, size=2**55) + BIG[:] = 1 + small[0] = 2 + repr_printer(BIG.ewise_mult(small), "BIG.ewise_mult(small)") + assert repr(BIG.ewise_mult(small)) == ( + "gb.VectorExpression size dtype\n" + "v_0.ewise_mult(v_1, op=binary.times[INT64]) 36028797018963968 INT64\n" + "\n" + '"Result" nvals size dtype\n' + "gb.Vector 1 36028797018963968 INT64\n" + "------------------------------------------\n" + "index 0 1 ... 36028797018963966 36028797018963967\n" + "value 2 ... \n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + repr_printer(BIG.ewise_add(small), "BIG.ewise_add(small)") + assert repr(BIG.ewise_add(small)) == ( + "gb.VectorExpression size dtype\n" + "v_0.ewise_add(v_1, op=monoid.plus[INT64]) 36028797018963968 INT64\n" + "\n" + "Result is too large to compute!\n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + BIG_bool = BIG.dup(dtype=bool) + small_bool = small.dup(dtype=bool) + small_bool[0] = False + repr_printer(BIG_bool | small_bool, "BIG_bool | small_bool") + assert repr(BIG_bool | small_bool) == ( + "gb.VectorEwiseAddExpr size left_dtype right_dtype\n" + "v_6 | v_7 36028797018963968 BOOL BOOL\n" + "\n" + "Result is too large to compute!\n" + "\n" + "Do op(expr) to create a VectorExpression for ewise_add.\n" + "For example: plus(v_6 | v_7)" + ) + C = A.dup(dtype=bool) + repr_printer(C & C, "C & C") + assert repr(C & C) == ( + "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" + "M_2 & M_2 1 5 BOOL BOOL\n" + "\n" + '"Result" nvals nrows ncols dtype\n' + "gb.Matrix 3 1 5 BOOL\n" + "-------------------------------------\n" + " 0 1 2 3 4\n" + "0 False True True\n" + "\n" + "Do op(expr) to create a MatrixExpression for ewise_mult.\n" + "For example: times(M_2 & M_2)" + ) + + +@autocompute +def test_autocompute_html(A, B, v): + html_printer(A & A, "A & A") + assert repr_html(A & A) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixMatMulExpr ?Do op(expr) to create a MatrixExpression for mxm.
For example: plus_times(A1 @ B1)
\n' + '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(A1 & A1)
\n' '\n' " \n" - ' \n' + ' \n' " \n" " \n" - " \n" " \n" + " \n" " \n" " \n" " \n" - " \n" + " \n" " \n" " \n" " \n" @@ -3890,745 +5353,739 @@ def test_infix_expr_repr_html(A, B, v): "\n" "" ) - html_printer(A.T @ B.T, "A.T @ B.T") - assert repr_html(A.T @ B.T) == ( + html_printer(A.ewise_add(A), "A.ewise_add(A)") + assert repr_html(A.ewise_add(A)) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixMatMulExpr ?Do op(expr) to create a MatrixExpression for mxm.
For example: plus_times(A1.T @ B1.T)
\n' + '
gb.MatrixExpression ?Do expr.new() or other << expr to calculate the expression.
\n' '
A1 @ B1
A1 & A1
nrows
ncols
right_dtype
left_dtype
right_dtype
115INT64INT64
\n' " \n" - ' \n' + ' \n' + " \n" " \n" " \n" - " \n" - " \n" + " \n" " \n" " \n" + " \n" + " \n" " \n" - " \n" - " \n" " \n" " \n" "
A1.T @ B1.T
A1.ewise_add(A1, op=monoid.plus[INT64])
nvals
nrows
ncols
right_dtype
left_dtype
dtype
3155INT64INT64
\n" "
\n" - "
" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
01234
0024
\n" + "
" ) - html_printer(v @ v, "v @ v") - assert repr_html(v @ v) == ( + + BIG = Vector(int, size=2**55) + small = Vector(int, size=2**55) + BIG[:] = 1 + small[0] = 2 + html_printer(BIG.ewise_mult(small), "BIG.ewise_mult(small)") + assert repr_html(BIG.ewise_mult(small)) == ( "
" f"{CSS_STYLE}" - '
gb.ScalarMatMulExpr ?Do op(expr) to create a ScalarExpression for inner.
For example: plus_times(v @ v)
\n' + '
gb.VectorExpression ?Do expr.new() or other << expr to calculate the expression.
\n' '\n' " \n" - ' \n' - " \n" - " \n" + ' \n' + " \n" + " \n" + " \n" " \n" " \n" - " \n" - " \n" + " \n" + " \n" + " \n" " \n" "
v @ v
left_dtype
right_dtype
v0.ewise_mult(v1, op=binary.times[INT64])
nvals
size
dtype
FP64FP64136028797018963968INT64
\n" "
\n" - "
" - ) - - -@pytest.mark.skipif("not pd") -def test_infix_expr_repr(A, B, v): - repr_printer(v & v, "v & v") - assert repr(v & v) == ( - "gb.VectorEwiseMultExpr size left_dtype right_dtype\n" - "v & v 5 FP64 FP64\n" - "\n" - "Do op(expr) to create a VectorExpression for ewise_mult.\n" - "For example: times(v & v)" - ) - repr_printer(v | v, "v | v") - assert repr(v | v) == ( - "gb.VectorEwiseAddExpr size left_dtype right_dtype\n" - "v | v 5 FP64 FP64\n" - "\n" - "Do op(expr) to create a VectorExpression for ewise_add.\n" - "For example: plus(v | v)" - ) - repr_printer(A @ v, "A @ v") - assert repr(A @ v) == ( - "gb.VectorMatMulExpr size left_dtype right_dtype\n" - "A_1 @ v 1 INT64 FP64\n" - "\n" - "Do op(expr) to create a VectorExpression for mxv.\n" - "For example: plus_times(A_1 @ v)" - ) - repr_printer(v @ A.T, "v @ A.T") - assert repr(v @ A.T) == ( - "gb.VectorMatMulExpr size left_dtype right_dtype\n" - "v @ A_1.T 1 FP64 INT64\n" - "\n" - "Do op(expr) to create a VectorExpression for vxm.\n" - "For example: plus_times(v @ A_1.T)" - ) - repr_printer(A & B.T, "A & B.T") - assert repr(A & B.T) == ( - "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" - "A_1 & B_1.T 1 5 INT64 INT64\n" - "\n" - "Do op(expr) to create a MatrixExpression for ewise_mult.\n" - "For example: times(A_1 & B_1.T)" - ) - repr_printer(A | A, "A | A") - assert repr(A | A) == ( - "gb.MatrixEwiseAddExpr nrows ncols left_dtype right_dtype\n" - "A_1 | A_1 1 5 INT64 INT64\n" - "\n" - "Do op(expr) to create a MatrixExpression for ewise_add.\n" - "For example: plus(A_1 | A_1)" - ) - repr_printer(A @ B, "A @ B") - assert repr(A @ B) == ( - "gb.MatrixMatMulExpr nrows ncols left_dtype right_dtype\n" - "A_1 @ B_1 1 1 INT64 INT64\n" - "\n" - "Do op(expr) to create a MatrixExpression for mxm.\n" - "For example: plus_times(A_1 @ B_1)" - ) - repr_printer(A.T @ B.T, "A.T @ B.T") - assert repr(A.T @ B.T) == ( - "gb.MatrixMatMulExpr nrows ncols left_dtype right_dtype\n" - "A_1.T @ B_1.T 5 5 INT64 INT64\n" - "\n" - "Do op(expr) to create a MatrixExpression for mxm.\n" - "For example: plus_times(A_1.T @ B_1.T)" - ) - repr_printer(v @ v, "v @ v") - assert repr(v @ v) == ( - "gb.ScalarMatMulExpr left_dtype right_dtype\n" - "v @ v FP64 FP64\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + + " \n" * 39 + + " \n" + + " \n" * 40 + + " \n" + " \n" + "
0123456789101112131415161718192021222324252627282930313233343536373839...36028797018963928360287970189639293602879701896393036028797018963931360287970189639323602879701896393336028797018963934360287970189639353602879701896393636028797018963937360287970189639383602879701896393936028797018963940360287970189639413602879701896394236028797018963943360287970189639443602879701896394536028797018963946360287970189639473602879701896394836028797018963949360287970189639503602879701896395136028797018963952360287970189639533602879701896395436028797018963955360287970189639563602879701896395736028797018963958360287970189639593602879701896396036028797018963961360287970189639623602879701896396336028797018963964360287970189639653602879701896396636028797018963967
2...
\n" + "
" ) - repr_printer(0.5 | s, "0.5 | s") - assert repr(0.5 | s) == ( - "gb.ScalarEwiseAddExpr left_dtype right_dtype\n" - "0.5 | s_1 float INT8\n" - "\n" - "Do op(expr) to create a ScalarExpression for ewise_add.\n" - "For example: plus(0.5 | s_1)" + html_printer(BIG.ewise_add(small), "BIG.ewise_add(small)") + assert repr_html(BIG.ewise_add(small)) == ( + "
" + f"{CSS_STYLE}" + '
gb.VectorExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
v0.ewise_add(v1, op=monoid.plus[INT64])
size
dtype
36028797018963968INT64
\n" + "
\n" + '
Result is too large to compute!
' ) - html = repr_html(s & 1) - assert "
s1 & 1
" in html - assert "INT8\n int" in html - - -@autocompute -def test_scalar_infix_expr_repr_auto(): - if not pd: # pragma: no cover (import) - pytest.skip("needs pandas") - # As for Vector, only BOOL `x & y` and `x | y` compute without an op, to show the result - s = Scalar.from_value(5, dtype=dtypes.INT8, name="s_1") - repr_printer(s & s, "s & s") - assert repr(s & s) == ( - "gb.ScalarEwiseMultExpr left_dtype right_dtype\n" - "s_1 & s_1 INT8 INT8\n" - "\n" - "Do op(expr) to create a ScalarExpression for ewise_mult.\n" - "For example: times(s_1 & s_1)" + BIG_bool = BIG.dup(dtype=bool) + small_bool = small.dup(dtype=bool) + small_bool[0] = False + html_printer(BIG_bool | small_bool, "BIG_bool | small_bool") + assert repr_html(BIG_bool | small_bool) == ( + "
" + f"{CSS_STYLE}" + '
gb.VectorEwiseAddExpr ?Do op(expr) to create a VectorExpression for ewise_add.
For example: plus(v6 | v7)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 2 + " \n" + "
v6 | v7
size
left_dtype
right_dtype
36028797018963968BOOL
\n" + "
\n" + '
Result is too large to compute!
' ) - assert "Result" not in repr(s | 1) - assert '
' not in repr_html(s & s) - t = Scalar.from_value(True, name="t") - repr_printer(t & True, "t & True") - assert repr(t & True) == ( - "gb.ScalarEwiseMultExpr left_dtype right_dtype\n" - "t & True BOOL bool\n" + C = A.dup(dtype=bool) + html_printer(C & C, "C & C") + assert repr_html(C & C) == ( + "
" + f"{CSS_STYLE}" + '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(M2 & M2)
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
M2 & M2
nvals
nrows
ncols
dtype
315BOOL
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
01234
0FalseTrueTrue
\n" + "
" ) - html = repr_html(False | t) - assert '
' in html - assert "True\n BOOL" in html -@pytest.mark.skipif("not pd") -def test_inner_outer_repr_html(v): - html_printer(v.inner(v), "v.inner(v)") - assert repr_html(v.inner(v)) == ( +def test_display_nan(): + v = Vector.from_coo([0, 1], [1.0, np.nan], size=3, name="v") + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 3 FP64 bitmap\n" + "-------------------------------------\n" + "index 0 1 2\n" + "value 1.0 nan " + ) + html_printer(v, "v") + assert repr_html(v) == ( "
" f"{CSS_STYLE}" - '
gb.ScalarExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '
v
\n' '\n' " \n" - ' \n' + ' \n' + " \n" + " \n" " \n" + " \n" " \n" " \n" + " \n" + " \n" " \n" + " \n" " \n" "
v.inner(v, op=semiring.plus_times[FP64])
gb.Vector
nvals
size
dtype
format
23FP64bitmap
\n" "
\n" - "
" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
012
1.0nan
\n" + "
" ) - html_printer(v.outer(v), "v.outer(v)") - assert repr_html(v.outer(v)) == ( + A = Matrix.from_coo([0, 0], [0, 1], [1.0, np.nan], ncols=3, nrows=2, name="A") + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 2 2 3 FP64 bitmapr\n" + "----------------------------------------------\n" + " 0 1 2\n" + "0 1.0 nan \n" + "1 " + ) + html_printer(A, "A") + assert repr_html(A) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '
A
\n' '\n' " \n" - ' \n' + ' \n' + " \n" " \n" " \n" " \n" + " \n" " \n" - " \n" - " \n" - " \n" + " \n" + " \n" * 2 + " \n" " \n" + " \n" " \n" "
v.outer(v, op=semiring.any_times[FP64])
gb.Matrix
nvals
nrows
ncols
dtype
format
55
23FP64bitmapr
\n" - "
\n" - "
" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 3 + " \n" + " \n" + "
012
01.0nan
1
\n" + "
" ) -@pytest.mark.skipif("not pd") -def test_inner_outer_repr(v): - # XXX: hmm, having `(GrB_Matrix)` here isn't so pretty - repr_printer(v.inner(v), "v.inner(v)") - assert repr(v.inner(v)) == ( - "gb.ScalarExpression dtype\n" - "v.inner((GrB_Matrix)v, op=semiring.plus_times[FP64]) FP64\n" - "\n" - "Do expr.new() or other << expr to calculate the expression." +def test_large_iso(): + A = Matrix(int, nrows=2**60, ncols=2**60) + A[:, :] << 1 + repr_printer(A, "A") + assert repr(A) == ( + '"M_0" nvals nrows ncols dtype format\n' + "gb.Matrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullr (iso)\n" + "--------------------------------------------------------------------------------------------\n" + " 0 ... 1152921504606846975\n" + "0 1 ... 1\n" + "1 1 ... 1\n" + "2 1 ... 1\n" + "3 1 ... 1\n" + "4 1 ... 1\n" + "... ... ... ...\n" + "1152921504606846971 1 ... 1\n" + "1152921504606846972 1 ... 1\n" + "1152921504606846973 1 ... 1\n" + "1152921504606846974 1 ... 1\n" + "1152921504606846975 1 ... 1" ) - repr_printer(v.outer(v), "v.outer(v)") - assert repr(v.outer(v)) == ( - "gb.MatrixExpression nrows ncols dtype\n" - "(GrB_Matrix)v.outer((GrB_Matrix)v, op=semiring.any_times[FP64]) 5 5 FP64\n" - "\n" - "Do expr.new() or other << expr to calculate the expression." + repr_printer(A.T, "A.T") + assert repr(A.T) == ( + '"M_0.T" nvals nrows ncols dtype format\n' + "gb.TransposedMatrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullc (iso)\n" + "------------------------------------------------------------------------------------------------------\n" + " 0 ... 1152921504606846975\n" + "0 1 ... 1\n" + "1 1 ... 1\n" + "2 1 ... 1\n" + "3 1 ... 1\n" + "4 1 ... 1\n" + "... ... ... ...\n" + "1152921504606846971 1 ... 1\n" + "1152921504606846972 1 ... 1\n" + "1152921504606846973 1 ... 1\n" + "1152921504606846974 1 ... 1\n" + "1152921504606846975 1 ... 1" ) - - -@autocompute -def test_autocompute(A, B, v): - if not pd: # pragma: no cover (import) - pytest.skip("needs pandas") - repr_printer(A & A, "A & A") - assert repr(A & A) == ( - "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" - "A_1 & A_1 1 5 INT64 INT64\n" - "\n" - "Do op(expr) to create a MatrixExpression for ewise_mult.\n" - "For example: times(A_1 & A_1)" + repr_printer(A.S, "A.S") + assert repr(A.S) == ( + '"M_0.S" nvals nrows ncols dtype format\n' + "StructuralMask\n" + "of gb.Matrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullr (iso)\n" + "-------------------------------------------------------------------------------------------------\n" + " 0 ... 1152921504606846975\n" + "0 1 ... 1\n" + "1 1 ... 1\n" + "2 1 ... 1\n" + "3 1 ... 1\n" + "4 1 ... 1\n" + "... ... ... ...\n" + "1152921504606846971 1 ... 1\n" + "1152921504606846972 1 ... 1\n" + "1152921504606846973 1 ... 1\n" + "1152921504606846974 1 ... 1\n" + "1152921504606846975 1 ... 1" ) - repr_printer(A.ewise_add(A), "A.ewise_add(A)") - assert repr(A.ewise_add(A)) == ( - "gb.MatrixExpression nrows ncols dtype\n" - "A_1.ewise_add(A_1, op=monoid.plus[INT64]) 1 5 INT64\n" - "\n" - '"Result" nvals nrows ncols dtype\n' - "gb.Matrix 3 1 5 INT64\n" - "-------------------------------------\n" - " 0 1 2 3 4\n" - "0 0 2 4\n" - "\n" - "Do expr.new() or other << expr to calculate the expression." + v = Vector(int, size=2**60) + v[:] = 1 + repr_printer(v, "v") + assert repr(v) == ( + '"v_0" nvals size dtype format\n' + "gb.Vector 1152921504606846976 1152921504606846976 INT64 full (iso)\n" + "----------------------------------------------------------------------\n" + "index 0 ... 1152921504606846975\n" + "value 1 ... 1" + ) + repr_printer(v.S, "v.S") + assert repr(v.S) == ( + '"v_0.S" nvals size dtype format\n' + "StructuralMask\n" + "of gb.Vector 1152921504606846976 1152921504606846976 INT64 full (iso)\n" + "---------------------------------------------------------------------------\n" + "index 0 ... 1152921504606846975\n" + "value 1 ... 1" ) - BIG = Vector(int, size=2**55) - small = Vector(int, size=2**55) - BIG[:] = 1 - small[0] = 2 - repr_printer(BIG.ewise_mult(small), "BIG.ewise_mult(small)") - assert repr(BIG.ewise_mult(small)) == ( - "gb.VectorExpression size dtype\n" - "v_0.ewise_mult(v_1, op=binary.times[INT64]) 36028797018963968 INT64\n" - "\n" - '"Result" nvals size dtype\n' - "gb.Vector 1 36028797018963968 INT64\n" - "------------------------------------------\n" - "index 0 1 ... 36028797018963966 36028797018963967\n" - "value 2 ... \n" + +def test_index_expr_vector(v): + repr_printer(v[0], "v[0]") + assert repr(v[0]) == ( + "gb.ScalarIndexExpr dtype\n" + "v[0] FP64\n" "\n" - "Do expr.new() or other << expr to calculate the expression." + "This expression may be used to extract or assign a Scalar.\n" + "Example extract: v[0].new()\n" + "Example assign: v[0] << s" ) - repr_printer(BIG.ewise_add(small), "BIG.ewise_add(small)") - assert repr(BIG.ewise_add(small)) == ( - "gb.VectorExpression size dtype\n" - "v_0.ewise_add(v_1, op=monoid.plus[INT64]) 36028797018963968 INT64\n" - "\n" - "Result is too large to compute!\n" + repr_printer(v[[0, 1]], "v[[0, 1]]") + assert repr(v[[0, 1]]) == ( + "gb.VectorIndexExpr size dtype\n" + "v[[0, 1]] 2 FP64\n" "\n" - "Do expr.new() or other << expr to calculate the expression." + "This expression may be used to extract or assign a Vector.\n" + "Example extract: v[[0, 1]].new()\n" + "Example assign: v[[0, 1]] << v" ) - BIG_bool = BIG.dup(dtype=bool) - small_bool = small.dup(dtype=bool) - small_bool[0] = False - repr_printer(BIG_bool | small_bool, "BIG_bool | small_bool") - assert repr(BIG_bool | small_bool) == ( - "gb.VectorEwiseAddExpr size left_dtype right_dtype\n" - "v_6 | v_7 36028797018963968 BOOL BOOL\n" - "\n" - "Result is too large to compute!\n" + repr_printer(v[1:3], "v[1:3]") + assert repr(v[1:3]) == ( + "gb.VectorIndexExpr size dtype\n" + "v[1:3] 2 FP64\n" "\n" - "Do op(expr) to create a VectorExpression for ewise_add.\n" - "For example: plus(v_6 | v_7)" + "This expression may be used to extract or assign a Vector.\n" + "Example extract: v[1:3].new()\n" + "Example assign: v[1:3] << v" ) - C = A.dup(dtype=bool) - repr_printer(C & C, "C & C") - assert repr(C & C) == ( - "gb.MatrixEwiseMultExpr nrows ncols left_dtype right_dtype\n" - "M_2 & M_2 1 5 BOOL BOOL\n" + repr_printer(v[::2], "v[::2]") + assert repr(v[::2]) == ( + "gb.VectorIndexExpr size dtype\n" + "v[::2] 3 FP64\n" "\n" - '"Result" nvals nrows ncols dtype\n' - "gb.Matrix 3 1 5 BOOL\n" - "-------------------------------------\n" - " 0 1 2 3 4\n" - "0 False True True\n" + "This expression may be used to extract or assign a Vector.\n" + "Example extract: v[::2].new()\n" + "Example assign: v[::2] << v" + ) + repr_printer(v[[0] * 50], "v[[0] * 50]") + assert repr(v[[0] * 50]) == ( + "gb.VectorIndexExpr size dtype\n" + "v[[0, 0, 0, ...]] 50 FP64\n" "\n" - "Do op(expr) to create a MatrixExpression for ewise_mult.\n" - "For example: times(M_2 & M_2)" + "This expression may be used to extract or assign a Vector.\n" + "Example extract: v[[0, 0, 0, ...]].new()\n" + "Example assign: v[[0, 0, 0, ...]] << v" ) -@autocompute -def test_autocompute_html(A, B, v): - if not pd: # pragma: no cover (import) - pytest.skip("needs pandas") - html_printer(A & A, "A & A") - assert repr_html(A & A) == ( - "
" - f"{CSS_STYLE}" - '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(A1 & A1)
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
A1 & A1
nrows
ncols
right_dtype
left_dtype
15INT64INT64
\n" - "
\n" - "
" - ) - html_printer(A.ewise_add(A), "A.ewise_add(A)") - assert repr_html(A.ewise_add(A)) == ( +def test_index_expr_vector_html(v): + html_printer(v[0], "v[0]") + assert repr_html(v[0]) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '
gb.ScalarIndexExpr ?This expression may be used to extract or assign a Scalar.
Example extract: v[0].new()
Example assign: v[0] << s
\n' '\n' " \n" - ' \n' - " \n" - " \n" - " \n" + ' \n' " \n" " \n" " \n" - " \n" - " \n" - " \n" - " \n" + " \n" " \n" "
A1.ewise_add(A1, op=monoid.plus[INT64])
nvals
nrows
ncols
v[0]
dtype
315INT64FP64
\n" "
\n" - "
\n" - "\n" - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
01234
0024
\n" - "
" + "
" ) - - BIG = Vector(int, size=2**55) - small = Vector(int, size=2**55) - BIG[:] = 1 - small[0] = 2 - html_printer(BIG.ewise_mult(small), "BIG.ewise_mult(small)") - assert repr_html(BIG.ewise_mult(small)) == ( + html_printer(v[[0, 1]], "v[[0, 1]]") + assert repr_html(v[[0, 1]]) == ( "
" f"{CSS_STYLE}" - '
gb.VectorExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '
gb.VectorIndexExpr ?This expression may be used to extract or assign a Vector.
Example extract: v[[0, 1]].new()
Example assign: v[[0, 1]] << v
\n' '\n' " \n" - ' \n' - " \n" + ' \n' " \n" " \n" " \n" " \n" - " \n" - " \n" - " \n" + " \n" + " \n" " \n" "
v0.ewise_mult(v1, op=binary.times[INT64])
nvals
v[[0, 1]]
size
dtype
136028797018963968INT642FP64
\n" "
\n" - "
\n" - "\n" - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - + " \n" * 39 - + " \n" - + " \n" * 40 - + " \n" - " \n" - "
0123456789101112131415161718192021222324252627282930313233343536373839...36028797018963928360287970189639293602879701896393036028797018963931360287970189639323602879701896393336028797018963934360287970189639353602879701896393636028797018963937360287970189639383602879701896393936028797018963940360287970189639413602879701896394236028797018963943360287970189639443602879701896394536028797018963946360287970189639473602879701896394836028797018963949360287970189639503602879701896395136028797018963952360287970189639533602879701896395436028797018963955360287970189639563602879701896395736028797018963958360287970189639593602879701896396036028797018963961360287970189639623602879701896396336028797018963964360287970189639653602879701896396636028797018963967
2...
\n" - "
" + "This expression may be used to extract or assign a Matrix.\n" + "Example extract: A_1[:, [1, 2]].new()\n" + "Example assign: A_1[:, [1, 2]] << M" ) - html_printer(BIG.ewise_add(small), "BIG.ewise_add(small)") - assert repr_html(BIG.ewise_add(small)) == ( + + +def test_index_expr_matrix_html(A): + html_printer(A[0, 1], "A[0, 1]") + assert repr_html(A[0, 1]) == ( "
" f"{CSS_STYLE}" - '
gb.VectorExpression ?Do expr.new() or other << expr to calculate the expression.
\n' + '
gb.ScalarIndexExpr ?This expression may be used to extract or assign a Scalar.
Example extract: A_1[0, 1].new()
Example assign: A_1[0, 1] << s
\n' '\n' " \n" - ' \n' - " \n" + ' \n' " \n" " \n" " \n" - " \n" " \n" " \n" "
v0.ewise_add(v1, op=monoid.plus[INT64])
size
A_1[0, 1]
dtype
36028797018963968INT64
\n" "
\n" - '
Result is too large to compute!
' + "
" ) - BIG_bool = BIG.dup(dtype=bool) - small_bool = small.dup(dtype=bool) - small_bool[0] = False - html_printer(BIG_bool | small_bool, "BIG_bool | small_bool") - assert repr_html(BIG_bool | small_bool) == ( + html_printer(A[:2, [1, 2]], "A[:2, [1, 2]]") + assert repr_html(A[:2, [1, 2]]) == ( "
" f"{CSS_STYLE}" - '
gb.VectorEwiseAddExpr ?Do op(expr) to create a VectorExpression for ewise_add.
For example: plus(v6 | v7)
\n' + '
gb.MatrixIndexExpr ?This expression may be used to extract or assign a Matrix.
Example extract: A_1[:, [1, 2]].new()
Example assign: A_1[:, [1, 2]] << M
\n' '\n' " \n" - ' \n' - " \n" - " \n" - " \n" + ' \n' + " \n" + " \n" + " \n" " \n" " \n" - " \n" + " \n" * 2 + " \n" + " \n" + " \n" + " \n" + " \n" "
v6 | v7
size
left_dtype
right_dtype
A_1[:, [1, 2]]
nrows
ncols
dtype
36028797018963968BOOL
12INT64
\n" "
\n" - '
Result is too large to compute!
' + "
" ) - C = A.dup(dtype=bool) - html_printer(C & C, "C & C") - assert repr_html(C & C) == ( - "
" + + +def test_scalar_as_vector(): + s = Scalar.from_value(5, is_cscalar=False) # pragma: is_grbscalar + v = s._as_vector() + repr_printer(v, "v") + assert repr(v) == ( + '"(GrB_Vector)s_0" nvals size dtype format\n' + "gb.Vector 1 1 INT64 full (iso)\n" + "-------------------------------------------------\n" + "index 0\n" + "value 5" + ) + expr = v.reduce() + repr_printer(expr, "expr") + assert repr(expr) == ( + "gb.ScalarExpression dtype\n" + "(GrB_Vector)s_0.reduce(monoid.plus[INT64]) INT64\n" + "\n" + "Do expr.new() or other << expr to calculate the expression." + ) + html_printer(v, "v") + assert repr_html(v) == ( f"{CSS_STYLE}" - '
gb.MatrixEwiseMultExpr ?Do op(expr) to create a MatrixExpression for ewise_mult.
For example: times(M2 & M2)
\n' + '
s0
\n' '\n' " \n" - ' \n' - " \n" - " \n" - " \n" + ' \n' + " \n" " \n" " \n" " \n" - " \n" - " \n" " \n" - " \n" + " \n" " \n" "
M2 & M2
nvals
nrows
ncols
gb.Scalar
value
dtype
315BOOLINT64
\n" "
\n" - "
\n" - "\n" - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
01234
0FalseTrueTrue
\n" - "
" - ) - - -@pytest.mark.skipif("not pd") -def test_display_nan(): - v = Vector.from_coo([0, 1], [1.0, np.nan], size=3, name="v") - repr_printer(v, "v") - assert repr(v) == ( - '"v" nvals size dtype format\n' - "gb.Vector 2 3 FP64 bitmap\n" - "-------------------------------------\n" - "index 0 1 2\n" - "value 1.0 nan " + "
" ) - html_printer(v, "v") - assert repr_html(v) == ( + html_printer(expr, "expr") + assert repr_html(expr) == ( "
" f"{CSS_STYLE}" - '
v
\n' + '
gb.ScalarExpression ?Do expr.new() or other << expr to calculate the expression.
\n' '\n' " \n" - ' \n' - " \n" - " \n" + ' \n' " \n" - " \n" " \n" " \n" - " \n" - " \n" - " \n" - " \n" + " \n" " \n" "
gb.Vector
nvals
size
s0.reduce(monoid.plus[INT64])
dtype
format
23FP64bitmapINT64
\n" "
\n" - "
\n" - "\n" - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
012
1.0nan
\n" - "
" - ) - A = Matrix.from_coo([0, 0], [0, 1], [1.0, np.nan], ncols=3, nrows=2, name="A") - repr_printer(A, "A") - assert repr(A) == ( - '"A" nvals nrows ncols dtype format\n' - "gb.Matrix 2 2 3 FP64 bitmapr\n" - "----------------------------------------------\n" - " 0 1 2\n" - "0 1.0 nan \n" - "1 " + "
" ) - html_printer(A, "A") - assert repr_html(A) == ( + + +@autocompute +def test_index_expr_autocompute(v): + html_printer(v[[0, 1]], "v[[0, 1]]") + assert repr_html(v[[0, 1]]) == ( "
" f"{CSS_STYLE}" - '
A
\n' + '
gb.VectorIndexExpr ?This expression may be used to extract or assign a Vector.
Example extract: v[[0, 1]].new()
Example assign: v[[0, 1]] << v
\n' '\n' " \n" - ' \n' + ' \n' " \n" - " \n" - " \n" + " \n" " \n" - " \n" " \n" - " \n" + " \n" * 2 + " \n" + " \n" + " \n" + " \n" " \n" - " \n" " \n" "
gb.Matrix
v[[0, 1]]
nvals
nrows
ncols
size
dtype
format
23
12FP64bitmapr
\n" "
\n" @@ -4652,330 +6109,554 @@ def test_display_nan(): " \n" " 0\n" " 1\n" - " 2\n" " \n" " \n" " \n" " \n" - " 0\n" - " 1.0\n" - " nan\n" + " \n" + " 0.0\n" " \n" " \n" - " \n" - " 1\n" + " \n" * 3 + " \n" " \n" "\n" "
" ) -@pytest.mark.skipif("not pd") -def test_large_iso(): - A = Matrix(int, nrows=2**60, ncols=2**60) - A[:, :] << 1 +def test_udt(): + record_dtype = np.dtype([("x", np.bool_), ("y", np.int64)], align=True) + udt = dtypes.register_anonymous(record_dtype, "record_dtype") + v = Vector(udt, size=2) + v[:] = (False, 2) + v[1] = (True, 3) + repr_printer(v, "v") + assert repr(v) == ( + '"v_0" nvals size dtype format\n' + "gb.Vector 2 2 record_dtype full\n" + "--------------------------------------------\n" + "index 0 1\n" + "value (False, 2) (True, 3)" + ) + A = Matrix(udt, nrows=1, ncols=3) + A[:, :2] = 0 repr_printer(A, "A") assert repr(A) == ( - '"M_0" nvals nrows ncols dtype format\n' - "gb.Matrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullr (iso)\n" - "--------------------------------------------------------------------------------------------\n" - " 0 ... 1152921504606846975\n" - "0 1 ... 1\n" - "1 1 ... 1\n" - "2 1 ... 1\n" - "3 1 ... 1\n" - "4 1 ... 1\n" - "... ... ... ...\n" - "1152921504606846971 1 ... 1\n" - "1152921504606846972 1 ... 1\n" - "1152921504606846973 1 ... 1\n" - "1152921504606846974 1 ... 1\n" - "1152921504606846975 1 ... 1" + '"M_0" nvals nrows ncols dtype format\n' + "gb.Matrix 2 1 3 record_dtype bitmapr (iso)\n" + "-----------------------------------------------------------\n" + " 0 1 2\n" + "0 (False, 0) (False, 0) " ) - repr_printer(A.T, "A.T") - assert repr(A.T) == ( - '"M_0.T" nvals nrows ncols dtype format\n' - "gb.TransposedMatrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullc (iso)\n" - "------------------------------------------------------------------------------------------------------\n" - " 0 ... 1152921504606846975\n" - "0 1 ... 1\n" - "1 1 ... 1\n" - "2 1 ... 1\n" - "3 1 ... 1\n" - "4 1 ... 1\n" - "... ... ... ...\n" - "1152921504606846971 1 ... 1\n" - "1152921504606846972 1 ... 1\n" - "1152921504606846973 1 ... 1\n" - "1152921504606846974 1 ... 1\n" - "1152921504606846975 1 ... 1" + np_dtype = np.dtype("(3,)uint16") + udt2 = dtypes.register_anonymous(np_dtype, "has_subdtype") + v = Vector(udt2, size=2) + v[:] = (1, 2, 3) + v[1] = (3, 2, 1) + repr_printer(v, "v") + assert repr(v) == ( + '"v_1" nvals size dtype format\n' + "gb.Vector 2 2 has_subdtype full\n" + "--------------------------------------------\n" + "index 0 1\n" + "value [1, 2, 3] [3, 2, 1]" ) - repr_printer(A.S, "A.S") - assert repr(A.S) == ( - '"M_0.S" nvals nrows ncols dtype format\n' - "StructuralMask\n" - "of gb.Matrix 9223372036854775807 1152921504606846976 1152921504606846976 INT64 fullr (iso)\n" - "-------------------------------------------------------------------------------------------------\n" - " 0 ... 1152921504606846975\n" - "0 1 ... 1\n" - "1 1 ... 1\n" - "2 1 ... 1\n" - "3 1 ... 1\n" - "4 1 ... 1\n" - "... ... ... ...\n" - "1152921504606846971 1 ... 1\n" - "1152921504606846972 1 ... 1\n" - "1152921504606846973 1 ... 1\n" - "1152921504606846974 1 ... 1\n" - "1152921504606846975 1 ... 1" + A = Matrix(udt2, nrows=1, ncols=3) + A[:, :2] = 1 + repr_printer(A, "A") + assert repr(A) == ( + '"M_1" nvals nrows ncols dtype format\n' + "gb.Matrix 2 1 3 has_subdtype bitmapr (iso)\n" + "-----------------------------------------------------------\n" + " 0 1 2\n" + "0 [1, 1, 1] [1, 1, 1] " ) - v = Vector(int, size=2**60) - v[:] = 1 + + +def test_udt_one_field_repr(): + # A record with one field shows as a 1-tuple, trailing comma included + udt = dtypes.register_anonymous(np.dtype([("one_field_x", np.int16)]), "one_field") + v = Vector(udt, size=3, name="v") + v[0] = (5,) + v[2] = (-7,) + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 3 one_field bitmap\n" + "-----------------------------------------\n" + "index 0 1 2\n" + "value (5,) (-7,)" + ) + # In COO form too, where main's pandas renderer raised TypeError on the record column + v = Vector(udt, size=1000, name="v") + v[100] = (5,) + v[500] = (-7,) repr_printer(v, "v") assert repr(v) == ( - '"v_0" nvals size dtype format\n' - "gb.Vector 1152921504606846976 1152921504606846976 INT64 full (iso)\n" - "----------------------------------------------------------------------\n" - "index 0 ... 1152921504606846975\n" - "value 1 ... 1" - ) - repr_printer(v.S, "v.S") - assert repr(v.S) == ( - '"v_0.S" nvals size dtype format\n' - "StructuralMask\n" - "of gb.Vector 1152921504606846976 1152921504606846976 INT64 full (iso)\n" - "---------------------------------------------------------------------------\n" - "index 0 ... 1152921504606846975\n" - "value 1 ... 1" + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 one_field sparse\n" + "-----------------------------------------\n" + " index val\n" + "0 100 (5,)\n" + "1 500 (-7,)" ) -def test_index_expr_vector(v): - repr_printer(v[0], "v[0]") - assert repr(v[0]) == ( - "gb.ScalarIndexExpr dtype\n" - "v[0] FP64\n" - "\n" - "This expression may be used to extract or assign a Scalar.\n" - "Example extract: v[0].new()\n" - "Example assign: v[0] << s" - ) - repr_printer(v[[0, 1]], "v[[0, 1]]") - assert repr(v[[0, 1]]) == ( - "gb.VectorIndexExpr size dtype\n" - "v[[0, 1]] 2 FP64\n" - "\n" - "This expression may be used to extract or assign a Vector.\n" - "Example extract: v[[0, 1]].new()\n" - "Example assign: v[[0, 1]] << v" - ) - repr_printer(v[1:3], "v[1:3]") - assert repr(v[1:3]) == ( - "gb.VectorIndexExpr size dtype\n" - "v[1:3] 2 FP64\n" - "\n" - "This expression may be used to extract or assign a Vector.\n" - "Example extract: v[1:3].new()\n" - "Example assign: v[1:3] << v" +def test_truncated_udt_repr(): + # The rows and columns shown are read with extract and assign, which need no + # operator, so a UDT object too large to show whole also shows without Numba + udt = dtypes.register_anonymous(np.dtype([("tr_x", np.int16)]), "tr_rec") + v = Vector(udt, size=300, name="v") + v[:] = (1,) + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 300 300 tr_rec full (iso)\n" + "------------------------------------------\n" + "index 0 1 2 3 4 5 ... 294 295 296 297 298 299\n" + "value (1,) (1,) (1,) (1,) (1,) (1,) ... (1,) (1,) (1,) (1,) (1,) (1,)" ) - repr_printer(v[::2], "v[::2]") - assert repr(v[::2]) == ( - "gb.VectorIndexExpr size dtype\n" - "v[::2] 3 FP64\n" - "\n" - "This expression may be used to extract or assign a Vector.\n" - "Example extract: v[::2].new()\n" - "Example assign: v[::2] << v" + A = Matrix(udt, nrows=100, ncols=300, name="A") + A[0, :] = (2,) + A[99, 299] = (3,) + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 301 100 300 tr_rec hypercsr\n" + "------------------------------------------------\n" + " 0 1 2 3 4 5 ... 294 295 296 297 298 299\n" + "0 (2,) (2,) (2,) (2,) (2,) (2,) ... (2,) (2,) (2,) (2,) (2,) (2,)\n" + "1 ... \n" + "2 ... \n" + "3 ... \n" + "4 ... \n" + ".. ... ... ... ... ... ... ... ... ... ... ... ... ...\n" + "95 ... \n" + "96 ... \n" + "97 ... \n" + "98 ... \n" + "99 ... (3,)" ) - repr_printer(v[[0] * 50], "v[[0] * 50]") - assert repr(v[[0] * 50]) == ( - "gb.VectorIndexExpr size dtype\n" - "v[[0, 0, 0, ...]] 50 FP64\n" - "\n" - "This expression may be used to extract or assign a Vector.\n" - "Example extract: v[[0, 0, 0, ...]].new()\n" - "Example assign: v[[0, 0, 0, ...]] << v" + repr_printer(A.T, "A.T") + assert repr(A.T) == ( + '"A.T" nvals nrows ncols dtype format\n' + "gb.TransposedMatrix 301 300 100 tr_rec hypercsc\n" + "----------------------------------------------------------\n" + " 0 1 2 3 4 5 6 7 8 9 10 ... 89 90 91 92 93 94 95 96 97 98 99\n" + "0 (2,) ... \n" + "1 (2,) ... \n" + "2 (2,) ... \n" + "3 (2,) ... \n" + "4 (2,) ... \n" + ".. ... .. .. .. .. .. .. .. .. .. .. ... .. .. .. .. .. .. .. .. .. .. ...\n" + "295 (2,) ... \n" + "296 (2,) ... \n" + "297 (2,) ... \n" + "298 (2,) ... \n" + "299 (2,) ... (3,)" + ) + + +def test_sparse_array_udt_repr(): + # An array UDT in COO form shows a list per entry, as the dense grid does + udt = dtypes.register_anonymous(np.dtype((np.int8, (3,))), "int8x3") + v = Vector(udt, size=1000, name="v") + v[100] = (1, 2, 3) + v[500] = (-1, 0, 1) + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 2 1000 int8x3 sparse\n" + "--------------------------------------\n" + " index val\n" + "0 100 [1, 2, 3]\n" + "1 500 [-1, 0, 1]" ) - - -def test_index_expr_vector_html(v): - html_printer(v[0], "v[0]") - assert repr_html(v[0]) == ( + html_printer(v, "v") + assert repr_html(v) == ( "
" f"{CSS_STYLE}" - '
gb.ScalarIndexExpr ?This expression may be used to extract or assign a Scalar.
Example extract: v[0].new()
Example assign: v[0] << s
\n' + '
v
\n' '\n' " \n" - ' \n' + ' \n' + " \n" + " \n" " \n" + " \n" " \n" " \n" - " \n" + " \n" + " \n" + " \n" + " \n" " \n" "
v[0]
gb.Vector
nvals
size
dtype
format
FP6421000int8x3sparse
\n" "
\n" - "
" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
indexval
0100[1, 2, 3]
1500[-1, 0, 1]
\n" + "
" ) - html_printer(v[[0, 1]], "v[[0, 1]]") - assert repr_html(v[[0, 1]]) == ( + A = Matrix(udt, nrows=1000, ncols=1000, name="A") + A[100, 5] = (1, 2, 3) + A[300, 7] = (4, -5, 6) + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 2 1000 1000 int8x3 hypercsr\n" + "------------------------------------------------\n" + " row col val\n" + "0 100 5 [1, 2, 3]\n" + "1 300 7 [4, -5, 6]" + ) + html_printer(A, "A") + assert repr_html(A) == ( "
" f"{CSS_STYLE}" - '
gb.VectorIndexExpr ?This expression may be used to extract or assign a Vector.
Example extract: v[[0, 1]].new()
Example assign: v[[0, 1]] << v
\n' + '
A
\n' '\n' " \n" - ' \n' - " \n" + ' \n' + " \n" + " \n" + " \n" " \n" + " \n" " \n" " \n" - " \n" - " \n" + " \n" + " \n" * 2 + " \n" + " \n" " \n" "
v[[0, 1]]
size
gb.Matrix
nvals
nrows
ncols
dtype
format
2FP6421000int8x3hypercsr
\n" "
\n" - "
" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
rowcolval
01005[1, 2, 3]
13007[4, -5, 6]
\n" + "
" ) -def test_index_expr_matrix(A): - repr_printer(A[0, 1], "A[0, 1]") - assert repr(A[0, 1]) == ( - "gb.ScalarIndexExpr dtype\n" - "A_1[0, 1] INT64\n" - "\n" - "This expression may be used to extract or assign a Scalar.\n" - "Example extract: A_1[0, 1].new()\n" - "Example assign: A_1[0, 1] << s" +def test_sparse_udt_repr_past_max_rows(): + # More values than display.max_rows: the first min_rows are read with an iterator + rec = dtypes.register_anonymous( + np.dtype([("rh_x", np.int16), ("rh_y", np.float32)]), "rec_head" ) - repr_printer(A[:2, [1, 2]], "A[:2, [1, 2]]") - assert repr(A[:2, [1, 2]]) == ( - "gb.MatrixIndexExpr nrows ncols dtype\n" - "A_1[:, [1, 2]] 1 2 INT64\n" - "\n" - "This expression may be used to extract or assign a Matrix.\n" - "Example extract: A_1[:, [1, 2]].new()\n" - "Example assign: A_1[:, [1, 2]] << M" + arr = dtypes.register_anonymous(np.dtype((np.int8, (3,))), "int8x3") + v = Vector(rec, size=1000, name="v") + A = Matrix(arr, nrows=1000, ncols=1000, name="A") + for i in range(5): + v[100 + 100 * i] = (i, i / 2) + A[100 + 100 * i, i] = (i, -i, 2 * i) + with display_options("display.max_rows", 4, "display.min_rows", 2): + repr_printer(v, "v") + assert repr(v) == ( + '"v" nvals size dtype format\n' + "gb.Vector 5 1000 rec_head sparse\n" + "----------------------------------------\n" + " index val\n" + "0 100 (0, 0.0)\n" + "1 200 (1, 0.5)\n" + "... ... ..." + ) + repr_printer(A, "A") + assert repr(A) == ( + '"A" nvals nrows ncols dtype format\n' + "gb.Matrix 5 1000 1000 int8x3 hypercsr\n" + "------------------------------------------------\n" + " row col val\n" + "0 100 0 [0, 0, 0]\n" + "1 200 1 [1, -1, 2]\n" + "... ... ... ..." + ) + html_printer(A, "A") + assert repr_html(A) == ( + "
" + f"{CSS_STYLE}" + '
A
\n' + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 2 + " \n" + " \n" + " \n" + "
gb.Matrix
nvals
nrows
ncols
dtype
format
51000int8x3hypercsr
\n" + "
\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" * 3 + " \n" + " \n" + "
rowcolval
01000[0, 0, 0]
12001[1, -1, 2]
......
\n" + "
" + ) + + +def test_empty(): + v = Vector(int, 0) + repr_printer(v, "v") + assert repr(v) == ( + '"v_0" nvals size dtype format\n' + "gb.Vector 0 0 INT64 sparse\n" + "-------------------------------------" + ) + A = Matrix(int, 0, 0) + repr_printer(A, "A") + assert repr(A) == ( + '"M_0" nvals nrows ncols dtype format\n' + "gb.Matrix 0 0 0 INT64 csr\n" + "---------------------------------------------" + ) + A = Matrix(int, 0, 5) + repr_printer(A, "A") + assert repr(A) == ( + '"M_1" nvals nrows ncols dtype format\n' + "gb.Matrix 0 0 5 INT64 csr\n" + "---------------------------------------------" ) -def test_index_expr_matrix_html(A): - html_printer(A[0, 1], "A[0, 1]") - assert repr_html(A[0, 1]) == ( +def test_empty_repr_html(): + # The text repr of an object with no rows or columns stops at the header; the + # HTML repr still shows the table, with its row and column labels. + A = Matrix(int, 0, 5, name="A") + html_printer(A, "A") + assert repr_html(A) == ( "
" f"{CSS_STYLE}" - '
gb.ScalarIndexExpr ?This expression may be used to extract or assign a Scalar.
Example extract: A_1[0, 1].new()
Example assign: A_1[0, 1] << s
\n' + '
A
\n' '\n' " \n" - ' \n' + ' \n' + " \n" + " \n" + " \n" " \n" + " \n" " \n" - " \n" + " \n" + " \n" * 2 + " \n" " \n" + " \n" " \n" "
A_1[0, 1]
gb.Matrix
nvals
nrows
ncols
dtype
format
05INT64csr
\n" "
\n" - "
" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
01234
\n" + "
" ) - html_printer(A[:2, [1, 2]], "A[:2, [1, 2]]") - assert repr_html(A[:2, [1, 2]]) == ( + A = Matrix(int, 5, 0, name="A") + html_printer(A, "A") + assert repr_html(A) == ( "
" f"{CSS_STYLE}" - '
gb.MatrixIndexExpr ?This expression may be used to extract or assign a Matrix.
Example extract: A_1[:, [1, 2]].new()
Example assign: A_1[:, [1, 2]] << M
\n' + '
A
\n' '\n' " \n" - ' \n' + ' \n' + " \n" " \n" " \n" " \n" + " \n" " \n" " \n" - " \n" - " \n" + " \n" + " \n" + " \n" " \n" + " \n" " \n" "
A_1[:, [1, 2]]
gb.Matrix
nvals
nrows
ncols
dtype
format
12050INT64hypercsr
\n" "
\n" - "
" - ) - - -@pytest.mark.skipif("not pd") -def test_scalar_as_vector(): - s = Scalar.from_value(5, is_cscalar=False) # pragma: is_grbscalar - v = s._as_vector() - repr_printer(v, "v") - assert repr(v) == ( - '"(GrB_Vector)s_0" nvals size dtype format\n' - "gb.Vector 1 1 INT64 full (iso)\n" - "-------------------------------------------------\n" - "index 0\n" - "value 5" - ) - expr = v.reduce() - repr_printer(expr, "expr") - assert repr(expr) == ( - "gb.ScalarExpression dtype\n" - "(GrB_Vector)s_0.reduce(monoid.plus[INT64]) INT64\n" + "
\n" + "\n" + '\n' + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
0
1
2
3
4
\n" + "
" ) + v = Vector(int, 0, name="v") html_printer(v, "v") assert repr_html(v) == ( - f"{CSS_STYLE}" - '
s0
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "
gb.Scalar
value
dtype
5INT64
\n" - "
\n" - "
" - ) - html_printer(expr, "expr") - assert repr_html(expr) == ( - "
" - f"{CSS_STYLE}" - '
gb.ScalarExpression ?Do expr.new() or other << expr to calculate the expression.
\n' - '\n' - " \n" - ' \n' - " \n" - " \n" - " \n" - " \n" - " \n" - "
s0.reduce(monoid.plus[INT64])
dtype
INT64
\n" - "
\n" - "
" - ) - - -@autocompute -def test_index_expr_autocompute(v): - if not pd: # pragma: no cover (import) - pytest.skip("needs pandas") - html_printer(v[[0, 1]], "v[[0, 1]]") - assert repr_html(v[[0, 1]]) == ( "
" f"{CSS_STYLE}" - '
gb.VectorIndexExpr ?This expression may be used to extract or assign a Vector.
Example extract: v[[0, 1]].new()
Example assign: v[[0, 1]] << v
\n' + '
v
\n' '\n' " \n" - ' \n' + ' \n' " \n" " \n" " \n" + " \n" " \n" - " \n" - " \n" - " \n" - " \n" + " \n" + " \n" * 2 + " \n" + " \n" " \n" "
v[[0, 1]]
gb.Vector
nvals
size
dtype
format
12FP64
0INT64sparse
\n" "
\n" @@ -4997,15 +6678,11 @@ def test_index_expr_autocompute(v): " \n" ' \n' " \n" - " 0\n" - " 1\n" " \n" " \n" " \n" " \n" " \n" - " 0.0\n" - " \n" " \n" " \n" "\n" @@ -5013,82 +6690,6 @@ def test_index_expr_autocompute(v): ) -@pytest.mark.skipif("not pd") -def test_udt(): - record_dtype = np.dtype([("x", np.bool_), ("y", np.int64)], align=True) - udt = dtypes.register_anonymous(record_dtype, "record_dtype") - v = Vector(udt, size=2) - v[:] = (False, 2) - v[1] = (True, 3) - repr_printer(v, "v") - assert repr(v) == ( - '"v_0" nvals size dtype format\n' - "gb.Vector 2 2 record_dtype full\n" - "--------------------------------------------\n" - "index 0 1\n" - "value (False, 2) (True, 3)" - ) - A = Matrix(udt, nrows=1, ncols=3) - A[:, :2] = 0 - repr_printer(A, "A") - assert repr(A) == ( - '"M_0" nvals nrows ncols dtype format\n' - "gb.Matrix 2 1 3 record_dtype bitmapr (iso)\n" - "-----------------------------------------------------------\n" - " 0 1 2\n" - "0 (False, 0) (False, 0) " - ) - np_dtype = np.dtype("(3,)uint16") - udt2 = dtypes.register_anonymous(np_dtype, "has_subdtype") - v = Vector(udt2, size=2) - v[:] = (1, 2, 3) - v[1] = (3, 2, 1) - repr_printer(v, "v") - assert repr(v) == ( - '"v_1" nvals size dtype format\n' - "gb.Vector 2 2 has_subdtype full\n" - "--------------------------------------------\n" - "index 0 1\n" - "value [1, 2, 3] [3, 2, 1]" - ) - A = Matrix(udt2, nrows=1, ncols=3) - A[:, :2] = 1 - repr_printer(A, "A") - assert repr(A) == ( - '"M_1" nvals nrows ncols dtype format\n' - "gb.Matrix 2 1 3 has_subdtype bitmapr (iso)\n" - "-----------------------------------------------------------\n" - " 0 1 2\n" - "0 [1, 1, 1] [1, 1, 1] " - ) - - -@pytest.mark.skipif("not pd") -def test_empty(): - v = Vector(int, 0) - repr_printer(v, "v") - assert repr(v) == ( - '"v_0" nvals size dtype format\n' - "gb.Vector 0 0 INT64 sparse\n" - "-------------------------------------" - ) - A = Matrix(int, 0, 0) - repr_printer(A, "A") - assert repr(A) == ( - '"M_0" nvals nrows ncols dtype format\n' - "gb.Matrix 0 0 0 INT64 csr\n" - "---------------------------------------------" - ) - A = Matrix(int, 0, 5) - repr_printer(A, "A") - assert repr(A) == ( - '"M_1" nvals nrows ncols dtype format\n' - "gb.Matrix 0 0 5 INT64 csr\n" - "---------------------------------------------" - ) - - -@pytest.mark.skipif("not pd") def test_vector_as_matrix(): # Built iso outright: the repr asserted below names the format, and # SuiteSparse 10.5.0 no longer infers iso from a build whose values happen diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 622b7e389..c1efcdb70 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -3999,6 +3999,31 @@ def test_ss_iteration(A): assert next(A.ss.iteritems()) is not None +@pytest.mark.skipif("not suitesparse") +def test_ss_iteration_udt(): + # A UDT has no iterator getter of its own type; its values come from the UDT getter + rec = dtypes.register_anonymous( + np.dtype([("it_x", np.int16), ("it_y", np.float32)]), "IterRecord" + ) + pair = dtypes.register_anonymous(np.dtype((np.uint32, (2,))), "IterPair") + for udt, values in [(rec, [(1, 0.5), (-2, 1.5), (3, -2.5)]), (pair, [(1, 2), (3, 4), (5, 6)])]: + A = Matrix.from_coo( + [0, 1, 2], [2, 0, 1], np.array(values, dtype=udt.np_type), dtype=udt, nrows=3, ncols=3 + ) + rows, cols, expected = A.to_coo() + assert [x.tolist() for x in A.ss.itervalues()] == expected.tolist() + r, c, vals = zip(*A.ss.iteritems(), strict=True) + assert list(r) == rows.tolist() + assert list(c) == cols.tolist() + assert [x.tolist() for x in vals] == expected.tolist() + # head of fewer than nvals reads with the iterator + head_rows, head_cols, head_values = A.ss.head(2, sort=True) + assert_array_equal(head_rows, rows[:2]) + assert_array_equal(head_cols, cols[:2]) + assert head_values.dtype == expected.dtype + assert_array_equal(head_values, expected[:2]) + + @pytest.mark.skipif("not supports_udfs") @pytest.mark.slow def test_udt(): diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index deac8cc85..0bfa80b21 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -2832,6 +2832,27 @@ def test_ss_iteration(v): assert next(v.ss.iteritems()) is not None +@pytest.mark.skipif("not suitesparse") +def test_ss_iteration_udt(): + # A UDT has no iterator getter of its own type; its values come from the UDT getter + rec = dtypes.register_anonymous( + np.dtype([("it_x", np.int16), ("it_y", np.float32)]), "IterRecord" + ) + pair = dtypes.register_anonymous(np.dtype((np.uint32, (2,))), "IterPair") + for udt, values in [(rec, [(1, 0.5), (-2, 1.5), (3, -2.5)]), (pair, [(1, 2), (3, 4), (5, 6)])]: + v = Vector.from_coo([1, 4, 7], np.array(values, dtype=udt.np_type), dtype=udt, size=9) + indices, expected = v.to_coo() + assert [x.tolist() for x in v.ss.itervalues()] == expected.tolist() + keys, vals = zip(*v.ss.iteritems(), strict=True) + assert list(keys) == indices.tolist() + assert [x.tolist() for x in vals] == expected.tolist() + # head of fewer than nvals reads with the iterator + head_indices, head_values = v.ss.head(2, sort=True) + assert_array_equal(head_indices, indices[:2]) + assert head_values.dtype == expected.dtype + assert_array_equal(head_values, expected[:2]) + + def test_broadcasting(A, v): # Vector on left expected = A.dup()