Skip to content

Commit fa77574

Browse files
author
Release Manager
committed
Trac #8598: Add graphical output to operation tables
Operation tables can be output as grids with color or grayscale squares representing the different elements of the algebraic structure. Adding these into {{{sage.matrix.operation_table.OperationTable}}} would be a nice self-contained project for someone looking for a project involving plotting and graphics. Despite the localized nature of the project, it would see wide applicability throughout Sage. Look for stubs in the source code, I'm pretty sure Mathematica does this for groups (Cayley table), but I can't get Wolfram Alpha or MathWorld to cough it up again for me now that I want it. URL: https://trac.sagemath.org/8598 Reported by: rbeezer Ticket author(s): Bruno Edwards Reviewer(s): Dima Pasechnik
2 parents 081d67a + da8a8f5 commit fa77574

File tree

1 file changed

+106
-37
lines changed

1 file changed

+106
-37
lines changed

src/sage/matrix/operation_table.py

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
# ****************************************************************************
1616

1717
from sage.structure.sage_object import SageObject
18+
from matplotlib.cm import gist_rainbow, Greys
19+
from sage.plot.matrix_plot import matrix_plot
20+
from sage.matrix.constructor import Matrix
21+
from sage.plot.text import text
22+
from copy import copy
1823

1924

2025
class OperationTable(SageObject):
@@ -374,15 +379,12 @@ class OperationTable(SageObject):
374379
k| k i g f d e l b h a j c
375380
l| l c j i g a h e k b d f
376381
377-
.. TODO::
378-
379-
Provide color and grayscale graphical representations of tables.
380-
See commented-out stubs in source code.
381-
382-
AUTHOR:
382+
AUTHORS:
383383
384384
- Rob Beezer (2010-03-15)
385+
- Bruno Edwards (2022-10-31)
385386
"""
387+
386388
def __init__(self, S, operation, names='letters', elements=None):
387389
r"""
388390
TESTS::
@@ -432,7 +434,7 @@ def __init__(self, S, operation, names='letters', elements=None):
432434
supported = {
433435
add: (add, '+', '+'),
434436
mul: (mul, '*', '\\ast')
435-
}
437+
}
436438
# default symbols for upper-left-hand-corner of table
437439
self._ascii_symbol = '.'
438440
self._latex_symbol = '\\cdot'
@@ -451,7 +453,7 @@ def __init__(self, S, operation, names='letters', elements=None):
451453
# the elements might not be hashable. But if they are it is much
452454
# faster to lookup in a hash table rather than in a list!
453455
try:
454-
get_row = {e: i for i,e in enumerate(self._elts)}.__getitem__
456+
get_row = {e: i for i, e in enumerate(self._elts)}.__getitem__
455457
except TypeError:
456458
get_row = self._elts.index
457459

@@ -461,7 +463,8 @@ def __init__(self, S, operation, names='letters', elements=None):
461463
try:
462464
result = self._operation(g, h)
463465
except Exception:
464-
raise TypeError('elements %s and %s of %s are incompatible with operation: %s' % (g,h,S,self._operation))
466+
raise TypeError('elements %s and %s of %s are incompatible with operation: %s' % (
467+
g, h, S, self._operation))
465468

466469
try:
467470
r = get_row(result)
@@ -477,7 +480,8 @@ def __init__(self, S, operation, names='letters', elements=None):
477480
except (KeyError, ValueError):
478481
failed = True
479482
if failed:
480-
raise ValueError('%s%s%s=%s, and so the set is not closed' % (g, self._ascii_symbol, h, result))
483+
raise ValueError('%s%s%s=%s, and so the set is not closed' % (
484+
g, self._ascii_symbol, h, result))
481485

482486
row.append(r)
483487
self._table.append(row)
@@ -558,7 +562,8 @@ def _name_maker(self, names):
558562
else:
559563
width = int(log(self._n - 1, base)) + 1
560564
for i in range(self._n):
561-
places = Integer(i).digits(base=base, digits=letters, padto=width)
565+
places = Integer(i).digits(
566+
base=base, digits=letters, padto=width)
562567
places.reverse()
563568
name_list.append(''.join(places))
564569
elif names == 'elements':
@@ -570,19 +575,22 @@ def _name_maker(self, names):
570575
name_list.append(estr)
571576
elif isinstance(names, list):
572577
if len(names) != self._n:
573-
raise ValueError('list of element names must be the same size as the set, %s != %s'%(len(names), self._n))
578+
raise ValueError('list of element names must be the same size as the set, %s != %s' % (
579+
len(names), self._n))
574580
width = 0
575581
for name in names:
576582
if not isinstance(name, str):
577-
raise ValueError('list of element names must only contain strings, not %s' % name)
583+
raise ValueError(
584+
'list of element names must only contain strings, not %s' % name)
578585
if len(name) > width:
579586
width = len(name)
580587
name_list.append(name)
581588
else:
582-
raise ValueError("element names must be a list, or one of the keywords: 'letters', 'digits', 'elements'")
589+
raise ValueError(
590+
"element names must be a list, or one of the keywords: 'letters', 'digits', 'elements'")
583591
name_dict = {}
584592
for i in range(self._n):
585-
name_dict[name_list[i]]=self._elts[i]
593+
name_dict[name_list[i]] = self._elts[i]
586594
return width, name_list, name_dict
587595

