Skip to content

Commit 6ed3f74

Browse files
authored
Merge pull request #450 from roryyorke/rory/ss-repr-latex
Added IPython LaTeX representation method for StateSpace objects
2 parents a382502 + 91b04de commit 6ed3f74

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

control/statesp.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
'statesp.use_numpy_matrix': False, # False is default in 0.9.0 and above
7575
'statesp.default_dt': None,
7676
'statesp.remove_useless_states': True,
77+
'statesp.latex_num_format': '.3g',
78+
'statesp.latex_repr_type': 'partitioned',
7779
}
7880

7981

@@ -128,6 +130,33 @@ def _ssmatrix(data, axis=1):
128130
return arr.reshape(shape)
129131

130132

133+
def _f2s(f):
134+
"""Format floating point number f for StateSpace._repr_latex_.
135+
136+
Numbers are converted to strings with statesp.latex_num_format.
137+
138+
Inserts column separators, etc., as needed.
139+
"""
140+
fmt = "{:" + config.defaults['statesp.latex_num_format'] + "}"
141+
sraw = fmt.format(f)
142+
# significand-exponent
143+
se = sraw.lower().split('e')
144+
# whole-fraction
145+
wf = se[0].split('.')
146+
s = wf[0]
147+
if wf[1:]:
148+
s += r'.&\hspace{{-1em}}{frac}'.format(frac=wf[1])
149+
else:
150+
s += r'\phantom{.}&\hspace{-1em}'
151+
152+
if se[1:]:
153+
s += r'&\hspace{{-1em}}\cdot10^{{{:d}}}'.format(int(se[1]))
154+
else:
155+
s += r'&\hspace{-1em}\phantom{\cdot}'
156+
157+
return s
158+
159+
131160
class StateSpace(LTI):
132161
"""StateSpace(A, B, C, D[, dt])
133162
@@ -158,6 +187,24 @@ class StateSpace(LTI):
158187
time. The default value of 'dt' is None and can be changed by changing the
159188
value of ``control.config.defaults['statesp.default_dt']``.
160189
190+
StateSpace instances have support for IPython LaTeX output,
191+
intended for pretty-printing in Jupyter notebooks. The LaTeX
192+
output can be configured using
193+
`control.config.defaults['statesp.latex_num_format']` and
194+
`control.config.defaults['statesp.latex_repr_type']`. The LaTeX output is
195+
tailored for MathJax, as used in Jupyter, and may look odd when
196+
typeset by non-MathJax LaTeX systems.
197+
198+
`control.config.defaults['statesp.latex_num_format']` is a format string
199+
fragment, specifically the part of the format string after `'{:'`
200+
used to convert floating-point numbers to strings. By default it
201+
is `'.3g'`.
202+
203+
`control.config.defaults['statesp.latex_repr_type']` must either be
204+
`'partitioned'` or `'separate'`. If `'partitioned'`, the A, B, C, D
205+
matrices are shown as a single, partitioned matrix; if
206+
`'separate'`, the matrices are shown separately.
207+
161208
"""
162209

163210
# Allow ndarray * StateSpace to give StateSpace._rmul_() priority
@@ -306,6 +353,136 @@ def __repr__(self):
306353
C=asarray(self.C).__repr__(), D=asarray(self.D).__repr__(),
307354
dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')
308355

