Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added IPython LaTeX representation method for StateSpace objects #450

Merged
merged 1 commit into from Dec 31, 2020

Conversation

roryyorke
Copy link
Contributor

Added StateSpace method _repr_latex_, which returns a
MathJax-centric LaTeX representation of the instance.

Added two statesp configuration options for this representation.
One affects number formatting, and the other chooses the output type.

Partially addesses #278; see much discussion there on this.

Example at https://nbviewer.jupyter.org/gist/roryyorke/6879e0b20735f533410dad883eb687e5.

@coveralls
Copy link

coveralls commented Aug 15, 2020

Coverage Status

Coverage increased (+0.1%) to 87.083% when pulling 91b04de on roryyorke:rory/ss-repr-latex into bbb2e36 on python-control:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 84.361% when pulling 96cd45a on roryyorke:rory/ss-repr-latex into d3142ff on python-control:master.

control/statesp.py Outdated Show resolved Hide resolved
control/statesp.py Outdated Show resolved Hide resolved
[],
[[1.2345,-2e-200],[-1,0]])

g1_p3_p = '\\[\n\\left(\n\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\hline\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\]'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe reformat as r'' strings for better readability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were generated. Can one coax repr (or whatever) to spit out raw strings?

@@ -561,6 +561,69 @@ def test_str(self):
assert str(sysdt1) == tref + "\ndt = 1.0\n"


def test_latex_repr(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the spirit of #438:

There are 4 identical tests with parameter variations. I suggest you use something like

    @pytest.mark.parametrize(
        "g, repr_latex, p3ref, p5ref",
        [(g1, None,          g1_p3_p, g1_p5_p),
         (g1, "partitioned", g1_p3_p, g1_p5_p),
         (g1, "separate",    g1_p3_s, g1_p5_s),
         (g2, "partitioned", g2_p3_p, g2_p5_p),
         (g2, "separate",    g2_p3_s, g2_p5_s)])
    def test_latex_repr(self, g, repr_latex, p3ref, p5ref):
        try:
            # add "editsdefaults" fixture and remove the reset_defaults as soon as #438 is merged
            reset_defaults()
            if repr_latex is not None:
                set_defaults('statesp', latex_repr_type=repr_latex)
            assert g1._repr_latex_() == p3ref
            set_defaults('statesp', latex_num_format='.5g')
            assert g1._repr_latex_() == p5ref
        finally:
            reset_defaults()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, looks good. Would this need to wait for #438, or do we already have pytest as a dependency?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the editsdefaults part, which is introduced by that PR, you should be able to do it right away. Pytest is already used and required on other tests.

See #437

@bnavigator
Copy link
Contributor

Example at https://nbviewer.jupyter.org/gist/roryyorke/6879e0b20735f533410dad883eb687e5.

Github also renders ipynb. You could add a demonstrating example to the docs.

@roryyorke
Copy link
Contributor Author

Github also renders ipynb. You could add a demonstrating example to the docs.

AFAIK Github doesn't render the LaTeX; the notebook is actually a gist at https://gist.github.com/roryyorke/6879e0b20735f533410dad883eb687e5, where you can see the lack of LaTeX rendering. I gather there are workarounds, but I haven't investigated.

Thanks for the review!

@bnavigator
Copy link
Contributor

I wonder what's different in sympy:
https://github.com/sympy/sympy/blob/master/examples/notebooks/Sylvester_resultant.ipynb

The output fields have latex, png and text

{
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAPwAAAAcBAMAAAC3wc/WAAAAMFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv3aB7AAAAD3RSTlMAMkS7zRCZdiKJ71Rmq90icBAQAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADWUlEQVRIDb1XTWgTQRh9+c82aRrEe+NJFKSBokdbMTe1nUOhKNQGqxFRaC5trSBdFIqC2IDY2oM0aAXpQYOiCD2YkwepoviP4B+19VRrMVSpGGd2djYzyaaki3Sgme9733vvy+zMTihgjdEdxIodBlrPEYdKaHp9q1Ot0F3BSxHWPG/gzIju+lWzpgrxGxr1KiUbOM0wV5xXIrHgkg1nTdAXjMRqF2xjVK+146FC7dJqzIeWWzVGCdfyND5q5RHjaVipo2B+LaqvQLC05NEyqbfPdbwM4un4YNQWB7SkWgiOX8qpCM+CqWZq0S8LwmVS1BWLGTttR8zfaodT7FEZPozQnzLISLug0YM2AtRZ324LNqlM79WTChBK83QHPHGlYCXujDtqJSwYyuGvDAiLV8AUEMmjkZhl157p7TITqFdTmFp39fdz4/RdYce1W2O+n7KLaH8L+BiFP47Hokof9aKI+VylvSer0qTsdrEoZUaoPnzRvp2w9oEsDlNS5+kzg+lyHV39TH8UvoG3m8d4zdQ27Bo7VEGWaGrtYMbGglG+E7gLaAZ8mfB8pFVVscxD6EW0H0/Ts7xmtu/eKx0YoZJoAjLm83PExoKW3L/pjfcD++jhJ1ohpCsqkezEZUyQBzwV7Qvw3xAEMUs0AfFZu2ZjQUuRZ/SlX8IcXT28cUvja1ugYzbKgV6dYLcRaolE22QiQUUNnxCuuJ4FjXFViykiaiULSkrRv+AKWz11TLPP8kF/vSZiwLLAzdXTfQrbvMwWTdDZfA5oyVVaAIEkrdLVG+0bzcUyRWlMAr2EbZA5zPb1WZvVSzRBZ3ORsPYVFsB9BHXQn9gXQIgMIUBkFY+TwCzuBRbRx3OzvYvufZYjpU+JVgKB6/SE61LNtIAriYDOTv4poCnXgguyyIyHEb7pXvEsBvMcENp3OJBTbxPINNlpBqFluSYsLo6PfgDCWTQRdA10njA7yFIEj70mvjd9/TMmKrRa6jmwoDwumSZ7uFM9MbkmLNqLRXoZ01uvLibTV42F1iBVeVNXNaD7nJYJ3jT8GRlYNfbpUlmT4tpDxQLd9GBnaxfLzE45cRg/obr3zrR5ZzJF9ZlmHQqynkmY7XsouZ4t5V5njeSODK1n7Pg/ov/2Jf8B6oTd7JsJTuYAAAAASUVORK5CYII=\n",
      "text/latex": [
       "$$\\left ( x^{2} - 5 x + 6, \\quad x^{2} - 3 x + 2\\right )$$"
      ],
      "text/plain": [
       "⎛ 2             2          ⎞\n",
       "⎝x  - 5⋅x + 6, x  - 3⋅x + 2⎠"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "f, g"
   ]
  },

@roryyorke
Copy link
Contributor Author

It looks like sympy's init_printing arranges for the relevant magic; see extract from its docstring below.

I'm not up to implementing this; if we do want it, I think we should see if we can use Sympy to achieve it.

    use_latex : string, boolean, or None, default=None
        If True, use default LaTeX rendering in GUI interfaces (png and
        mathjax);
        if False, do not use LaTeX rendering;
        if None, make a guess based on the environment;
        if 'png', enable latex rendering with an external latex compiler,
        falling back to matplotlib if external compilation fails;
        if 'matplotlib', enable LaTeX rendering with matplotlib;
        if 'mathjax', enable LaTeX text generation, for example MathJax
        rendering in IPython notebook or text rendering in LaTeX documents;
        if 'svg', enable LaTeX rendering with an external latex compiler,
        no fallback

@bnavigator
Copy link
Contributor

Not within the scope of this PR, I guess. I was just curious.

@bnavigator bnavigator added this to the 0.9.0 milestone Aug 16, 2020
@bnavigator bnavigator linked an issue Aug 16, 2020 that may be closed by this pull request
@roryyorke
Copy link
Contributor Author

roryyorke commented Aug 19, 2020 via email

@bnavigator
Copy link
Contributor

bnavigator commented Aug 19, 2020

psf/black#1132 won't help here. The reference strings do not contain enough whitespace to wrap before the line length limit.

There is always pprint, but even the results from that are too long:

>>> from pprint import pprint
>>> import numpy as np
>>> from control import StateSpace
>>> LTX_G1 = StateSpace([[np.pi, 1e100],
...                      [-1.23456789, 5e-23]],
...                     [[0], [1]],
...                     [[987654321, 0.001234]],
...                     [[5]])
>>> pprint(LTX_G1._repr_latex_())
('\\[\n'
 '\\left(\n'
 '\\begin{array}{rllrll|rll}\n'
 '3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n'
 '-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n'
 '\\hline\n'
 '9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n'
 '\\end{array}\\right)\n'
 '\\]')

Here is a manual implementation with breaking at the column separators and using rstrings. But is it really worth the complexity?

>>> print("(r'{}')".format("''\\n'\n r'".join(["&'\n r'".join(l.split("&"))
...                                            for l in LTX_G1._repr_latex_()
...                                                           .splitlines()])))
(r'\[''\n'
 r'\left(''\n'
 r'\begin{array}{rllrll|rll}''\n'
 r'3.&'
 r'\hspace{-1em}14&'
 r'\hspace{-1em}\phantom{\cdot}&'
 r'1\phantom{.}&'
 r'\hspace{-1em}&'
 r'\hspace{-1em}\cdot10^{100}&'
 r'0\phantom{.}&'
 r'\hspace{-1em}&'
 r'\hspace{-1em}\phantom{\cdot}\\''\n'
 r'-1.&'
 r'\hspace{-1em}23&'
 r'\hspace{-1em}\phantom{\cdot}&'
 r'5\phantom{.}&'
 r'\hspace{-1em}&'
 r'\hspace{-1em}\cdot10^{-23}&'
 r'1\phantom{.}&'
 r'\hspace{-1em}&'
 r'\hspace{-1em}\phantom{\cdot}\\''\n'
 r'\hline''\n'
 r'9.&'
 r'\hspace{-1em}88&'
 r'\hspace{-1em}\cdot10^{8}&'
 r'0.&'
 r'\hspace{-1em}00123&'
 r'\hspace{-1em}\phantom{\cdot}&'
 r'5\phantom{.}&'
 r'\hspace{-1em}&'
 r'\hspace{-1em}\phantom{\cdot}\\''\n'
 r'\end{array}\right)''\n'
 r'\]')

You could also avoid the manual break at '&' and put some strategically placed spaces into _repr_latex_ itself.

@roryyorke
Copy link
Contributor Author

The results look fine in QtConsole (tested v4.7.6 from conda-forge).

Comment on lines 719 to 742
def test_latex_repr(g, ref, repr_type, num_format):
"""Test `._latex_repr_` with different config values

This is a 'gold image' test, so if you change behaviour,
you'll need to regenerate the reference results.
Try something like:
control.reset_defaults()
print(f'p3_p : {g1._repr_latex_()!r}')
"""
from control import set_defaults, reset_defaults
try:
# add "editsdefaults" fixture and remove the reset_defaults as soon as
# gh-438 is merged
reset_defaults()
if num_format is not None:
set_defaults('statesp', latex_num_format=num_format)

if repr_type is not None:
set_defaults('statesp', latex_repr_type=repr_type)

refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
assert g._repr_latex_() == ref[refkey]
finally:
reset_defaults()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #456 introducing the editsdefaults fixture, you can now use it directly:

Suggested change
def test_latex_repr(g, ref, repr_type, num_format):
"""Test `._latex_repr_` with different config values
This is a 'gold image' test, so if you change behaviour,
you'll need to regenerate the reference results.
Try something like:
control.reset_defaults()
print(f'p3_p : {g1._repr_latex_()!r}')
"""
from control import set_defaults, reset_defaults
try:
# add "editsdefaults" fixture and remove the reset_defaults as soon as
# gh-438 is merged
reset_defaults()
if num_format is not None:
set_defaults('statesp', latex_num_format=num_format)
if repr_type is not None:
set_defaults('statesp', latex_repr_type=repr_type)
refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
assert g._repr_latex_() == ref[refkey]
finally:
reset_defaults()
def test_latex_repr(g, ref, repr_type, num_format, editsdefaults):
"""Test `._latex_repr_` with different config values
This is a 'gold image' test, so if you change behaviour,
you'll need to regenerate the reference results.
Try something like:
control.reset_defaults()
print(f'p3_p : {g1._repr_latex_()!r}')
"""
from control import set_defaults
if num_format is not None:
set_defaults('statesp', latex_num_format=num_format)
if repr_type is not None:
set_defaults('statesp', latex_repr_type=repr_type)
refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
assert g._repr_latex_() == ref[refkey]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that is tidier. Have merged from master and changed to use the new fixture.

@murrayrm
Copy link
Member

@roryyorke When you get a chance, can you rebase this against the current master.

Added StateSpace method `_repr_latex_`, which returns a
MathJax-centric LaTeX representation of the instance.

Added two `statesp` configuration options for this representation.
One affects number formatting, and the other chooses the output type.
@roryyorke
Copy link
Contributor Author

Hm, this is not quite right: I've restored a file that was removed. Will try again...

@roryyorke
Copy link
Contributor Author

No, I take that back. I thought control/tests/statesp_test.py had been removed, and that I had re-added it, but I see it was still there. I think a test has gone missing (test_sample_system_prewarping), experimenting with git bisect to find where it went missing.

@roryyorke
Copy link
Contributor Author

OK, I see in c432fd5 the two prewarp tests for xferfcn and ss were merged into one parameterized test.

Sorry for all the noise. I think this PR is rebased and ready.

@murrayrm murrayrm merged commit 6ed3f74 into python-control:master Dec 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants