|
73 | 73 | _statesp_defaults = {
|
74 | 74 | 'statesp.use_numpy_matrix': False, # False is default in 0.9.0 and above
|
75 | 75 | 'statesp.remove_useless_states': True,
|
76 |
| -} |
| 76 | + 'statesp.latex_num_format': '.3g', |
| 77 | + 'statesp.latex_repr_type': 'partitioned', |
| 78 | + } |
77 | 79 |
|
78 | 80 |
|
79 | 81 | def _ssmatrix(data, axis=1):
|
@@ -127,6 +129,33 @@ def _ssmatrix(data, axis=1):
|
127 | 129 | return arr.reshape(shape)
|
128 | 130 |
|
129 | 131 |
|
| 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 | + |
130 | 159 | class StateSpace(LTI):
|
131 | 160 | """StateSpace(A, B, C, D[, dt])
|
132 | 161 |
|
@@ -164,6 +193,24 @@ class StateSpace(LTI):
|
164 | 193 | timebase; the result will have the timebase of the latter system.
|
165 | 194 | The default value of dt can be changed by changing the value of
|
166 | 195 | ``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. |
167 | 214 | """
|
168 | 215 |
|
169 | 216 | # Allow ndarray * StateSpace to give StateSpace._rmul_() priority
|
@@ -329,6 +376,136 @@ def __repr__(self):
|
329 | 376 | C=asarray(self.C).__repr__(), D=asarray(self.D).__repr__(),
|
330 | 377 | dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')
|
331 | 378 |
|
| 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 | + |
332 | 509 | # Negation of a system
|
333 | 510 | def __neg__(self):
|
334 | 511 | """Negate a state space system."""
|
|
0 commit comments