356+
def _latex_partitioned_stateless(self):
357+
"""`Partitioned` matrix LaTeX representation for stateless systems
358+
359+
Model is presented as a matrix, D. No partition lines are shown.
360+
361+
Returns
362+
-------
363+
s : string with LaTeX representation of model
364+
"""
365+
lines = [
366+
r'\[',
367+
r'\left(',
368+
(r'\begin{array}'
369+
+ r'{' + 'rll' * self.inputs + '}')
370+
]
371+
372+
for Di in asarray(self.D):
373+
lines.append('&'.join(_f2s(Dij) for Dij in Di)
374+
+ '\\\\')
375+
376+
lines.extend([
377+
r'\end{array}'
378+
r'\right)',
379+
r'\]'])
380+
381+
return '\n'.join(lines)
382+
383+
def _latex_partitioned(self):
384+
"""Partitioned matrix LaTeX representation of state-space model
385+
386+
Model is presented as a matrix partitioned into A, B, C, and D
387+
parts.
388+
389+
Returns
390+
-------
391+
s : string with LaTeX representation of model
392+
"""
393+
if self.states == 0:
394+
return self._latex_partitioned_stateless()
395+
396+
lines = [
397+
r'\[',
398+
r'\left(',
399+
(r'\begin{array}'
400+
+ r'{' + 'rll' * self.states + '|' + 'rll' * self.inputs + '}')
401+
]
402+
403+
for Ai, Bi in zip(asarray(self.A), asarray(self.B)):
404+
lines.append('&'.join([_f2s(Aij) for Aij in Ai]
405+
+ [_f2s(Bij) for Bij in Bi])
406+
+ '\\\\')
407+
lines.append(r'\hline')
408+
for Ci, Di in zip(asarray(self.C), asarray(self.D)):
409+
lines.append('&'.join([_f2s(Cij) for Cij in Ci]
410+
+ [_f2s(Dij) for Dij in Di])
411+
+ '\\\\')
412+
413+
lines.extend([
414+
r'\end{array}'
415+
r'\right)',
416+
r'\]'])
417+
418+
return '\n'.join(lines)
419+
420+
def _latex_separate(self):
421+
"""Separate matrices LaTeX representation of state-space model
422+
423+
Model is presented as separate, named, A, B, C, and D matrices.
424+
425+
Returns
426+
-------
427+
s : string with LaTeX representation of model
428+
"""
429+
lines = [
430+
r'\[',
431+
r'\begin{array}{ll}',
432+
]
433+
434+
def fmt_matrix(matrix, name):
435+
matlines = [name
436+
+ r' = \left(\begin{array}{'
437+
+ 'rll' * matrix.shape[1]
438+
+ '}']
439+
for row in asarray(matrix):
440+
matlines.append('&'.join(_f2s(entry) for entry in row)
441+
+ '\\\\')
442+
matlines.extend([
443+
r'\end{array}'
444+
r'\right)'])
445+
return matlines
446+
447+
if self.states > 0:
448+
lines.extend(fmt_matrix(self.A, 'A'))
449+
lines.append('&')
450+
lines.extend(fmt_matrix(self.B, 'B'))
451+
lines.append('\\\\')
452+
453+
lines.extend(fmt_matrix(self.C, 'C'))
454+
lines.append('&')
455+
lines.extend(fmt_matrix(self.D, 'D'))
456+
457+
lines.extend([
458+
r'\end{array}',
459+
r'\]'])
460+
461+
return '\n'.join(lines)
462+
463+
def _repr_latex_(self):
464+
"""LaTeX representation of state-space model
465+
466+
Output is controlled by config options statesp.latex_repr_type
467+
and statesp.latex_num_format.
468+
469+
The output is primarily intended for Jupyter notebooks, which
470+
use MathJax to render the LaTeX, and the results may look odd
471+
when processed by a 'conventional' LaTeX system.
472+
473+
Returns
474+
-------
475+
s : string with LaTeX representation of model
476+
477+
"""
478+
if config.defaults['statesp.latex_repr_type'] == 'partitioned':
479+
return self._latex_partitioned()
480+
elif config.defaults['statesp.latex_repr_type'] == 'separate':
481+
return self._latex_separate()
482+
else:
483+
cfg = config.defaults['statesp.latex_repr_type']
484+
raise ValueError("Unknown statesp.latex_repr_type '{cfg}'".format(cfg=cfg))
485+
309486
# Negation of a system
310487
def __neg__(self):
311488
"""Negate a state space system."""

control/tests/statesp_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from control.tests.conftest import ismatarrayout, slycotonly
2121
from control.xferfcn import TransferFunction, ss2tf
2222

