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}}"

738074817582

class 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)

203225

static = True

@@ -432,22 +454,29 @@ def _truncatecoeff(self):

432454

[self.num, self.den] = data

433455434456

def __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()

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 = ""

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):

445469

if 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

453482

dashcount = max(len(numstr), len(denstr))

@@ -461,10 +490,9 @@ def __str__(self, var=None):

461490462491

outstr += "\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"

468496469497

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"""

487515488-

mimo = self.ninputs > 1 or self.noutputs > 1

516+

mimo = not self.issiso()

489517490518

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}")

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)

504537505538

numstr = _tf_string_to_latex(numstr, var=var)

506539

denstr = _tf_string_to_latex(denstr, var=var)

507540508541

out += [r"\frac{", numstr, "}{", denstr, "}"]

509542510-

if mimo and j < self.noutputs - 1:

543+

if mimo and ni < self.ninputs - 1:

511544

out.append("&")

512545513546

if mimo:

@@ -1285,7 +1318,7 @@ def _tf_polynomial_to_string(coeffs, var='s'):

12851318

N = len(coeffs) - 1

1286131912871320

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

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+13261401

def _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

"""

16191710

num, den = zpk2tf(zeros, poles, gain)

16201711

return TransferFunction(num, den, *args, **kwargs)