Merge pull request #869 from henklaak/feature_print_zpk · python-control/python-control@26c44e1
@@ -69,7 +69,14 @@
696970707171# Define module default parameter values
72-_xferfcn_defaults = {}
72+_xferfcn_defaults = {
73+'xferfcn.display_format': 'poly',
74+'xferfcn.floating_point_format': '.4g'
75+}
76+77+def _float2str(value):
78+_num_format = config.defaults.get('xferfcn.floating_point_format', ':.4g')
79+return f"{value:{_num_format}}"
738074817582class TransferFunction(LTI):
@@ -92,6 +99,10 @@ class TransferFunction(LTI):
9299 time, positive number is discrete time with specified
93100 sampling time, None indicates unspecified timebase (either
94101 continuous or discrete time).
102+ display_format: None, 'poly' or 'zpk'
103+ Set the display format used in printing the TransferFunction object.
104+ Default behavior is polynomial display and can be changed by
105+ changing config.defaults['xferfcn.display_format'].
9510696107 Attributes
97108 ----------
@@ -198,6 +209,17 @@ def __init__(self, *args, **kwargs):
198209#
199210# Process keyword arguments
200211#
212+# During module init, TransferFunction.s and TransferFunction.z
213+# get initialized when defaults are not fully initialized yet.
214+# Use 'poly' in these cases.
215+216+self.display_format = kwargs.pop(
217+'display_format',
218+config.defaults.get('xferfcn.display_format', 'poly'))
219+220+if self.display_format not in ('poly', 'zpk'):
221+raise ValueError("display_format must be 'poly' or 'zpk',"
222+" got '%s'" % self.display_format)
201223202224# Determine if the transfer function is static (needed for dt)
203225static = True
@@ -432,22 +454,29 @@ def _truncatecoeff(self):
432454 [self.num, self.den] = data
433455434456def __str__(self, var=None):
435-"""String representation of the transfer function."""
457+"""String representation of the transfer function.
436458437-mimo = self.ninputs > 1 or self.noutputs > 1
459+ Based on the display_format property, the output will be formatted as
460+ either polynomials or in zpk form.
461+ """
462+mimo = not self.issiso()
438463if var is None:
439-# TODO: replace with standard calls to lti functions
440-var = 's' if self.dt is None or self.dt == 0 else 'z'
464+var = 's' if self.isctime() else 'z'
441465outstr = ""
442466443-for i in range(self.ninputs):
444-for j in range(self.noutputs):
467+for ni in range(self.ninputs):
468+for no in range(self.noutputs):
445469if mimo:
446-outstr += "\nInput %i to output %i:" % (i + 1, j + 1)
470+outstr += "\nInput %i to output %i:" % (ni + 1, no + 1)
447471448472# Convert the numerator and denominator polynomials to strings.
449-numstr = _tf_polynomial_to_string(self.num[j][i], var=var)
450-denstr = _tf_polynomial_to_string(self.den[j][i], var=var)
473+if self.display_format == 'poly':
474+numstr = _tf_polynomial_to_string(self.num[no][ni], var=var)
475+denstr = _tf_polynomial_to_string(self.den[no][ni], var=var)
476+elif self.display_format == 'zpk':
477+z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni])
478+numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var)
479+denstr = _tf_factorized_polynomial_to_string(p, var=var)
451480452481# Figure out the length of the separating line
453482dashcount = max(len(numstr), len(denstr))
@@ -461,10 +490,9 @@ def __str__(self, var=None):
461490462491outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n"
463492464-# See if this is a discrete time system with specific sampling time
465-if not (self.dt is None) and type(self.dt) != bool and self.dt > 0:
466-# TODO: replace with standard calls to lti functions
467-outstr += "\ndt = " + self.dt.__str__() + "\n"
493+# If this is a strict discrete time system, print the sampling time
494+if type(self.dt) != bool and self.isdtime(strict=True):
495+outstr += "\ndt = " + str(self.dt) + "\n"
468496469497return outstr
470498@@ -485,7 +513,7 @@ def __repr__(self):
485513def _repr_latex_(self, var=None):
486514"""LaTeX representation of transfer function, for Jupyter notebook"""
487515488-mimo = self.ninputs > 1 or self.noutputs > 1
516+mimo = not self.issiso()
489517490518if var is None:
491519# ! TODO: replace with standard calls to lti functions
@@ -496,18 +524,23 @@ def _repr_latex_(self, var=None):
496524if mimo:
497525out.append(r"\begin{bmatrix}")
498526499-for i in range(self.noutputs):
500-for j in range(self.ninputs):
527+for no in range(self.noutputs):
528+for ni in range(self.ninputs):
501529# Convert the numerator and denominator polynomials to strings.
502-numstr = _tf_polynomial_to_string(self.num[i][j], var=var)
503-denstr = _tf_polynomial_to_string(self.den[i][j], var=var)
530+if self.display_format == 'poly':
531+numstr = _tf_polynomial_to_string(self.num[no][ni], var=var)
532+denstr = _tf_polynomial_to_string(self.den[no][ni], var=var)
533+elif self.display_format == 'zpk':
534+z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni])
535+numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var)
536+denstr = _tf_factorized_polynomial_to_string(p, var=var)
504537505538numstr = _tf_string_to_latex(numstr, var=var)
506539denstr = _tf_string_to_latex(denstr, var=var)
507540508541out += [r"\frac{", numstr, "}{", denstr, "}"]
509542510-if mimo and j < self.noutputs - 1:
543+if mimo and ni < self.ninputs - 1:
511544out.append("&")
512545513546if mimo:
@@ -1285,7 +1318,7 @@ def _tf_polynomial_to_string(coeffs, var='s'):
12851318N = len(coeffs) - 1
1286131912871320for k in range(len(coeffs)):
1288-coefstr = '%.4g' % abs(coeffs[k])
1321+coefstr = _float2str(abs(coeffs[k]))
12891322power = (N - k)
12901323if power == 0:
12911324if coefstr != '0':
@@ -1323,6 +1356,48 @@ def _tf_polynomial_to_string(coeffs, var='s'):
13231356return thestr
13241357132513581359+def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'):
1360+"""Convert a factorized polynomial to a string"""
1361+1362+if roots.size == 0:
1363+return _float2str(gain)
1364+1365+factors = []
1366+for root in sorted(roots, reverse=True):
1367+if np.isreal(root):
1368+if root == 0:
1369+factor = f"{var}"
1370+factors.append(factor)
1371+elif root > 0:
1372+factor = f"{var} - {_float2str(np.abs(root))}"
1373+factors.append(factor)
1374+else:
1375+factor = f"{var} + {_float2str(np.abs(root))}"
1376+factors.append(factor)
1377+elif np.isreal(root * 1j):
1378+if root.imag > 0:
1379+factor = f"{var} - {_float2str(np.abs(root))}j"
1380+factors.append(factor)
1381+else:
1382+factor = f"{var} + {_float2str(np.abs(root))}j"
1383+factors.append(factor)
1384+else:
1385+if root.real > 0:
1386+factor = f"{var} - ({_float2str(root)})"
1387+factors.append(factor)
1388+else:
1389+factor = f"{var} + ({_float2str(-root)})"
1390+factors.append(factor)
1391+1392+multiplier = ''
1393+if round(gain, 4) != 1.0:
1394+multiplier = _float2str(gain) + " "
1395+1396+if len(factors) > 1 or multiplier:
1397+factors = [f"({factor})" for factor in factors]
1398+1399+return multiplier + " ".join(factors)
1400+13261401def _tf_string_to_latex(thestr, var='s'):
13271402""" make sure to superscript all digits in a polynomial string
13281403 and convert float coefficients in scientific notation
@@ -1486,6 +1561,10 @@ def tf(*args, **kwargs):
14861561 Polynomial coefficients of the numerator
14871562 den: array_like, or list of list of array_like
14881563 Polynomial coefficients of the denominator
1564+ display_format: None, 'poly' or 'zpk'
1565+ Set the display format used in printing the TransferFunction object.
1566+ Default behavior is polynomial display and can be changed by
1567+ changing config.defaults['xferfcn.display_format']..
1489156814901569 Returns
14911570 -------
@@ -1538,7 +1617,7 @@ def tf(*args, **kwargs):
1538161715391618 >>> # Create a variable 's' to allow algebra operations for SISO systems
15401619 >>> s = tf('s')
1541- >>> G = (s + 1)/(s**2 + 2*s + 1)
1620+ >>> G = (s + 1) / (s**2 + 2*s + 1)
1542162115431622 >>> # Convert a StateSpace to a TransferFunction object.
15441623 >>> sys_ss = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
@@ -1609,12 +1688,24 @@ def zpk(zeros, poles, gain, *args, **kwargs):
16091688 name : string, optional
16101689 System name (used for specifying signals). If unspecified, a generic
16111690 name <sys[id]> is generated with a unique integer id.
1691+ display_format: None, 'poly' or 'zpk'
1692+ Set the display format used in printing the TransferFunction object.
1693+ Default behavior is polynomial display and can be changed by
1694+ changing config.defaults['xferfcn.display_format'].
1612169516131696 Returns
16141697 -------
16151698 out: :class:`TransferFunction`
16161699 Transfer function with given zeros, poles, and gain.
161717001701+ Examples
1702+ --------
1703+ >>> from control import tf
1704+ >>> G = zpk([1],[2, 3], gain=1, display_format='zpk')
1705+ >>> G
1706+ s - 1
1707+ ---------------
1708+ (s - 2) (s - 3)
16181709 """
16191710num, den = zpk2tf(zeros, poles, gain)
16201711return TransferFunction(num, den, *args, **kwargs)