Repository navigation
Create latex output for Jupyter notebooks #278
Description
Activity
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
\cdotto separate the10^xpart.Some points:
- MathJax doesn't support
@in\arraypreamble [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
reproutput (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.
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))
- MathJax doesn't support
one can't align columns on the decimal point.
How about
\hspace{-1em}? The MathJaxcolumnspacingis1emby 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#L1377import 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)
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
\phantomentries 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))
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
"real" LaTeX would have a set length
\arraycolsep. Maybe that could be included into the\hspaceoverriding the1emwith some clever LaTeX hack?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.
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.
Reacted by Ben GreinerOh, 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.
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.defaultsand 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 = [ ... ]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.
Reacted by Ben Greiner- linked a pull request that will close this issueAdded IPython LaTeX representation method for StateSpace objects #450
on Aug 16, 2020 - removed a link to a pull requestAdded IPython LaTeX representation method for StateSpace objects #450
on Aug 16, 2020 @bnavigator was that on purpose that you unlinked this issue to #450 ?
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`)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.I like the idea of requiring
display()to get mathjax. Then, as suggested, we could use theconfigmodule to set defaults for when display is automatically called.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
displayjust a way to force output of a variable, so IPython cellsa = ss(1,2,3,4); display(a)and
a = ss(1,2,3,4) aare 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 setlatex_maxsizeto -1 to disable the LaTeX display altogether. See https://ipython.readthedocs.io/en/stable/config/integrating.htmlReacted by Ben Greiner







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.