12
\$\begingroup\$

I frequently use MathJax, and often I need to write matrices. Since writing matrices using MathJax is very tiresome task, I decided to automate it.

Code:

import numpy as np import pyperclip class Conversion(): def __init__(self, matrix, n = None, m = None): if matrix == None: self.matrix = np.random.randint(-10,10, size = [n,m]) else: '''If variable "matrix", for example, contains [1,2,3,4], instead of [[1,2,3,4]], functions below will throw error, hence we need to make sure that the data type is correct.''' if type(matrix[0]) in (str,int,float): self.matrix = np.array([matrix,]) else: self.matrix = np.array(matrix) self.rows = len(self.matrix) self.cols = len(self.matrix[0]) def single(self, choosen_brackets = ')'): available_brackets = {' ' : 'matrix', ')' : 'pmatrix', ']' : 'bmatrix', '}' : 'Bmatrix', '|' : 'vmatrix', '||': 'Vmatrix' } choosen_brackets = available_brackets[choosen_brackets] body = '$$\\begin {} \n'.format('{' + choosen_brackets + '}') for row in self.matrix: row = [str(x) for x in row] body += ' & '.join(row) + '\\\\' + '\n' body +='\end {} $$'.format('{' + choosen_brackets + '}') print(body) pyperclip.copy(body) def augmented(self, choosen_brackets = '[]'): '''We are assuming that the last column of the given matrix is a vector.''' pos_of_the_verticar_bar = '{' + 'c'*(self.cols-1) + '|' + 'c' + '}' body = '$$\\left [ \\begin {array} %s \n' % (pos_of_the_verticar_bar) for row in self.matrix: row = [str(x) for x in row] body += ' & '.join(row) + '\\\\' + '\n' body +='\end {array} \\right ]$$' print(body) pyperclip.copy(body) 

Notes:

  1. Since function augmented is quite similar to single, I could've merged them into one. However, I think that keeping them separate makes the code a little bit more readable.

  2. On MSE, mathjax equations are enclosed with $$ instead of \$

  3. "single" is a bad name for a function, I admit. I haven't found any better options. Feel free to offer suggestions.

What can be improved?

Written for Python 3.6.5.

\$\endgroup\$
0

    2 Answers 2

    12
    \$\begingroup\$

    This looks like a handy tool to have around, maybe we can make it even better.

    Style

    As per the official Python Style Guide (often known by his nickname PEP8), keyword-arguments of functions should have no whitespace around the =.

    Matrix size

    numpy arrays have a property called .shape which describes their number of rows and number of cols (in case of a 2D array at least). So

    self.rows = len(self.matrix) self.cols = len(self.matrix[0]) 

    could become

    self.rows, self.cols = self.matrix.shape 

    Since your code only works for 2D matrices, it's maybe also a good idea to check for that. The .ndim attribute can be of help here.

    Type checking

    type(matrix[0]) in (str,int,float) should instead be isinstance(matrix[0], (str, int, float)).

    Checking None

    The official recommendation is to always use if sth is None: when checking for None.

    String joining

    row = [str(x) for x in row] body += ' & '.join(row) + '\\\\' + '\n' 

    could be done in a single line:

    body += ' & '.join(str(x) for x in row) + '\\\\' + '\n' 

    This means you also don't have to reassign row to be something different than it was before.

    String formatting

    Since you said you're using Python 3.6, maybe have a look at f-strings for even more concise string formatting syntax. No matter if you choose to do this, maybe at least get rid of the old-style % formatting in augmented(...) by escaping { and } using a leading \.

    Function output

    I find it preferable if you'd let the user decide what he wants to do with what your function returns. So instead of printing and copying the formatted matrix code, maybe just return body and let the caller decide how to proceed from here. You could even define a "convenience" function (to_clipboard) that basically only does pyperclip.copy(...), although this is likely not necessary. Another idea would be to make at least the copy part opt-in via a "flag" bool parameter.

    Class

    Maybe a class is not the right tool here. A possible alternative would be to move what's done in __init__ into a "private" (read name starts with _) helper function and get rid of the class.


    Missing backslash

    There seems to be a little bug in both of your functions (see how bad code duplication is ;-)): body +='\end ... is likely missing an additional \.

    Unused argument

    If I'm not mistaken, the choosen_brackets argument of augmented is not used in the function.

    Typo

    choosen_brackets should likely be chosen_brackets

    \$\endgroup\$
    2
    • \$\begingroup\$Thank you for your review! May I ask why it is recommended to use is None instead of == None?\$\endgroup\$CommentedOct 16, 2019 at 2:19
    • \$\begingroup\$Strictly speaking, is checks if both variables point to the exact same object in memory and not just that they compare equal. Since None is a singleton, this is likely the more natural operation to use. But to be honest, there are probably not a lot of (other) hard facts why you should use is None over == None. Both variants will give you the desired result here.\$\endgroup\$
      – AlexV
      CommentedOct 16, 2019 at 7:47
    9
    \$\begingroup\$

    Python's string formatting has come a long way from the "%s" formatting days. Nowadays classes can even determine on their own how to handle format specifiers. Therefore I would write a matrix class that can be pretty printed with different format options determining the matrix style.

    class MathJaxMatrix: brackets = {'': 'matrix', 'p': 'pmatrix', 'b': 'bmatrix', 'B': 'Bmatrix', 'v': 'vmatrix', 'V': 'Vmatrix'} e_msg = "unsupported format string passed to MathJaxMatrix.__format__" def __init__(self, m): if m.ndim == 1: m = m.reshape(len(m), 1) self.m = m self.rows, self.cols = m.shape def __str__(self): return "\\\\ \n".join(" & ".join(str(x) for x in row) for row in self.m) def __format__(self, format_spec=None): if format_spec is None: return str(self) if format_spec == "a": format_spec = '{' + 'c'*(self.cols-1) + '|c}' start = rf'$$\left[ \begin{{array}}{format_spec}' end = r'\end{array} \right]$$' else: try: brackets = self.brackets[format_spec] except KeyError as e: raise TypeError(self.e_msg) from e start = f'$$ \\begin{{{brackets}}}' end = f'\end{{{brackets}}} $$' return "\n".join([start, str(self), end]) 

    Which you can use like this:

    In [40]: x = np.random.rand(4, 5) In [41]: m = MathJaxMatrix(x) In [42]: print(m) 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 In [43]: print(f"{m}") $$\begin{matrix} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{matrix} $$ In [44]: print(f"{m:p}") $$\begin{pmatrix} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{pmatrix} $$ In [45]: print(f"{m:a}") $$ \left[ \begin{array}{cccc|c} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{array} \right] $$ In [62]: print(f"{m:e}") --------------------------------------------------------------------------- KeyError Traceback (most recent call last) ... KeyError: 'e' The above exception was the direct cause of the following exception: TypeError Traceback (most recent call last) ... TypeError: unsupported format string passed to MathJaxMatrix.__format__ 

    Note that this removes the repeated code for getting a string representation of the actual matrix, uses np.ndarray.ndim and np.reshape to ensure it is a proper 2D matrix. I used the first letter of the different *matrix options to distinguish them because } is not allowed in format specifications.

    The actual convenience function is then quite short:

    def format_matrix(m, format_spec): m = MathJaxMatrix(m) s = f"{m:{format_spec}}" print(s) pyperclip.copy(s) 
    \$\endgroup\$

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.