Skip to content

Create latex output for Jupyter notebooks #278

Description

@murrayrm

PR #276 adds latex output for transfer functions. It would be nice to add this for other LTI types as well (particularly StateSpace objects). Having the output match the text approximation in __repr__ seems like a good first cut at format to use.

Activity

  1. repagh commented on Jun 21, 2019

    @repagh
    Member

    I got this representation in a Spyder console
    image
    I think we need to get the numbers printed in exponent form a bit nicer still.

  2. roryyorke commented on Aug 2, 2020

    @roryyorke
    Contributor

    Here's a proof of concept for SS, presenting the system in "partitioned matrix" form.

    The format is loosely based on Sympy's LaTeX output for matrices with floating point entries, which centre-aligns and uses a \cdot to separate the 10^x part.

    Some points:

    • MathJax doesn't support @ in \array preamble [1], so one can't align columns on the decimal point. One could left-align, and use \llap{-} for the minus signs; then the leading digits are aligned in each column. MathJax limitations only apply for notebook output.
    • If one uses a "tabular" output like this, perhaps _repr_html_ would be better than _repr_latex_; I haven't investigated that. That might be better if one, e.g., labelled rows with state and output names, and columns with state and input names.
    • this doesn't match the current repr output (A = [[ ... ]] B = [[ ... ]], etc.)
    • a complete display doesn't make sense for big (nx, ny, nu > 10?) systems; we could have something like numpy's or panda's display limits. I think something similar applies to TFs, i.e., we should omit intermediate polynomial terms for high-degree denominators and numerators. One trick would be still showing the partitions, so this could get pretty untidy-looking.

    [1] mathjax/MathJax-docs#205

    import numpy as np
    from IPython.display import Latex
    
    def _f2s(f):
        s = f'{f:.3g}'
        if 'e' in s:
            n, e = s.split('e')
            e = int(e)
            s = f'{n}\\cdot10^{{{e}}}'
        return s
    
    
    def format_ss(A, B, C, D):
        nx = A.shape[0]
        nu = B.shape[1]
        ny = C.shape[0]
    
        lines = [
            r'\[',
            r'\left(',
            r'\begin{array}{' + 'c' * nx + '|' + 'c' * nu + '}'
            ]
    
        for Ai, Bi in zip(A, B):
            lines.append('&'.join([_f2s(Aij) for Aij in Ai]
                                  + [_f2s(Bij) for Bij in Bi])
                         + '\\\\')
        lines.append(r'\hline')
        for Ci, Di in zip(C, D):
            lines.append('&'.join([_f2s(Cij) for Cij in Ci]
                                  + [_f2s(Dij) for Dij in Di])
                         + '\\\\')
        
        lines.extend([
            r'\end{array}'
            r'\right)',
            r'\]'])
    
        return '\n'.join(lines)
    
    nx = np.random.randint(low=1,high=5)
    nu = np.random.randint(low=1,high=5)
    ny = np.random.randint(low=1,high=5)
    A = 10.0 ** np.random.randint(low=-20, high=20, size=(nx,nx)) * np.random.randn(nx,nx)
    B = 10.0 ** np.random.randint(low=-20, high=20, size=(nx,nu)) * np.random.randn(nx,nu)
    C = 10.0 ** np.random.randint(low=-20, high=20, size=(ny,nx)) * np.random.randn(ny,nx)
    D = 10.0 ** np.random.randint(low=-20, high=20, size=(ny,nu)) * np.random.randn(ny,nu)
    Latex(format_ss(A,B,C,D))

    ss-example

  3. bnavigator commented on Aug 2, 2020

    @bnavigator
    Contributor

    one can't align columns on the decimal point.

    How about \hspace{-1em}? The MathJax columnspacing is 1em by default and I could not find any place, that overrides the default:
    \begin{array} -> https://github.com/mathjax/MathJax-src/blob/master/ts/input/tex/base/BaseMappings.ts#L659
    -> https://github.com/mathjax/MathJax-src/blob/master/ts/input/tex/base/BaseMethods.ts#L1433
    -> https://github.com/mathjax/MathJax-src/blob/master/ts/input/tex/base/BaseMethods.ts#L1377

    import numpy as np
    from IPython.display import Latex
    
    def _f2s(f):
        s = '{:-.3g}'.format(f)
        ne = s.split('e')
        n = ne[0].split('.')
        s = n[0]
        s += r'.&\hspace{{-1em}}{:0<2s}'.format(n[1]) if n[1:] else r'\phantom{.}&'
        if ne[1:]:
            if not n[1:]:
                s += r'\hspace{-1em}\phantom{00}'
            s += r'\cdot10^{{{:d}}}'.format(int(ne[1]))
        return s
    
    def format_ss(A, B, C, D):
        nx = A.shape[0]
        nu = B.shape[1]
        ny = C.shape[0]
    
        lines = [
            r'\[',
            r'\left(',
            r'\begin{array}{' + 'rl' * nx + '|' + 'rl' * nu + '}'
            ]
    
        for Ai, Bi in zip(A, B):
            lines.append('&'.join([_f2s(Aij) for Aij in Ai]
                                  + [_f2s(Bij) for Bij in Bi])
                         + '\\\\')
        lines.append(r'\hline')
        for Ci, Di in zip(C, D):
            lines.append('&'.join([_f2s(Cij) for Cij in Ci]
                                  + [_f2s(Dij) for Dij in Di])
                         + '\\\\')
        
        lines.extend([
            r'\end{array}'
            r'\right)',
            r'\]'])
    
        return '\n'.join(lines)
    
    #nx = np.random.randint(low=1,high=5)
    #nu = np.random.randint(low=1,high=5)
    #ny = np.random.randint(low=1,high=5)
    nx = 3
    nu = 2
    ny = 2
    A = 10.0 ** np.random.randint(low=-20, high=20, size=(nx,nx)) * np.random.randn(nx,nx)
    B = 10.0 ** np.random.randint(low=-20, high=20, size=(nx,nu)) * np.random.randn(nx,nu)
    C = 10.0 ** np.random.randint(low=-20, high=20, size=(ny,nx)) * np.random.randn(ny,nx)
    D = 10.0 ** np.random.randint(low=-20, high=20, size=(ny,nu)) * np.random.randn(ny,nu)
    B[1,1] = 3e4
    B[2,1] = -9e4
    C[0,1] = 123
    lstr = format_ss(A,B,C,D)
    #print(lstr)
    Latex(lstr)

    Jupyter Notebook:
    image

    Jupyter console:
    image

    One could argue how much space to leave after the decimal separator and if the phantom ".00" for integer mantissae when there is an exponent part should be visible. And whether the \phantom entries or the whole column will need no be removed for systems where the whole column for the fractional part is empty:

    A2 = np.full((5,5),-99)
    B2 = np.full((5,1),2)
    C2 = np.full((1,5),3)
    D2 = [[99]]
    Latex(format_ss(A2, B2, C2, D2))

    image

  4. roryyorke commented on Aug 4, 2020

    @roryyorke
    Contributor

    How about \hspace{-1em}?

    Ha! Ingenious. This does tie us to MathJax, but we could add features for "real" LaTeX (for exporting notebooks to LaTeX) if anyone ever asks for it.

    As for the extra spacing introduced by the extra column: it would be ideal to remove it, but that is a finesse.

    Mathematica uses a similar output format for its state-space objects [1].

    @murrayrm, do you think this is worth pursuing?

    [1] https://reference.wolfram.com/language/ref/StateSpaceModel.html

  5. bnavigator commented on Aug 4, 2020

    @bnavigator
    Contributor

    "real" LaTeX would have a set length \arraycolsep. Maybe that could be included into the \hspace overriding the 1em with some clever LaTeX hack?

  6. murrayrm commented on Aug 5, 2020

    @murrayrm
    MemberAuthor

    Nice to have something in place. I would probably got for something that is as "plain latex" (or MathJax) as possible. It won't look as pretty, but it can be updated later as more functionality is added to the formatting packages.

    Another possibility would be to allow the user to pass a formatting string for the table style, item entries, etc with some sort of reasonable default value.

  7. roryyorke commented on Aug 8, 2020

    @roryyorke
    Contributor

    WIP rendered https://nbviewer.jupyter.org/gist/roryyorke/6879e0b20735f533410dad883eb687e5 from code at roryyorke/python-control/tree/rory/ss-repr-latex

    I've taken @bnavigator 's idea a little further by adding a third set of columns for the exponents; this makes easy to scan down the column for orders of magnitude, and it also Does The Right Thing when there are no exponents to display.

    One special case is a gain-only system, which shows the partitions even though the A/B/C entries are empty. This makes it clear this is a "D-only" system, but it doesn't look that good. I think I'll change this.

    It's probably not too diffiicult to make the formatting somewhat customizable, though we would need to be able to parse the resulting string to insert the column separators and so on. I'm not familiar with the python-control control config system (I think there is one?), will take a look.

  8. roryyorke commented on Aug 8, 2020

    @roryyorke
    Contributor

    Oh, one thing I'm not sure about is how to write tests for this. I don't see a lot tests for this sort of thing in, e.g., sympy.

  9. bnavigator commented on Aug 8, 2020

    @bnavigator
    Contributor

    It's probably not too diffiicult to make the formatting somewhat customizable, though we would need to be able to parse the resulting string to insert the column separators and so on. I'm not familiar with the python-control control config system (I think there is one?), will take a look.

    It's called config.defaults and IMHO should be renamed anyway.

    I would like to have the option to display the matrices (possibly still arranged in a table) without the partitioned matrix like this.

        [ ... ... ]      [ ... ]
    A = [ ... ... ]  B = [ ... ]
        [ ... ... ]      [ ... ]
    
    C = [ ... ... ]  D = [ ... ]
    
  10. roryyorke commented on Aug 9, 2020

    @roryyorke
    Contributor

    Branch and gist updated. User can select from 'partitioned' and 'separate' types, and specify format string.

    Static-gain ("stateless") systems are special-cased.

    How should configuration options be documented?

    I think this should have some minimal testing, though I'll probably use a "gold image" approach, i.e., generate reference strings using the implementation.

    image

    image

  11. added this to the 0.9.x milestone on Dec 26, 2020
  12. sawyerbfuller commented on Feb 12, 2021

    @sawyerbfuller
    Contributor

    @bnavigator was that on purpose that you unlinked this issue to #450 ?

  13. bnavigator commented on Feb 12, 2021

    @bnavigator
    Contributor

    Yes. #450 did not close this. It added the functionality for StateSpace, but not for other system objects. (TransferFunction._repr_latex_ already exists, FRD and iosys don't have it yet`)

  14. danielbrown2 commented on Apr 1, 2021

    @danielbrown2

    Having Jupyter display the Mathjax is useful for small state spaces, but I've had some problems with larger ones causing Jupyter Notebook to hang. I then have to forcibly stop my browser and then reopening, hoping that I can clear all outputs before it starts rendering.

    Using display(ss) to show the Mathjax version might be more appropriate, or maybe have some size limit where it falls back to just showing a string, or a flag to switch this feature off. I had this issue when I just want to check what a variable in a notebook is quickly or run some function that returns a statespace, then I end up printing a 100x100 StateSpace I'm working with and crashing everything.

  15. murrayrm commented on Apr 1, 2021

    @murrayrm
    MemberAuthor

    I like the idea of requiring display() to get mathjax. Then, as suggested, we could use the config module to set defaults for when display is automatically called.

  16. roryyorke commented on Apr 11, 2021

    @roryyorke
    Contributor

    Having Jupyter display the Mathjax is useful for small state spaces, but I've had some problems with larger ones causing Jupyter Notebook to hang. I then have to forcibly stop my browser and then reopening, hoping that I can clear all outputs before it starts rendering.

    A short-term workaround is to remove the related method after importing python-control:

    import control as ct
    del ct.statesp.StateSpace._repr_latex_
    

    Using display(ss) to show the Mathjax version might be more appropriate,

    My understanding is that display just a way to force output of a variable, so IPython cells

    a = ss(1,2,3,4);
    display(a)
    

    and

    a = ss(1,2,3,4)
    a
    

    are equivalent.

    A switch back to "plain old repr" for bigger systems seems like a reasonable idea. We could add a config.defaults['statesp.latex_maxsize'] and if either (nstates + ninputs) or (nstate + noutputs) > latex_maxsize, have _repr_latex_ return None. One could then set latex_maxsize to -1 to disable the LaTeX display altogether. See https://ipython.readthedocs.io/en/stable/config/integrating.html

  17. removed this from the 0.9.1 milestone on Dec 31, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions