Skip to content

Commit

Permalink
gh-37606: Border matrix
Browse files Browse the repository at this point in the history
    
<!-- ^ Please provide a concise and informative title. -->
<!-- ^ Don't put issue numbers in the title, do this in the PR
description below. -->
<!-- ^ For example, instead of "Fixes #12345" use "Introduce new method
to calculate 1 + 2". -->
<!-- v Describe your changes below in detail. -->
<!-- v Why is this change required? What problem does it solve? -->
<!-- v If this PR resolves an open issue, please link to it here. For
example, "Fixes #12345". -->
Plain TeX users may remember `\bordermatrix`. Here we implement this as
options of the `Matrix` class's `str` method.
```
            sage: M = matrix([[1,2,3], [4,5,6], [7,8,9]])
            sage: M.subdivide(None, 2)
            sage: print(M.str(unicode=True,
            ....:             top_border=['ab', 'cde', 'f'],
            ....:             bottom_border=['*', '', ''],
            ....:             left_border=[1, 10, 100],
            ....:             right_border=['', ' <', '']))
                        ab cde   f
                     1⎛  1   2│  3⎞
                    10⎜  4   5│  6⎟ <
                   100⎝  7   8│  9⎠
                         *
```

Follow-up PR: As the guiding application for this feature, we equip
finite-dimensional modules with basis with methods `_repr_matrix`,
`_ascii_art_matrix`, `_unicode_art_matrix` that can be used as in this
example:
```
                sage: M = matrix(ZZ, [[1, 0, 0], [0, 1, 0]],
                ....:            column_keys=['a', 'b', 'c'],
                ....:            row_keys=['v', 'w']); M
                Generic morphism:
                From: Free module generated by {'a', 'b', 'c'} over
Integer Ring
                To:   Free module generated by {'v', 'w'} over Integer
Ring
                sage: M._unicode_art_ = M._unicode_art_matrix
                sage: unicode_art(M)                            #
indirect doctest
                    a b c
                  v⎛1 0 0⎞
                  w⎝0 1 0⎠
```

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [ ] I have created tests covering the changes.
- [ ] I have updated the documentation accordingly.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - #12345: short description why this is a dependency -->
<!-- - #34567: ... -->
    
URL: #37606
Reported by: Matthias Köppe
Reviewer(s): David Coudert, Matthias Köppe
  • Loading branch information
Release Manager committed Apr 7, 2024
2 parents 58e8c57 + d45422e commit 13a555d
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 19 deletions.
135 changes: 119 additions & 16 deletions src/sage/matrix/matrix0.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,9 @@ cdef class Matrix(sage.structure.element.Matrix):
return self.str()

def str(self, rep_mapping=None, zero=None, plus_one=None, minus_one=None,
*, unicode=False, shape=None, character_art=False):
*, unicode=False, shape=None, character_art=False,
left_border=None, right_border=None,
top_border=None, bottom_border=None):
r"""
Return a nice string representation of the matrix.
Expand Down Expand Up @@ -1846,6 +1848,16 @@ cdef class Matrix(sage.structure.element.Matrix):
:class:`~sage.typeset.unicode_art.UnicodeArt` which support line
breaking of wide matrices that exceed the window width
- ``left_border``, ``right_border`` -- sequence (default: ``None``);
if not ``None``, call :func:`str` on the elements and use the
results as labels for the rows of the matrix. The labels appear
outside of the parentheses.
- ``top_border``, ``bottom_border`` -- sequence (default: ``None``);
if not ``None``, call :func:`str` on the elements and use the
results as labels for the columns of the matrix. The labels appear
outside of the parentheses.
EXAMPLES::
sage: R = PolynomialRing(QQ,6,'z')
Expand Down Expand Up @@ -1921,6 +1933,21 @@ cdef class Matrix(sage.structure.element.Matrix):
[ 0.333333333333333 66.6666666666667]
[ -3.00000000000000 1.00000000000000e6]
Matrices with borders::
sage: M = matrix([[1,2,3], [4,5,6], [7,8,9]])
sage: M.subdivide(None, 2)
sage: print(M.str(unicode=True,
....: top_border=['ab', 'cde', 'f'],
....: bottom_border=['*', '', ''],
....: left_border=[1, 10, 100],
....: right_border=['', ' <', '']))
ab cde f
1⎛ 1 2│ 3⎞
10⎜ 4 5│ 6⎟ <
100⎝ 7 8│ 9⎠
*
TESTS:
Prior to :issue:`11544` this could take a full minute to run (2011). ::
Expand Down Expand Up @@ -1958,6 +1985,32 @@ cdef class Matrix(sage.structure.element.Matrix):
sage: matrix(R, [[2/3 - 10^6 * x^3 + 3 * y + O(x, y)^4]])
[2/3 + 3*y - 1000000*x^3 + O(x, y)^4]
sage: matrix.options._reset()
Edge cases of matrices with borders::
sage: print(matrix(ZZ, 0, 0).str(
....: top_border=[], bottom_border=[], left_border=[], right_border=[]))
[]
sage: print(matrix(ZZ, 0, 4).str(
....: unicode=True,
....: top_border='abcd', bottom_border=range(4)))
()
sage: print(matrix(ZZ, 1, 4).str(
....: unicode=True,
....: top_border='abcd', bottom_border=range(4)))
a b c d
(0 0 0 0)
0 1 2 3
sage: print(matrix(ZZ, 2, 4).str(
....: unicode=True,
....: top_border='abcd', bottom_border=range(4), left_border='uv'))
a b c d
u⎛0 0 0 0⎞
v⎝0 0 0 0⎠
0 1 2 3
sage: print(matrix(ZZ, 2, 0).str(
....: top_border='', left_border='uv', right_border=['*', '']))
[]
"""
cdef Py_ssize_t nr, nc, r, c
nr = self._nrows
Expand Down Expand Up @@ -2063,6 +2116,12 @@ cdef class Matrix(sage.structure.element.Matrix):

# compute column widths
S = []
if top_border is not None:
for x in top_border:
S.append(str(x))
top_count = 1
else:
top_count = 0
for x in entries:
# Override the usual representations with those specified
if callable(rep_mapping):
Expand All @@ -2075,39 +2134,83 @@ cdef class Matrix(sage.structure.element.Matrix):
else:
rep = repr(x)
S.append(rep)
if bottom_border is not None:
for x in bottom_border:
S.append(str(x))
bottom_count = 1
else:
bottom_count = 0

width = max(map(len, S))
left = []
rows = []
right = []

hline = cl.join(hl * ((width + 1)*(b - a) - 1)
for a,b in zip([0] + col_divs, col_divs + [nc]))
for a,b in zip([0] + col_divs, col_divs + [nc]))

# compute rows
for r from 0 <= r < nr:
rows += [hline] * row_divs.count(r)
for r in range(-top_count, nr + bottom_count):
if 0 <= r < nr:
n = row_divs.count(r)
if n:
left.extend([""] * n)
rows.extend([hline] * n)
right.extend([""] * n)
if left_border is not None and 0 <= r < nr:
left.append(str(left_border[r]))
else:
left.append("")
s = ""
for c from 0 <= c < nc:
if col_div_counts[c]:
sep = vl * col_div_counts[c]
if 0 <= r < nr:
sep = vl * col_div_counts[c]
else:
sep = " " * col_div_counts[c]
elif c == 0:
sep = ""
else:
sep = " "
entry = S[r * nc + c]
entry = S[(r + top_count) * nc + c]
entry = " " * (width - len(entry)) + entry
s = s + sep + entry
s = s + vl * col_div_counts[nc]
else:
if 0 <= r < nr:
s = s + vl * col_div_counts[nc]
else:
s = s + " " * col_div_counts[nc]
rows.append(s)
rows += [hline] * row_divs.count(nr)

last_row = len(rows) - 1
if last_row == 0:
rows[0] = slb + rows[0] + srb
if right_border is not None and 0 <= r < nr:
right.append(str(right_border[r]))
else:
right.append("")
else:
if nr == nr + bottom_count:
n = row_divs.count(nr)
if n:
left.extend([""] * n)
rows.extend([hline] * n)
right.extend([""] * n)

# left and right brackets
for i in range(top_count):
rows[i] = " "*len(slb) + rows[i] + " "*len(srb)
if len(rows) == top_count + 1 + bottom_count:
rows[top_count] = slb + rows[top_count] + srb
else:
rows[0] = tlb + rows[0] + trb
for r from 1 <= r < last_row:
rows[r] = mlb + rows[r] + mrb
rows[last_row] = blb + rows[last_row] + brb
rows[top_count] = tlb + rows[top_count] + trb
for i in range(top_count + 1, len(rows) - bottom_count - 1):
rows[i] = mlb + rows[i] + mrb
rows[-1 - bottom_count] = blb + rows[-1 - bottom_count] + brb
for i in range(bottom_count):
rows[-1 - i] = " "*len(slb) + rows[-1 - i] + " "*len(srb)

# left and right border
left_width = max(len(s) for s in left)
right_width = max(len(s) for s in right)
for i in range(len(rows)):
rows[i] = left[i].rjust(left_width) + rows[i] + right[i].rjust(right_width)

if character_art:
breakpoints = []
Expand Down
24 changes: 21 additions & 3 deletions src/sage/matrix/matrix_mod2_dense.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse


def str(self, rep_mapping=None, zero=None, plus_one=None, minus_one=None,
*, unicode=False, shape=None, character_art=False):
*, unicode=False, shape=None, character_art=False,
left_border=None, right_border=None,
top_border=None, bottom_border=None):
r"""
Return a nice string representation of the matrix.
Expand Down Expand Up @@ -384,6 +386,16 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse
:class:`~sage.typeset.unicode_art.UnicodeArt` which support line
breaking of wide matrices that exceed the window width
- ``left_border``, ``right_border`` -- sequence (default: ``None``);
if not ``None``, call :func:`str` on the elements and use the
results as labels for the rows of the matrix. The labels appear
outside of the parentheses.
- ``top_border``, ``bottom_border`` -- sequence (default: ``None``);
if not ``None``, call :func:`str` on the elements and use the
results as labels for the columns of the matrix. The labels appear
outside of the parentheses.
EXAMPLES::
sage: B = matrix(GF(2), 3, 3, [0, 1, 0, 0, 1, 1, 0, 0, 0])
Expand Down Expand Up @@ -416,13 +428,19 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse
# Set the mapping based on keyword arguments
# We ignore minus_one (it's only there for compatibility with Matrix)
if (rep_mapping is not None or zero is not None or plus_one is not None
or unicode or shape is not None or character_art):
or unicode or shape is not None or character_art
or left_border is not None or right_border is not None
or top_border is not None or bottom_border is not None):
# Shunt mappings off to the generic code since they might not be
# single characters
return matrix_dense.Matrix_dense.str(self, rep_mapping=rep_mapping,
zero=zero, plus_one=plus_one,
unicode=unicode, shape=shape,
character_art=character_art)
character_art=character_art,
left_border=left_border,
right_border=right_border,
top_border=top_border,
bottom_border=bottom_border)

if self._nrows == 0 or self._ncols == 0:
return "[]"
Expand Down

0 comments on commit 13a555d

Please sign in to comment.