|
74 | 74 | 'statesp.use_numpy_matrix': False, # False is default in 0.9.0 and above
|
75 | 75 | 'statesp.default_dt': None,
|
76 | 76 | 'statesp.remove_useless_states': True,
|
| 77 | + 'statesp.latex_num_format': '.3g', |
| 78 | + 'statesp.latex_repr_type': 'partitioned', |
77 | 79 | }
|
78 | 80 |
|
79 | 81 |
|
@@ -128,6 +130,33 @@ def _ssmatrix(data, axis=1):
|
128 | 130 | return arr.reshape(shape)
|
129 | 131 |
|
130 | 132 |
|
| 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 | + |
131 | 160 | class StateSpace(LTI):
|
132 | 161 | """StateSpace(A, B, C, D[, dt])
|
133 | 162 |
|
@@ -158,6 +187,24 @@ class StateSpace(LTI):
|
158 | 187 | time. The default value of 'dt' is None and can be changed by changing the
|
159 | 188 | value of ``control.config.defaults['statesp.default_dt']``.
|
160 | 189 |
|
| 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 | +
|
161 | 208 | """
|
162 | 209 |
|
163 | 210 | # Allow ndarray * StateSpace to give StateSpace._rmul_() priority
|
@@ -306,6 +353,136 @@ def __repr__(self):
|
306 | 353 | C=asarray(self.C).__repr__(), D=asarray(self.D).__repr__(),
|
307 | 354 | dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')
|
308 | 355 |
|
| 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 | + |
309 | 486 | # Negation of a system
|
310 | 487 | def __neg__(self):
|
311 | 488 | """Negate a state space system."""
|
|
0 commit comments