23+
from .conftest import editsdefaults
2324

2425
class TestStateSpace:
2526
"""Tests for the StateSpace class."""
@@ -840,3 +841,64 @@ def test_statespace_defaults(self, matarrayout):
840841
for k, v in _statesp_defaults.items():
841842
assert defaults[k] == v, \
842843
"{} is {} but expected {}".format(k, defaults[k], v)
844+
845+
846+
# test data for test_latex_repr below
847+
LTX_G1 = StateSpace([[np.pi, 1e100], [-1.23456789, 5e-23]],
848+
[[0], [1]],
849+
[[987654321, 0.001234]],
850+
[[5]])
851+
852+
LTX_G2 = StateSpace([],
853+
[],
854+
[],
855+
[[1.2345, -2e-200], [-1, 0]])
856+
857+
LTX_G1_REF = {
858+
'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\\]',
859+
860+
'p5_p' : '\\[\n\\left(\n\\begin{array}{rllrll|rll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n-1.&\\hspace{-1em}2346&\\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}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\]',
861+
862+
'p3_s' : '\\[\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}14&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}88&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}00123&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n\\]',
863+
864+
'p5_s' : '\\[\n\\begin{array}{ll}\nA = \\left(\\begin{array}{rllrll}\n3.&\\hspace{-1em}1416&\\hspace{-1em}\\phantom{\\cdot}&1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{100}\\\\\n-1.&\\hspace{-1em}2346&\\hspace{-1em}\\phantom{\\cdot}&5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-23}\\\\\n\\end{array}\\right)\n&\nB = \\left(\\begin{array}{rll}\n0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\\\\nC = \\left(\\begin{array}{rllrll}\n9.&\\hspace{-1em}8765&\\hspace{-1em}\\cdot10^{8}&0.&\\hspace{-1em}001234&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n&\nD = \\left(\\begin{array}{rll}\n5\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n\\]',
865+
}
866+
867+
LTX_G2_REF = {
868+
'p3_p' : '\\[\n\\left(\n\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\]',
869+
870+
'p5_p' : '\\[\n\\left(\n\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\]',
871+
872+
'p3_s' : '\\[\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}23&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n\\]',
873+
874+
'p5_s' : '\\[\n\\begin{array}{ll}\nD = \\left(\\begin{array}{rllrll}\n1.&\\hspace{-1em}2345&\\hspace{-1em}\\phantom{\\cdot}&-2\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\cdot10^{-200}\\\\\n-1\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}&0\\phantom{.}&\\hspace{-1em}&\\hspace{-1em}\\phantom{\\cdot}\\\\\n\\end{array}\\right)\n\\end{array}\n\\]',
875+
}
876+
877+
refkey_n = {None: 'p3', '.3g': 'p3', '.5g': 'p5'}
878+
refkey_r = {None: 'p', 'partitioned': 'p', 'separate': 's'}
879+
880+
881+
@pytest.mark.parametrize(" g, ref",
882+
[(LTX_G1, LTX_G1_REF),
883+
(LTX_G2, LTX_G2_REF)])
884+
@pytest.mark.parametrize("repr_type", [None, "partitioned", "separate"])
885+
@pytest.mark.parametrize("num_format", [None, ".3g", ".5g"])
886+
def test_latex_repr(g, ref, repr_type, num_format, editsdefaults):
887+
"""Test `._latex_repr_` with different config values
888+
889+
This is a 'gold image' test, so if you change behaviour,
890+
you'll need to regenerate the reference results.
891+
Try something like:
892+
control.reset_defaults()
893+
print(f'p3_p : {g1._repr_latex_()!r}')
894+
"""
895+
from control import set_defaults
896+
if num_format is not None:
897+
set_defaults('statesp', latex_num_format=num_format)
898+
899+
if repr_type is not None:
900+
set_defaults('statesp', latex_repr_type=repr_type)
901+
902+
refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
903+
assert g._repr_latex_() == ref[refkey]
904+

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy