Skip to content

Commit b9dc8df

Browse files
authored
Merge branch 'master' into rebase-pr431
2 parents 8b82850 + 6ed3f74 commit b9dc8df

File tree

5 files changed

+322
-31
lines changed

5 files changed

+322
-31
lines changed

control/statesp.py

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@
7373
_statesp_defaults = {
7474
'statesp.use_numpy_matrix': False, # False is default in 0.9.0 and above
7575
'statesp.remove_useless_states': True,
76-
}
76+
'statesp.latex_num_format': '.3g',
77+
'statesp.latex_repr_type': 'partitioned',
78+
}
7779

7880

7981
def _ssmatrix(data, axis=1):
@@ -127,6 +129,33 @@ def _ssmatrix(data, axis=1):
127129
return arr.reshape(shape)
128130

129131

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

169216
# Allow ndarray * StateSpace to give StateSpace._rmul_() priority
@@ -329,6 +376,136 @@ def __repr__(self):
329376
C=asarray(self.C).__repr__(), D=asarray(self.D).__repr__(),
330377
dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')
331378

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

control/tests/config_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_get_param(self):
3737
assert ct.config._get_param('config', 'test1', None) == 1
3838
assert ct.config._get_param('config', 'test1', None, 1) == 1
3939

40-
ct.config.defaults['config.test3'] is None
40+
ct.config.defaults['config.test3'] = None
4141
assert ct.config._get_param('config', 'test3') is None
4242
assert ct.config._get_param('config', 'test3', 1) == 1
4343
assert ct.config._get_param('config', 'test3', None, 1) is None

control/tests/conftest.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,22 @@
2828
"PendingDeprecationWarning")
2929

3030

31-
@pytest.fixture(scope="session", autouse=TEST_MATRIX_AND_ARRAY,
31+
@pytest.fixture(scope="session", autouse=True)
32+
def control_defaults():
33+
"""Make sure the testing session always starts with the defaults.
34+
35+
This should be the first fixture initialized,
36+
so that all other fixtures see the general defaults (unless they set them
37+
themselves) even before importing control/__init__. Enforce this by adding
38+
it as an argument to all other session scoped fixtures.
39+
"""
40+
control.reset_defaults()
41+
the_defaults = control.config.defaults.copy()
42+
yield
43+
# assert that nothing changed it without reverting
44+
assert control.config.defaults == the_defaults
45+
46+
@pytest.fixture(scope="function", autouse=TEST_MATRIX_AND_ARRAY,
3247
params=[pytest.param("arrayout", marks=matrixerrorfilter),
3348
pytest.param("matrixout", marks=matrixfilter)])
3449
def matarrayout(request):
@@ -70,7 +85,7 @@ def check_deprecated_matrix():
7085
yield
7186

7287

73-
@pytest.fixture(scope="session",
88+
@pytest.fixture(scope="function",
7489
params=[p for p, usebydefault in
7590
[(pytest.param(np.array,
7691
id="arrayin"),
@@ -90,7 +105,7 @@ def editsdefaults():
90105
"""Make sure any changes to the defaults only last during a test"""
91106
restore = control.config.defaults.copy()
92107
yield
93-
control.config.defaults.update(restore)
108+
control.config.defaults = restore.copy()
94109

95110

96111
@pytest.fixture(scope="function")

control/tests/freqresp_test.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,39 +120,62 @@ def test_mimo():
120120
tf(sysMIMO)
121121

122122

123-
def test_bode_margin():
123+
@pytest.mark.parametrize(
124+
"Hz, Wcp, Wcg",
125+
[pytest.param(False, 6.0782869, 10., id="omega"),
126+
pytest.param(True, 0.9673894, 1.591549, id="Hz")])
127+
@pytest.mark.parametrize(
128+
"deg, p0, pm",
129+
[pytest.param(False, -np.pi, -2.748266, id="rad"),
130+
pytest.param(True, -180, -157.46405841, id="deg")])
131+
@pytest.mark.parametrize(
132+
"dB, maginfty1, maginfty2, gminv",
133+
[pytest.param(False, 1, 1e-8, 0.4, id="mag"),
134+
pytest.param(True, 0, -1e+5, -7.9588, id="dB")])
135+
def test_bode_margin(dB, maginfty1, maginfty2, gminv,
136+
deg, p0, pm,
137+
Hz, Wcp, Wcg):
124138
"""Test bode margins"""
125139
num = [1000]
126140
den = [1, 25, 100, 0]
127141
sys = ctrl.tf(num, den)
128142
plt.figure()
129-
ctrl.bode_plot(sys, margins=True, dB=False, deg=True, Hz=False)
143+
ctrl.bode_plot(sys, margins=True, dB=dB, deg=deg, Hz=Hz)
130144
fig = plt.gcf()
131145
allaxes = fig.get_axes()
132146

133-
mag_to_infinity = (np.array([6.07828691, 6.07828691]),
134-
np.array([1., 1e-8]))
135-
assert_allclose(mag_to_infinity, allaxes[0].lines[2].get_data())
136-
137-
gm_to_infinty = (np.array([10., 10.]),
138-
np.array([4e-1, 1e-8]))
139-
assert_allclose(gm_to_infinty, allaxes[0].lines[3].get_data())
140-
141-
one_to_gm = (np.array([10., 10.]),
142-
np.array([1., 0.4]))
143-
assert_allclose(one_to_gm, allaxes[0].lines[4].get_data())
144-
145-
pm_to_infinity = (np.array([6.07828691, 6.07828691]),
146-
np.array([100000., -157.46405841]))
147-
assert_allclose(pm_to_infinity, allaxes[1].lines[2].get_data())
148-
149-
pm_to_phase = (np.array([6.07828691, 6.07828691]),
150-
np.array([-157.46405841, -180.]))
151-
assert_allclose(pm_to_phase, allaxes[1].lines[3].get_data())
152-
153-
phase_to_infinity = (np.array([10., 10.]),
154-
np.array([1e-8, -1.8e2]))
155-
assert_allclose(phase_to_infinity, allaxes[1].lines[4].get_data())
147+
mag_to_infinity = (np.array([Wcp, Wcp]),
148+
np.array([maginfty1, maginfty2]))
149+
assert_allclose(mag_to_infinity,
150+
allaxes[0].lines[2].get_data(),
151+
rtol=1e-5)
152+
153+
gm_to_infinty = (np.array([Wcg, Wcg]),
154+
np.array([gminv, maginfty2]))
155+
assert_allclose(gm_to_infinty,
156+
allaxes[0].lines[3].get_data(),
157+
rtol=1e-5)
158+
159+
one_to_gm = (np.array([Wcg, Wcg]),
160+
np.array([maginfty1, gminv]))
161+
assert_allclose(one_to_gm, allaxes[0].lines[4].get_data(),
162+
rtol=1e-5)
163+
164+
pm_to_infinity = (np.array([Wcp, Wcp]),
165+
np.array([1e5, pm]))
166+
assert_allclose(pm_to_infinity,
167+
allaxes[1].lines[2].get_data(),
168+
rtol=1e-5)
169+
170+
pm_to_phase = (np.array([Wcp, Wcp]),
171+
np.array([pm, p0]))
172+
assert_allclose(pm_to_phase, allaxes[1].lines[3].get_data(),
173+
rtol=1e-5)
174+
175+
phase_to_infinity = (np.array([Wcg, Wcg]),
176+
np.array([1e-8, p0]))
177+
assert_allclose(phase_to_infinity, allaxes[1].lines[4].get_data(),
178+
rtol=1e-5)
156179

157180

158181
@pytest.fixture

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