6969
7070
7171# 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 }} "
7380
7481
7582class 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'].
95106
96107 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 )
201223
202224 # Determine if the transfer function is static (needed for dt)
203225 static = True
@@ -432,22 +454,29 @@ def _truncatecoeff(self):
432454 [self .num , self .den ] = data
433455
434456 def __str__ (self , var = None ):
435- """String representation of the transfer function."""
457+ """String representation of the transfer function.
436458
437- 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 ()
438463 if 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'
441465 outstr = ""
442466
443- 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 ):
445469 if mimo :
446- outstr += "\n Input %i to output %i:" % (i + 1 , j + 1 )
470+ outstr += "\n Input %i to output %i:" % (ni + 1 , no + 1 )
447471
448472 # 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 )
451480
452481 # Figure out the length of the separating line
453482 dashcount = max (len (numstr ), len (denstr ))
@@ -461,10 +490,9 @@ def __str__(self, var=None):
461490
462491 outstr += "\n " + numstr + "\n " + dashes + "\n " + denstr + "\n "
463492
464- # 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 += "\n dt = " + 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 += "\n dt = " + str (self .dt ) + "\n "
468496
469497 return outstr
470498
@@ -485,7 +513,7 @@ def __repr__(self):
485513 def _repr_latex_ (self , var = None ):
486514 """LaTeX representation of transfer function, for Jupyter notebook"""
487515
488- mimo = self . ninputs > 1 or self .noutputs > 1
516+ mimo = not self .issiso ()
489517
490518 if var is None :
491519 # ! TODO: replace with standard calls to lti functions
@@ -496,18 +524,23 @@ def _repr_latex_(self, var=None):
496524 if mimo :
497525 out .append (r"\begin{bmatrix}" )
498526
499- 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 )
504537
505538 numstr = _tf_string_to_latex (numstr , var = var )
506539 denstr = _tf_string_to_latex (denstr , var = var )
507540
508541 out += [r"\frac{" , numstr , "}{" , denstr , "}" ]
509542
510- if mimo and j < self .noutputs - 1 :
543+ if mimo and ni < self .ninputs - 1 :
511544 out .append ("&" )
512545
513546 if mimo :
@@ -1285,7 +1318,7 @@ def _tf_polynomial_to_string(coeffs, var='s'):
12851318 N = len (coeffs ) - 1
12861319
12871320 for k in range (len (coeffs )):
1288- coefstr = '%.4g' % abs (coeffs [k ])
1321+ coefstr = _float2str ( abs (coeffs [k ]) )
12891322 power = (N - k )
12901323 if power == 0 :
12911324 if coefstr != '0' :
@@ -1323,6 +1356,48 @@ def _tf_polynomial_to_string(coeffs, var='s'):
13231356 return thestr
13241357
13251358
1359+ 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']..
14891568
14901569 Returns
14911570 -------
@@ -1538,7 +1617,7 @@ def tf(*args, **kwargs):
15381617
15391618 >>> # 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)
15421621
15431622 >>> # 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'].
16121695
16131696 Returns
16141697 -------
16151698 out: :class:`TransferFunction`
16161699 Transfer function with given zeros, poles, and gain.
16171700
1701+ 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 """
16191710 num , den = zpk2tf (zeros , poles , gain )
16201711 return TransferFunction (num , den , * args , ** kwargs )
0 commit comments