588596
def __getitem__(self, pair):
@@ -632,13 +640,15 @@ def __getitem__(self, pair):
632640
IndexError: invalid indices of operation table: ((1,512), (1,3,2,4)(5,7))
633641
"""
634642
if not (isinstance(pair, tuple) and len(pair) == 2):
635-
raise TypeError('indexing into an operation table requires exactly two elements')
643+
raise TypeError(
644+
'indexing into an operation table requires exactly two elements')
636645
g, h = pair
637646
try:
638647
row = self._elts.index(g)
639648
col = self._elts.index(h)
640649
except ValueError:
641-
raise IndexError('invalid indices of operation table: (%s, %s)' % (g, h))
650+
raise IndexError(
651+
'invalid indices of operation table: (%s, %s)' % (g, h))
642652
return self._elts[self._table[row][col]]
643653

644654
def __eq__(self, other):
@@ -747,8 +757,9 @@ def set_print_symbols(self, ascii, latex):
747757
...
748758
ValueError: ASCII symbol should be a single character, not 5
749759
"""
750-
if not isinstance(ascii, str) or not len(ascii)==1:
751-
raise ValueError('ASCII symbol should be a single character, not %s' % ascii)
760+
if not isinstance(ascii, str) or not len(ascii) == 1:
761+
raise ValueError(
762+
'ASCII symbol should be a single character, not %s' % ascii)
752763
if not isinstance(latex, str):
753764
raise ValueError('LaTeX symbol must be a string, not %s' % latex)
754765
self._ascii_symbol = ascii
@@ -935,22 +946,79 @@ def matrix_of_variables(self):
935946
from sage.rings.rational_field import QQ
936947
R = PolynomialRing(QQ, 'x', self._n)
937948
MS = MatrixSpace(R, self._n, self._n)
938-
entries = [R('x'+str(self._table[i][j])) for i in range(self._n) for j in range(self._n)]
939-
return MS( entries )
940-
941-
#def color_table():
942-
#r"""
943-
#Returns a graphic image as a square grid where entries are color coded.
944-
#"""
945-
#pass
946-
#return None
947-
948-
#def gray_table():
949-
#r"""
950-
#Returns a graphic image as a square grid where entries are coded as grayscale values.
951-
#"""
952-
#pass
953-
#return None
949+
entries = [R('x'+str(self._table[i][j]))
950+
for i in range(self._n) for j in range(self._n)]
951+
return MS(entries)
952+
953+
# documentation hack
954+
# makes the cmap default argument look nice in the docs
955+
# by copying the gist_rainbow object and overriding __repr__
956+
gist_rainbow_copy=copy(gist_rainbow)
957+
class ReprOverrideLinearSegmentedColormap(gist_rainbow_copy.__class__):
958+
def __repr__(self):
959+
return "gist_rainbow"
960+
gist_rainbow_copy.__class__=ReprOverrideLinearSegmentedColormap
961+
962+
963+
def color_table(self, element_names=True, cmap=gist_rainbow_copy, **options):
964+
r"""
965+
Returns a graphic image as a square grid where entries are color coded.
966+
967+
INPUT:
968+
969+
- ``element_names`` - (default : ``True``) Whether to display text with element names on the image
970+
971+
- ``cmap`` - (default : ``gist_rainbow``) colour map for plot, see matplotlib.cm
972+
973+
- ``**options`` - passed on to matrix_plot call
974+
975+
EXAMPLES::
976+
977+
sage: from sage.matrix.operation_table import OperationTable
978+
sage: OTa = OperationTable(SymmetricGroup(3), operation=operator.mul)
979+
sage: OTa.color_table()
980+
Graphics object consisting of 37 graphics primitives
981+
982+
.. PLOT::
983+
984+
from sage.matrix.operation_table import OperationTable
985+
OTa = OperationTable(SymmetricGroup(3), operation=operator.mul)
986+
sphinx_plot(OTa.color_table(), figsize=(3.0,3.0))
987+
"""
988+
989+
# Base matrix plot object, without text
990+
plot = matrix_plot(Matrix(self._table), cmap=cmap,
991+
frame=False, **options)
992+
993+
if element_names:
994+
995+
# adapted from ._ascii_table()
996+
# prepare widenames[] list for labelling on image
997+
n = self._n
998+
width = self._width
999+
1000+
widenames = []
1001+
for name in self._names:
1002+
widenames.append("{0: >{1}s}".format(name, width))
1003+
1004+
# iterate through each element
1005+
for g in range(n):
1006+
for h in range(n):
1007+
1008+
# add text to the plot
1009+
tPos = (g, h)
1010+
tText = widenames[self._table[g][h]]
1011+
t = text(tText, tPos, rgbcolor=(0, 0, 0))
1012+
plot = plot + t
1013+
1014+
# https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html
1015+
import warnings
1016+
warnings.filterwarnings("ignore", message="The value of the smallest subnormal for")
1017+
1018+
return plot
1019+
1020+
def gray_table(self, **options):
1021+
return self.color_table(cmap=Greys, **options)
9541022

9551023
def _ascii_table(self):
9561024
r"""
@@ -1031,7 +1099,7 @@ def _ascii_table(self):
10311099
widenames.append('{0: >{1}s}'.format(name, width))
10321100

10331101
# Headers
1034-
table = ['{0: >{1}s} '.format(self._ascii_symbol,width)]
1102+
table = ['{0: >{1}s} '.format(self._ascii_symbol, width)]
10351103
table += [' '+widenames[i] for i in range(n)]+['\n']
10361104
table += [' ']*width + ['+'] + ['-']*(n*(width+1))+['\n']
10371105

@@ -1068,7 +1136,8 @@ def _latex_(self):
10681136

10691137
# Row label and body of table
10701138
for g in range(n):
1071-
table.append('{}') # Interrupts newline and [], so not line spacing
1139+
# Interrupts newline and [], so not line spacing
1140+
table.append('{}')
10721141
table.append(names[g])
10731142
for h in range(n):
10741143
table.append('&'+names[self._table[g][h]])

0 commit comments

Comments
 (0)