From 5c854592a994a4cab6b1151991a62b0e985806bf Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Tue, 21 Feb 2023 16:26:39 +0100 Subject: [PATCH 01/14] Add print zpk form --- control/tests/xferfcn_test.py | 29 ++++++++- control/xferfcn.py | 110 ++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 6e1cf6ce2..eda2ce661 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -9,7 +9,7 @@ import control as ct from control import StateSpace, TransferFunction, rss, evalfr -from control import ss, ss2tf, tf, tf2ss +from control import ss, ss2tf, tf, tf2ss, zpk from control import isctime, isdtime, sample_system, defaults from control.statesp import _convert_to_statespace from control.xferfcn import _convert_to_transfer_function @@ -906,6 +906,33 @@ def test_printing_mimo(self): assert isinstance(str(sys), str) assert isinstance(sys._repr_latex_(), str) + @pytest.mark.parametrize( + "zeros, poles, gain, output", + [([0], [-1], 1, "\n s\n-----\ns + 1\n"), + ([-1], [-1], 1, "\ns + 1\n-----\ns + 1\n"), + ([-1], [1], 1, "\ns + 1\n-----\ns - 1\n"), + ([1], [-1], 1, "\ns - 1\n-----\ns + 1\n"), + ([-1], [-1], 2, "\n2 (s + 1)\n---------\n s + 1\n"), + ([-1], [-1], 0, "\n0\n-\n1\n"), + ([-1], [1j, -1j], 1, "\n s + 1\n-----------------\n(s - 1j) (s + 1j)\n"), + ([4j, -4j], [2j, -2j], 2, "\n2 (s - 4j) (s + 4j)\n-------------------\n (s - 2j) (s + 2j)\n"), + ([1j, -1j], [-1, -4], 2, "\n2 (s - 1j) (s + 1j)\n-------------------\n (s + 1) (s + 4)\n"), + ([1], [-1 + 1j, -1 - 1j], 1, "\n s - 1\n-------------------------\n(s + (1-1j)) (s + (1+1j))\n"), + ([1], [1 + 1j, 1 - 1j], 1, "\n s - 1\n-------------------------\n(s - (1+1j)) (s - (1-1j))\n"), + ]) + def test_printing_zpk(self, zeros, poles, gain, output): + """Test _tf_polynomial_to_string for constant systems""" + G = zpk(zeros, poles, gain) + print(G) + res = G.to_zpk() + print(res) + assert res == output + + def test_printing_zpk_invalid(self): + G = tf([1], [1 + 1j]) + with pytest.raises(ValueError, match='complex valued'): + G.to_zpk() + @slycotonly def test_size_mismatch(self): """Test size mismacht""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 0bc84e096..e9c98ec6c 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -468,6 +468,73 @@ def __str__(self, var=None): return outstr + def to_zpk(self, var=None): + """Return string representation of the transfer function as factorized + polynomials. + + Examples + -------- + >>> from control import tf + >>> G = tf([1],[1, -2, 1]) + >>> G.to_zpk() + 1 + --------------- + (s - 1) (s - 1) + + """ + + mimo = self.ninputs > 1 or self.noutputs > 1 + if var is None: + # TODO: replace with standard calls to lti functions + var = 's' if self.dt is None or self.dt == 0 else 'z' + outstr = "" + + for i in range(self.ninputs): + for j in range(self.noutputs): + if mimo: + outstr += "\nInput %i to output %i:" % (i + 1, j + 1) + + # Convert the numerator and denominator polynomials to strings. + dcgain = self.num[j][i][-1] / self.den[j][i][-1] + + num_roots = roots(self.num[j][i]).astype(complex) + den_roots = roots(self.den[j][i]).astype(complex) + + polygain = np.prod(num_roots) / np.prod(den_roots) + + if abs(polygain) == 0 and abs(dcgain) == 0: + k = 1 + else: + k = dcgain/polygain + if not np.isreal(k): + raise ValueError("Transfer function has complex valued gain. " + "Please check polynomials for non-complimentary poles.") + + k = np.abs(k) + + numstr = _tf_factorized_polynomial_to_string(num_roots, gain=k, var=var) + denstr = _tf_factorized_polynomial_to_string(den_roots, var=var) + + # Figure out the length of the separating line + dashcount = max(len(numstr), len(denstr)) + dashes = '-' * dashcount + + # Center the numerator or denominator + if len(numstr) < dashcount: + numstr = ' ' * ((dashcount - len(numstr)) // 2) + numstr + if len(denstr) < dashcount: + denstr = ' ' * ((dashcount - len(denstr)) // 2) + denstr + + outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n" + + # See if this is a discrete time system with specific sampling time + if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: + # TODO: replace with standard calls to lti functions + outstr += "\ndt = " + self.dt.__str__() + "\n" + + return outstr + + # represent to implement a re-loadable version def __repr__(self): """Print transfer function in loadable form""" @@ -1323,6 +1390,49 @@ def _tf_polynomial_to_string(coeffs, var='s'): return thestr +def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'): + """Convert a factorized polynomial to a string""" + + if roots.size == 0: + return f"{gain:.4g}" + + factors = [] + for root in sorted(roots, reverse=True): + if np.isreal(root): + if root == 0: + factor = f"{var}" + factors.append(factor) + elif root > 0: + factor = f"{var} - {np.abs(root):.4g}" + factors.append(factor) + else: + factor = f"{var} + {np.abs(root):.4g}" + factors.append(factor) + elif np.isreal(root * 1j): + if root.imag > 0: + factor = f"{var} - {np.abs(root):.4g}j" + factors.append(factor) + else: + factor = f"{var} + {np.abs(root):.4g}j" + factors.append(factor) + else: + if root.real > 0: + factor = f"{var} - ({root:.4g})" + factors.append(factor) + else: + factor = f"{var} + ({-root:.4g})" + factors.append(factor) + + + multiplier = '' + if round(gain, 4) != 1.0: + multiplier = f"{gain:.4g} " + + if len(factors) > 1 or multiplier: + factors = [f"({factor})" for factor in factors] + + return multiplier + " ".join(factors) + def _tf_string_to_latex(thestr, var='s'): """ make sure to superscript all digits in a polynomial string and convert float coefficients in scientific notation From 57620f19b742ef40cdeb9b7cc4e4da1977383ddd Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Tue, 21 Feb 2023 16:35:23 +0100 Subject: [PATCH 02/14] Fix unittest --- control/xferfcn.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index e9c98ec6c..1460c0d20 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -505,9 +505,13 @@ def to_zpk(self, var=None): if abs(polygain) == 0 and abs(dcgain) == 0: k = 1 else: + if abs(polygain) == 0: + raise ValueError( + f"Transfer function has infinite gain. " + "Please check polynomials.") k = dcgain/polygain if not np.isreal(k): - raise ValueError("Transfer function has complex valued gain. " + raise ValueError(f"Transfer function has complex valued gain (k = {k}). " "Please check polynomials for non-complimentary poles.") k = np.abs(k) From b36732db57524d3e874d2ead637564bccd8ec838 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Tue, 21 Feb 2023 16:46:19 +0100 Subject: [PATCH 03/14] Add fix for precision noise --- control/xferfcn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/control/xferfcn.py b/control/xferfcn.py index 1460c0d20..3a15f6c4f 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -502,6 +502,11 @@ def to_zpk(self, var=None): polygain = np.prod(num_roots) / np.prod(den_roots) + # Round imaginary part down to zero for values close to + # precision to prevent small errors to mess up things. + polygain = complex(polygain.real, + round(polygain.imag, 12)) + if abs(polygain) == 0 and abs(dcgain) == 0: k = 1 else: From 45b5ae4273ddc172a6b71ecda45e9f779bf1a0fa Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Tue, 21 Feb 2023 17:07:38 +0100 Subject: [PATCH 04/14] Cleanup unittest --- control/tests/xferfcn_test.py | 98 ++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index eda2ce661..55a5ec111 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -908,24 +908,96 @@ def test_printing_mimo(self): @pytest.mark.parametrize( "zeros, poles, gain, output", - [([0], [-1], 1, "\n s\n-----\ns + 1\n"), - ([-1], [-1], 1, "\ns + 1\n-----\ns + 1\n"), - ([-1], [1], 1, "\ns + 1\n-----\ns - 1\n"), - ([1], [-1], 1, "\ns - 1\n-----\ns + 1\n"), - ([-1], [-1], 2, "\n2 (s + 1)\n---------\n s + 1\n"), - ([-1], [-1], 0, "\n0\n-\n1\n"), - ([-1], [1j, -1j], 1, "\n s + 1\n-----------------\n(s - 1j) (s + 1j)\n"), - ([4j, -4j], [2j, -2j], 2, "\n2 (s - 4j) (s + 4j)\n-------------------\n (s - 2j) (s + 2j)\n"), - ([1j, -1j], [-1, -4], 2, "\n2 (s - 1j) (s + 1j)\n-------------------\n (s + 1) (s + 4)\n"), - ([1], [-1 + 1j, -1 - 1j], 1, "\n s - 1\n-------------------------\n(s + (1-1j)) (s + (1+1j))\n"), - ([1], [1 + 1j, 1 - 1j], 1, "\n s - 1\n-------------------------\n(s - (1+1j)) (s - (1-1j))\n"), + [([0], [-1], 1, + '\n' + ' s\n' + '-----\n' + 's + 1\n'), + ([-1], [-1], 1, + '\n' + 's + 1\n' + '-----\n' + 's + 1\n'), + ([-1], [1], 1, + '\n' + 's + 1\n' + '-----\n' + 's - 1\n'), + ([1], [-1], 1, + '\n' + 's - 1\n' + '-----\n' + 's + 1\n'), + ([-1], [-1], 2, + '\n' + '2 (s + 1)\n' + '---------\n' + ' s + 1\n'), + ([-1], [-1], 0, + '\n' + '0\n' + '-\n' + '1\n'), + ([-1], [1j, -1j], 1, + '\n' + ' s + 1\n' + '-----------------\n' + '(s - 1j) (s + 1j)\n'), + ([4j, -4j], [2j, -2j], 2, + '\n' + '2 (s - 4j) (s + 4j)\n' + '-------------------\n' + ' (s - 2j) (s + 2j)\n'), + ([1j, -1j], [-1, -4], 2, + '\n' + '2 (s - 1j) (s + 1j)\n' + '-------------------\n' + ' (s + 1) (s + 4)\n'), + ([1], [-1 + 1j, -1 - 1j], 1, + '\n' + ' s - 1\n' + '-------------------------\n' + '(s + (1-1j)) (s + (1+1j))\n'), + ([1], [1 + 1j, 1 - 1j], 1, + '\n' + ' s - 1\n' + '-------------------------\n' + '(s - (1+1j)) (s - (1-1j))\n'), ]) def test_printing_zpk(self, zeros, poles, gain, output): """Test _tf_polynomial_to_string for constant systems""" G = zpk(zeros, poles, gain) - print(G) res = G.to_zpk() - print(res) + assert res == output + + @pytest.mark.parametrize( + "num, den, output", + [([[[11], [21]], [[12], [22]]], + [[[1, -3, 2], [1, 1, -6]], [[1, 0, 1], [1, -1, -20]]], + ('\n' + 'Input 1 to output 1:\n' + ' 11\n' + '---------------\n' + '(s - 2) (s - 1)\n' + '\n' + 'Input 1 to output 2:\n' + ' 12\n' + '-----------------\n' + '(s - 1j) (s + 1j)\n' + '\n' + 'Input 2 to output 1:\n' + ' 21\n' + '---------------\n' + '(s - 2) (s + 3)\n' + '\n' + 'Input 2 to output 2:\n' + ' 22\n' + '---------------\n' + '(s - 5) (s + 4)\n'))]) + def test_printing_zpk_mimo(self, num, den, output): + """Test _tf_polynomial_to_string for constant systems""" + G = tf(num, den) + res = G.to_zpk() assert res == output def test_printing_zpk_invalid(self): From 93f7b465bca4869d74768f1742e2baf13af2d9bf Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Fri, 24 Feb 2023 11:01:02 +0100 Subject: [PATCH 05/14] Rework after review --- control/tests/xferfcn_test.py | 11 +-- control/xferfcn.py | 134 +++++++++++++--------------------- pyproject.toml | 2 +- 3 files changed, 53 insertions(+), 94 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 55a5ec111..cd5396c07 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -967,7 +967,7 @@ def test_printing_mimo(self): def test_printing_zpk(self, zeros, poles, gain, output): """Test _tf_polynomial_to_string for constant systems""" G = zpk(zeros, poles, gain) - res = G.to_zpk() + res = str(G) assert res == output @pytest.mark.parametrize( @@ -996,15 +996,10 @@ def test_printing_zpk(self, zeros, poles, gain, output): '(s - 5) (s + 4)\n'))]) def test_printing_zpk_mimo(self, num, den, output): """Test _tf_polynomial_to_string for constant systems""" - G = tf(num, den) - res = G.to_zpk() + G = tf(num, den, display_format='zpk') + res = str(G) assert res == output - def test_printing_zpk_invalid(self): - G = tf([1], [1 + 1j]) - with pytest.raises(ValueError, match='complex valued'): - G.to_zpk() - @slycotonly def test_size_mismatch(self): """Test size mismacht""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 3a15f6c4f..3e67cb31b 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -92,6 +92,9 @@ class TransferFunction(LTI): time, positive number is discrete time with specified sampling time, None indicates unspecified timebase (either continuous or discrete time). + display_format: None, 'poly' or 'zpk' + Set the display format used in printing the TransferFunction object. + Default behavior is polynomial display. Attributes ---------- @@ -149,7 +152,7 @@ class TransferFunction(LTI): # Give TransferFunction._rmul_() priority for ndarray * TransferFunction __array_priority__ = 11 # override ndarray and matrix types - def __init__(self, *args, **kwargs): + def __init__(self, *args, display_format=None, **kwargs): """TransferFunction(num, den[, dt]) Construct a transfer function. @@ -198,6 +201,14 @@ def __init__(self, *args, **kwargs): # # Process keyword arguments # + if display_format is None: + display_format = 'poly' + + if display_format not in ('poly', 'zpk'): + raise ValueError("display_format must be 'poly' or 'zpk'," + " got '%s'" % display_format) + + self.display_format = display_format # Determine if the transfer function is static (needed for dt) static = True @@ -432,61 +443,16 @@ def _truncatecoeff(self): [self.num, self.den] = data def __str__(self, var=None): - """String representation of the transfer function.""" - - mimo = self.ninputs > 1 or self.noutputs > 1 - if var is None: - # TODO: replace with standard calls to lti functions - var = 's' if self.dt is None or self.dt == 0 else 'z' - outstr = "" - - for i in range(self.ninputs): - for j in range(self.noutputs): - if mimo: - outstr += "\nInput %i to output %i:" % (i + 1, j + 1) - - # Convert the numerator and denominator polynomials to strings. - numstr = _tf_polynomial_to_string(self.num[j][i], var=var) - denstr = _tf_polynomial_to_string(self.den[j][i], var=var) + """String representation of the transfer function. - # Figure out the length of the separating line - dashcount = max(len(numstr), len(denstr)) - dashes = '-' * dashcount - - # Center the numerator or denominator - if len(numstr) < dashcount: - numstr = ' ' * ((dashcount - len(numstr)) // 2) + numstr - if len(denstr) < dashcount: - denstr = ' ' * ((dashcount - len(denstr)) // 2) + denstr - - outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n" - - # See if this is a discrete time system with specific sampling time - if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: - # TODO: replace with standard calls to lti functions - outstr += "\ndt = " + self.dt.__str__() + "\n" - - return outstr - - def to_zpk(self, var=None): - """Return string representation of the transfer function as factorized - polynomials. - - Examples - -------- - >>> from control import tf - >>> G = tf([1],[1, -2, 1]) - >>> G.to_zpk() - 1 - --------------- - (s - 1) (s - 1) + Based on the display_format property, the output will be formatted as + either polynomials or in zpk form. """ - mimo = self.ninputs > 1 or self.noutputs > 1 + mimo = not self.issiso() if var is None: - # TODO: replace with standard calls to lti functions - var = 's' if self.dt is None or self.dt == 0 else 'z' + var = 's' if self.isctime() else 'z' outstr = "" for i in range(self.ninputs): @@ -495,34 +461,13 @@ def to_zpk(self, var=None): outstr += "\nInput %i to output %i:" % (i + 1, j + 1) # Convert the numerator and denominator polynomials to strings. - dcgain = self.num[j][i][-1] / self.den[j][i][-1] - - num_roots = roots(self.num[j][i]).astype(complex) - den_roots = roots(self.den[j][i]).astype(complex) - - polygain = np.prod(num_roots) / np.prod(den_roots) - - # Round imaginary part down to zero for values close to - # precision to prevent small errors to mess up things. - polygain = complex(polygain.real, - round(polygain.imag, 12)) - - if abs(polygain) == 0 and abs(dcgain) == 0: - k = 1 - else: - if abs(polygain) == 0: - raise ValueError( - f"Transfer function has infinite gain. " - "Please check polynomials.") - k = dcgain/polygain - if not np.isreal(k): - raise ValueError(f"Transfer function has complex valued gain (k = {k}). " - "Please check polynomials for non-complimentary poles.") - - k = np.abs(k) - - numstr = _tf_factorized_polynomial_to_string(num_roots, gain=k, var=var) - denstr = _tf_factorized_polynomial_to_string(den_roots, var=var) + if self.display_format == 'poly': + numstr = _tf_polynomial_to_string(self.num[j][i], var=var) + denstr = _tf_polynomial_to_string(self.den[j][i], var=var) + elif self.display_format == 'zpk': + z, p, k = tf2zpk(self.num[j][i], self.den[j][i]) + numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var) + denstr = _tf_factorized_polynomial_to_string(p, var=var) # Figure out the length of the separating line dashcount = max(len(numstr), len(denstr)) @@ -538,8 +483,7 @@ def to_zpk(self, var=None): # See if this is a discrete time system with specific sampling time if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: - # TODO: replace with standard calls to lti functions - outstr += "\ndt = " + self.dt.__str__() + "\n" + outstr += "\ndt = " + str(self.dt) + "\n" return outstr @@ -575,8 +519,13 @@ def _repr_latex_(self, var=None): for i in range(self.noutputs): for j in range(self.ninputs): # Convert the numerator and denominator polynomials to strings. - numstr = _tf_polynomial_to_string(self.num[i][j], var=var) - denstr = _tf_polynomial_to_string(self.den[i][j], var=var) + if self.display_format == 'poly': + numstr = _tf_polynomial_to_string(self.num[j][i], var=var) + denstr = _tf_polynomial_to_string(self.den[j][i], var=var) + elif self.display_format == 'zpk': + z, p, k = tf2zpk(self.num[j][i], self.den[j][i]) + numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var) + denstr = _tf_factorized_polynomial_to_string(p, var=var) numstr = _tf_string_to_latex(numstr, var=var) denstr = _tf_string_to_latex(denstr, var=var) @@ -1432,7 +1381,6 @@ def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'): factor = f"{var} + ({-root:.4g})" factors.append(factor) - multiplier = '' if round(gain, 4) != 1.0: multiplier = f"{gain:.4g} " @@ -1605,6 +1553,9 @@ def tf(*args, **kwargs): Polynomial coefficients of the numerator den: array_like, or list of list of array_like Polynomial coefficients of the denominator + display_format: None, 'poly' or 'zpk' + Set the display format used in printing the TransferFunction object. + Default behavior is polynomial display. Returns ------- @@ -1657,7 +1608,7 @@ def tf(*args, **kwargs): >>> # Create a variable 's' to allow algebra operations for SISO systems >>> s = tf('s') - >>> G = (s + 1)/(s**2 + 2*s + 1) + >>> G = (s + 1) / (s**2 + 2*s + 1) >>> # Convert a StateSpace to a TransferFunction object. >>> sys_ss = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") @@ -1728,14 +1679,27 @@ def zpk(zeros, poles, gain, *args, **kwargs): name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + display_format: None, 'poly' or 'zpk' + Set the display format used in printing the TransferFunction object. + Default behavior is zpk display. Returns ------- out: :class:`TransferFunction` Transfer function with given zeros, poles, and gain. + Examples + -------- + >>> from control import tf + >>> G = zpk([1],[2, 3], gain=6) + >>> G + s - 1 + --------------- + (s - 2) (s - ) """ num, den = zpk2tf(zeros, poles, gain) + if 'display_format' not in kwargs: + kwargs['display_format'] = 'zpk' return TransferFunction(num, den, *args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index 01b2155e4..0b1f42a90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ source = "https://github.com/python-control/python-control" write_to = "control/_version.py" [tool.pytest.ini_options] -addopts = "-ra" +addopts = "-ra --ff -x" filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] From d2807b0c53c6ec41a2545278e789bff98d754487 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Fri, 24 Feb 2023 11:05:21 +0100 Subject: [PATCH 06/14] Shouldn't have commited the pyproject.toml, revering --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b1f42a90..01b2155e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ source = "https://github.com/python-control/python-control" write_to = "control/_version.py" [tool.pytest.ini_options] -addopts = "-ra --ff -x" +addopts = "-ra" filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] From 5652bf025fd7966960029634a195d072d9220619 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Fri, 24 Feb 2023 11:24:10 +0100 Subject: [PATCH 07/14] Fix variable mixup due to inconsistent naming --- control/xferfcn.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 3e67cb31b..afb5e7a30 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -455,17 +455,17 @@ def __str__(self, var=None): var = 's' if self.isctime() else 'z' outstr = "" - for i in range(self.ninputs): - for j in range(self.noutputs): + for ni in range(self.ninputs): + for no in range(self.noutputs): if mimo: - outstr += "\nInput %i to output %i:" % (i + 1, j + 1) + outstr += "\nInput %i to output %i:" % (ni + 1, no + 1) # Convert the numerator and denominator polynomials to strings. if self.display_format == 'poly': - numstr = _tf_polynomial_to_string(self.num[j][i], var=var) - denstr = _tf_polynomial_to_string(self.den[j][i], var=var) + numstr = _tf_polynomial_to_string(self.num[no][ni], var=var) + denstr = _tf_polynomial_to_string(self.den[no][ni], var=var) elif self.display_format == 'zpk': - z, p, k = tf2zpk(self.num[j][i], self.den[j][i]) + z, p, k = tf2zpk(self.num[j][i], self.den[no][ni]) numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var) denstr = _tf_factorized_polynomial_to_string(p, var=var) @@ -505,7 +505,7 @@ def __repr__(self): def _repr_latex_(self, var=None): """LaTeX representation of transfer function, for Jupyter notebook""" - mimo = self.ninputs > 1 or self.noutputs > 1 + mimo = not self.issiso() if var is None: # ! TODO: replace with standard calls to lti functions @@ -516,14 +516,14 @@ def _repr_latex_(self, var=None): if mimo: out.append(r"\begin{bmatrix}") - for i in range(self.noutputs): - for j in range(self.ninputs): + for no in range(self.noutputs): + for ni in range(self.ninputs): # Convert the numerator and denominator polynomials to strings. if self.display_format == 'poly': - numstr = _tf_polynomial_to_string(self.num[j][i], var=var) - denstr = _tf_polynomial_to_string(self.den[j][i], var=var) + numstr = _tf_polynomial_to_string(self.num[no][ni], var=var) + denstr = _tf_polynomial_to_string(self.den[no][ni], var=var) elif self.display_format == 'zpk': - z, p, k = tf2zpk(self.num[j][i], self.den[j][i]) + z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni]) numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var) denstr = _tf_factorized_polynomial_to_string(p, var=var) From 58e9d63e0a36081bcd754411ae1b56c645b8fd7d Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Fri, 24 Feb 2023 11:26:44 +0100 Subject: [PATCH 08/14] Fix variable mixup due to inconsistent naming --- control/xferfcn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index afb5e7a30..5ed78b780 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -465,7 +465,7 @@ def __str__(self, var=None): numstr = _tf_polynomial_to_string(self.num[no][ni], var=var) denstr = _tf_polynomial_to_string(self.den[no][ni], var=var) elif self.display_format == 'zpk': - z, p, k = tf2zpk(self.num[j][i], self.den[no][ni]) + z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni]) numstr = _tf_factorized_polynomial_to_string(z, gain=k, var=var) denstr = _tf_factorized_polynomial_to_string(p, var=var) @@ -532,7 +532,7 @@ def _repr_latex_(self, var=None): out += [r"\frac{", numstr, "}{", denstr, "}"] - if mimo and j < self.noutputs - 1: + if mimo and no < self.noutputs - 1: out.append("&") if mimo: From 521a306eea0a09897e921a60e43aac999a216bb8 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Fri, 24 Feb 2023 11:49:41 +0100 Subject: [PATCH 09/14] Typos --- control/xferfcn.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 5ed78b780..1d2f7be56 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -94,7 +94,7 @@ class TransferFunction(LTI): continuous or discrete time). display_format: None, 'poly' or 'zpk' Set the display format used in printing the TransferFunction object. - Default behavior is polynomial display. + Default behavior is polynomial display. Attributes ---------- @@ -487,7 +487,6 @@ def __str__(self, var=None): return outstr - # represent to implement a re-loadable version def __repr__(self): """Print transfer function in loadable form""" @@ -532,7 +531,7 @@ def _repr_latex_(self, var=None): out += [r"\frac{", numstr, "}{", denstr, "}"] - if mimo and no < self.noutputs - 1: + if mimo and ni < self.ninputs - 1: out.append("&") if mimo: @@ -1691,11 +1690,11 @@ def zpk(zeros, poles, gain, *args, **kwargs): Examples -------- >>> from control import tf - >>> G = zpk([1],[2, 3], gain=6) + >>> G = zpk([1],[2, 3], gain=1) >>> G s - 1 --------------- - (s - 2) (s - ) + (s - 2) (s - 3) """ num, den = zpk2tf(zeros, poles, gain) if 'display_format' not in kwargs: From 2644874b2a67b195bf092fb06fbe2cea813e15c6 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Sat, 25 Feb 2023 12:35:25 +0100 Subject: [PATCH 10/14] Add default for xferfcn.display_format --- control/xferfcn.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 1d2f7be56..1bf0ad875 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -69,7 +69,9 @@ # Define module default parameter values -_xferfcn_defaults = {} +_xferfcn_defaults = { + 'xferfcn.display_format': 'poly', +} class TransferFunction(LTI): @@ -94,7 +96,8 @@ class TransferFunction(LTI): continuous or discrete time). display_format: None, 'poly' or 'zpk' Set the display format used in printing the TransferFunction object. - Default behavior is polynomial display. + Default behavior is polynomial display and can be changed by + changing config.defaults['xferfcn.display_format']. Attributes ---------- @@ -152,7 +155,7 @@ class TransferFunction(LTI): # Give TransferFunction._rmul_() priority for ndarray * TransferFunction __array_priority__ = 11 # override ndarray and matrix types - def __init__(self, *args, display_format=None, **kwargs): + def __init__(self, *args, **kwargs): """TransferFunction(num, den[, dt]) Construct a transfer function. @@ -201,14 +204,17 @@ def __init__(self, *args, display_format=None, **kwargs): # # Process keyword arguments # - if display_format is None: - display_format = 'poly' + # During module init, TransferFunction.s and TransferFunction.z + # get initialized when defaults are not fully initialized yet. + # Use 'poly' in these cases. - if display_format not in ('poly', 'zpk'): - raise ValueError("display_format must be 'poly' or 'zpk'," - " got '%s'" % display_format) + self.display_format = kwargs.pop( + 'display_format', + config.defaults.get('xferfcn.display_format', 'poly')) - self.display_format = display_format + if self.display_format not in ('poly', 'zpk'): + raise ValueError("display_format must be 'poly' or 'zpk'," + " got '%s'" % self.display_format) # Determine if the transfer function is static (needed for dt) static = True @@ -447,9 +453,7 @@ def __str__(self, var=None): Based on the display_format property, the output will be formatted as either polynomials or in zpk form. - """ - mimo = not self.issiso() if var is None: var = 's' if self.isctime() else 'z' @@ -481,8 +485,8 @@ def __str__(self, var=None): outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n" - # See if this is a discrete time system with specific sampling time - if not (self.dt is None) and type(self.dt) != bool and self.dt > 0: + # If this is a strict discrete time system, print the sampling time + if self.isdtime(strict=True): outstr += "\ndt = " + str(self.dt) + "\n" return outstr @@ -1554,7 +1558,8 @@ def tf(*args, **kwargs): Polynomial coefficients of the denominator display_format: None, 'poly' or 'zpk' Set the display format used in printing the TransferFunction object. - Default behavior is polynomial display. + Default behavior is polynomial display and can be changed by + changing config.defaults['xferfcn.display_format'].. Returns ------- @@ -1680,7 +1685,8 @@ def zpk(zeros, poles, gain, *args, **kwargs): name is generated with a unique integer id. display_format: None, 'poly' or 'zpk' Set the display format used in printing the TransferFunction object. - Default behavior is zpk display. + Default behavior is polynomial display and can be changed by + changing config.defaults['xferfcn.display_format']. Returns ------- @@ -1690,15 +1696,13 @@ def zpk(zeros, poles, gain, *args, **kwargs): Examples -------- >>> from control import tf - >>> G = zpk([1],[2, 3], gain=1) + >>> G = zpk([1],[2, 3], gain=1, display_format='zpk') >>> G s - 1 --------------- (s - 2) (s - 3) """ num, den = zpk2tf(zeros, poles, gain) - if 'display_format' not in kwargs: - kwargs['display_format'] = 'zpk' return TransferFunction(num, den, *args, **kwargs) From 8101b31992de7ca3b0e385dae381671bbb3429b1 Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Sat, 25 Feb 2023 12:39:09 +0100 Subject: [PATCH 11/14] Update after failed tests --- control/tests/xferfcn_test.py | 2 +- control/xferfcn.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index cd5396c07..b82464a72 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -966,7 +966,7 @@ def test_printing_mimo(self): ]) def test_printing_zpk(self, zeros, poles, gain, output): """Test _tf_polynomial_to_string for constant systems""" - G = zpk(zeros, poles, gain) + G = zpk(zeros, poles, gain, display_format='zpk') res = str(G) assert res == output diff --git a/control/xferfcn.py b/control/xferfcn.py index 1bf0ad875..c97a5004b 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -486,7 +486,7 @@ def __str__(self, var=None): outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n" # If this is a strict discrete time system, print the sampling time - if self.isdtime(strict=True): + if type(self.dt) != bool and self.isdtime(strict=True): outstr += "\ndt = " + str(self.dt) + "\n" return outstr From 8d7ac56eb7a5bc4657de6eae9ff43bb43826f9cd Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Sat, 25 Feb 2023 13:48:32 +0100 Subject: [PATCH 12/14] Add default for xferfcn.floating_point_format --- control/tests/xferfcn_test.py | 29 ++++++++++++++++++++++++++++- control/xferfcn.py | 22 +++++++++++++--------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index b82464a72..a30ed66c2 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -10,7 +10,7 @@ import control as ct from control import StateSpace, TransferFunction, rss, evalfr from control import ss, ss2tf, tf, tf2ss, zpk -from control import isctime, isdtime, sample_system, defaults +from control import isctime, isdtime, sample_system, defaults, reset_defaults from control.statesp import _convert_to_statespace from control.xferfcn import _convert_to_transfer_function from control.tests.conftest import slycotonly, matrixfilter @@ -970,6 +970,33 @@ def test_printing_zpk(self, zeros, poles, gain, output): res = str(G) assert res == output + @pytest.mark.parametrize( + "zeros, poles, gain, format, output", + [([1], [1 + 1j, 1 - 1j], 1, ".2f", + '\n' + ' 1.00\n' + '-------------------------------------\n' + '(s + (1.00-1.41j)) (s + (1.00+1.41j))\n'), + ([1], [1 + 1j, 1 - 1j], 1, ".3f", + '\n' + ' 1.000\n' + '-----------------------------------------\n' + '(s + (1.000-1.414j)) (s + (1.000+1.414j))\n'), + ([1], [1 + 1j, 1 - 1j], 1, ".6g", + '\n' + ' 1\n' + '-------------------------------------\n' + '(s + (1-1.41421j)) (s + (1+1.41421j))\n') + ]) + def test_printing_zpk_format(self, zeros, poles, gain, format, output): + """Test _tf_polynomial_to_string for constant systems""" + defaults['xferfcn.floating_point_format'] = format + G = tf([1], [1,2,3], display_format='zpk') + res = str(G) + reset_defaults() + + assert res == output + @pytest.mark.parametrize( "num, den, output", [([[[11], [21]], [[12], [22]]], diff --git a/control/xferfcn.py b/control/xferfcn.py index c97a5004b..c5c447403 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -71,8 +71,12 @@ # Define module default parameter values _xferfcn_defaults = { 'xferfcn.display_format': 'poly', + 'xferfcn.floating_point_format': '.4g' } +def _float2str(value): + formatter = "{:" + config.defaults.get('xferfcn.floating_point_format', ':.4g') + "}" + return formatter.format(value) class TransferFunction(LTI): """TransferFunction(num, den[, dt]) @@ -1313,7 +1317,7 @@ def _tf_polynomial_to_string(coeffs, var='s'): N = len(coeffs) - 1 for k in range(len(coeffs)): - coefstr = '%.4g' % abs(coeffs[k]) + coefstr = _float2str(abs(coeffs[k])) power = (N - k) if power == 0: if coefstr != '0': @@ -1355,7 +1359,7 @@ def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'): """Convert a factorized polynomial to a string""" if roots.size == 0: - return f"{gain:.4g}" + return _float2str(gain) factors = [] for root in sorted(roots, reverse=True): @@ -1364,29 +1368,29 @@ def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'): factor = f"{var}" factors.append(factor) elif root > 0: - factor = f"{var} - {np.abs(root):.4g}" + factor = f"{var} - {_float2str(np.abs(root))}" factors.append(factor) else: - factor = f"{var} + {np.abs(root):.4g}" + factor = f"{var} + {_float2str(np.abs(root))}" factors.append(factor) elif np.isreal(root * 1j): if root.imag > 0: - factor = f"{var} - {np.abs(root):.4g}j" + factor = f"{var} - {_float2str(np.abs(root))}j" factors.append(factor) else: - factor = f"{var} + {np.abs(root):.4g}j" + factor = f"{var} + {_float2str(np.abs(root))}j" factors.append(factor) else: if root.real > 0: - factor = f"{var} - ({root:.4g})" + factor = f"{var} - ({_float2str(root)})" factors.append(factor) else: - factor = f"{var} + ({-root:.4g})" + factor = f"{var} + ({_float2str(-root)})" factors.append(factor) multiplier = '' if round(gain, 4) != 1.0: - multiplier = f"{gain:.4g} " + multiplier = _float2str(gain) + " " if len(factors) > 1 or multiplier: factors = [f"({factor})" for factor in factors] From 2ca47e107c1d95a107ff13790bcc31d803ccf27f Mon Sep 17 00:00:00 2001 From: Henk van der Laak Date: Sat, 25 Feb 2023 14:34:29 +0100 Subject: [PATCH 13/14] Fix improper way of setting defaults in test --- control/tests/xferfcn_test.py | 6 ++++-- control/xferfcn.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index a30ed66c2..ebb781b89 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -10,7 +10,8 @@ import control as ct from control import StateSpace, TransferFunction, rss, evalfr from control import ss, ss2tf, tf, tf2ss, zpk -from control import isctime, isdtime, sample_system, defaults, reset_defaults +from control import isctime, isdtime, sample_system +from control import defaults, reset_defaults, set_defaults from control.statesp import _convert_to_statespace from control.xferfcn import _convert_to_transfer_function from control.tests.conftest import slycotonly, matrixfilter @@ -990,8 +991,9 @@ def test_printing_zpk(self, zeros, poles, gain, output): ]) def test_printing_zpk_format(self, zeros, poles, gain, format, output): """Test _tf_polynomial_to_string for constant systems""" - defaults['xferfcn.floating_point_format'] = format G = tf([1], [1,2,3], display_format='zpk') + + set_defaults('xferfcn', floating_point_format=format) res = str(G) reset_defaults() diff --git a/control/xferfcn.py b/control/xferfcn.py index c5c447403..093f3654d 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -78,6 +78,7 @@ def _float2str(value): formatter = "{:" + config.defaults.get('xferfcn.floating_point_format', ':.4g') + "}" return formatter.format(value) + class TransferFunction(LTI): """TransferFunction(num, den[, dt]) From 693397339aae50f90329ee89e50b4a21d797014d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 25 Mar 2023 08:53:13 -0700 Subject: [PATCH 14/14] update xferfcn._float2str to be more pythonic per @ilayn --- control/xferfcn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 093f3654d..6edb26858 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -75,8 +75,8 @@ } def _float2str(value): - formatter = "{:" + config.defaults.get('xferfcn.floating_point_format', ':.4g') + "}" - return formatter.format(value) + _num_format = config.defaults.get('xferfcn.floating_point_format', ':.4g') + return f"{value:{_num_format}}" class TransferFunction(LTI):