From 8c9e807ec64c62cde1dc6e174580099dfac8afd9 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Fri, 19 Mar 2021 23:44:48 +0100 Subject: [PATCH 1/2] discard zero imaginary part for sys.dcgain() --- control/lti.py | 7 +++++++ control/statesp.py | 25 ++++++++++++++++++------- control/tests/freqresp_test.py | 12 ++++++------ control/tests/statesp_test.py | 7 +++---- control/tests/xferfcn_test.py | 6 +++--- control/xferfcn.py | 15 +++++++++++---- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/control/lti.py b/control/lti.py index 30569863a..01d04e020 100644 --- a/control/lti.py +++ b/control/lti.py @@ -208,6 +208,13 @@ def dcgain(self): raise NotImplementedError("dcgain not implemented for %s objects" % str(self.__class__)) + def _dcgain(self, warn_infinite): + zeroresp = self(0 if self.isctime() else 1, + warn_infinite=warn_infinite) + if np.all(np.logical_or(np.isreal(zeroresp), np.isnan(zeroresp.imag))): + return zeroresp.real + else: + return zeroresp # Test to see if a system is SISO def issiso(sys, strict=False): diff --git a/control/statesp.py b/control/statesp.py index d2b613024..c75e6f66a 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1216,16 +1216,27 @@ def dcgain(self, warn_infinite=False): .. math: G(1) = C (I - A)^{-1} B + D + Parameters + ---------- + warn_infinite : bool, optional + By default, don't issue a warning message if the zero-frequency + gain is infinite. Setting `warn_infinite` to generate the warning + message. + Returns ------- - gain : ndarray - An array of shape (outputs,inputs); the array will either be the - zero-frequency (or DC) gain, or, if the frequency response is - singular, the array will be filled with (inf + nanj). - + gain : (outputs, inputs) ndarray or scalar + Array or scalar value for SISO systems, depending on + config.defaults['control.squeeze_frequency_response']. + The value of the array elements or the scalar is either the + zero-frequency (or DC) gain, or `inf`, if the frequency response + is singular. + + For real valued systems, the empty imaginary part of the + complex zero-frequency response is discarded and a real array or + scalar is returned. """ - return self(0, warn_infinite=warn_infinite) if self.isctime() \ - else self(1, warn_infinite=warn_infinite) + return self._dcgain(warn_infinite) def _isstatic(self): """True if and only if the system has no dynamics, that is, diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 983330af0..2ef426151 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -423,7 +423,7 @@ def test_dcgain_consistency(): sys_ss = ctrl.tf2ss(sys_tf) assert 0 in sys_ss.pole() - # Finite (real) numerator over 0 denominator => inf + nanj + # Finite (real) numerator over 0 denominator => inf np.testing.assert_equal( sys_tf(0, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( @@ -433,9 +433,9 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( - sys_tf.dcgain(warn_infinite=False), complex(np.inf, np.nan)) + sys_tf.dcgain(warn_infinite=False), np.inf) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), complex(np.inf, np.nan)) + sys_ss.dcgain(warn_infinite=False), np.inf) # Set up transfer function with pole, zero at the origin sys_tf = ctrl.tf([1, 0], [1, 0]) @@ -448,7 +448,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_tf(0j, warn_infinite=False), complex(np.nan, np.nan)) np.testing.assert_equal( - sys_tf.dcgain(warn_infinite=False), complex(np.nan, np.nan)) + sys_tf.dcgain(warn_infinite=False), np.nan) # Set up state space version sys_ss = ctrl.tf2ss(ctrl.tf([1, 0], [1, 1])) * \ @@ -462,7 +462,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.nan, np.nan)) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), complex(np.nan, np.nan)) + sys_ss.dcgain(warn_infinite=False), np.nan) elif 0 in sys_ss.pole(): # Pole at the origin, but zero elsewhere => should get (inf + nanj) np.testing.assert_equal( @@ -470,7 +470,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), complex(np.inf, np.nan)) + sys_ss.dcgain(warn_infinite=False), np.inf) else: # Near pole/zero cancellation => nothing sensible to check pass diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 983b9d7a6..6a7509001 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -498,7 +498,7 @@ def test_dc_gain_cont(self): np.testing.assert_allclose(sys2.dcgain(), expected) sys3 = StateSpace(0., 1., 1., 0.) - np.testing.assert_equal(sys3.dcgain(), complex(np.inf, np.nan)) + np.testing.assert_equal(sys3.dcgain(), np.inf) def test_dc_gain_discr(self): """Test DC gain for discrete-time state-space systems.""" @@ -516,7 +516,7 @@ def test_dc_gain_discr(self): # summer sys = StateSpace(1, 1, 1, 0, True) - np.testing.assert_equal(sys.dcgain(), complex(np.inf, np.nan)) + np.testing.assert_equal(sys.dcgain(), np.inf) @pytest.mark.parametrize("outputs", range(1, 6)) @pytest.mark.parametrize("inputs", range(1, 6)) @@ -539,7 +539,7 @@ def test_dc_gain_integrator(self, outputs, inputs, dt): c = np.eye(max(outputs, states))[:outputs, :states] d = np.zeros((outputs, inputs)) sys = StateSpace(a, b, c, d, dt) - dc = np.full_like(d, complex(np.inf, np.nan), dtype=complex) + dc = np.full_like(d, np.inf, dtype=float) if sys.issiso(): dc = dc.squeeze() @@ -953,4 +953,3 @@ def test_xferfcn_ndarray_precedence(op, tf, arr): ss = ct.tf2ss(tf) result = op(arr, ss) assert isinstance(result, ct.StateSpace) - diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index b892655e9..06e7fc9d8 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -807,7 +807,7 @@ def test_dcgain_cont(self): np.testing.assert_equal(sys2.dcgain(), 2) sys3 = TransferFunction(6, [1, 0]) - np.testing.assert_equal(sys3.dcgain(), complex(np.inf, np.nan)) + np.testing.assert_equal(sys3.dcgain(), np.inf) num = [[[15], [21], [33]], [[10], [14], [22]]] den = [[[1, 3], [2, 3], [3, 3]], [[1, 5], [2, 7], [3, 11]]] @@ -827,13 +827,13 @@ def test_dcgain_discr(self): # differencer sys = TransferFunction(1, [1, -1], True) - np.testing.assert_equal(sys.dcgain(), complex(np.inf, np.nan)) + np.testing.assert_equal(sys.dcgain(), np.inf) # differencer, with warning sys = TransferFunction(1, [1, -1], True) with pytest.warns(RuntimeWarning, match="divide by zero"): np.testing.assert_equal( - sys.dcgain(warn_infinite=True), complex(np.inf, np.nan)) + sys.dcgain(warn_infinite=True), np.inf) # summer sys = TransferFunction([1, -1], [1], True) diff --git a/control/xferfcn.py b/control/xferfcn.py index 50e4870a8..3e48c7f24 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1070,12 +1070,19 @@ def dcgain(self, warn_infinite=False): Returns ------- - gain : ndarray - The zero-frequency gain + gain : (outputs, inputs) ndarray or scalar + Array or scalar value for SISO systems, depending on + config.defaults['control.squeeze_frequency_response']. + The value of the array elements or the scalar is either the + zero-frequency (or DC) gain, or `inf`, if the frequency response + is singular. + + For real valued systems, the empty imaginary part of the + complex zero-frequency response is discarded and a real array or + scalar is returned. """ - return self(0, warn_infinite=warn_infinite) if self.isctime() \ - else self(1, warn_infinite=warn_infinite) + return self._dcgain(warn_infinite) def _isstatic(self): """returns True if and only if all of the numerator and denominator From 9a54254eec86d99aacbf3df30e693c139dc2af1c Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 20 Mar 2021 00:55:28 +0100 Subject: [PATCH 2/2] Apply review suggestions by @murrayrm: revert comment change, remove parameter --- control/tests/freqresp_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 2ef426151..321580ba7 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -423,7 +423,7 @@ def test_dcgain_consistency(): sys_ss = ctrl.tf2ss(sys_tf) assert 0 in sys_ss.pole() - # Finite (real) numerator over 0 denominator => inf + # Finite (real) numerator over 0 denominator => inf + nanj np.testing.assert_equal( sys_tf(0, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( @@ -433,9 +433,9 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( - sys_tf.dcgain(warn_infinite=False), np.inf) + sys_tf.dcgain(), np.inf) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), np.inf) + sys_ss.dcgain(), np.inf) # Set up transfer function with pole, zero at the origin sys_tf = ctrl.tf([1, 0], [1, 0]) @@ -448,7 +448,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_tf(0j, warn_infinite=False), complex(np.nan, np.nan)) np.testing.assert_equal( - sys_tf.dcgain(warn_infinite=False), np.nan) + sys_tf.dcgain(), np.nan) # Set up state space version sys_ss = ctrl.tf2ss(ctrl.tf([1, 0], [1, 1])) * \ @@ -462,7 +462,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.nan, np.nan)) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), np.nan) + sys_ss.dcgain(), np.nan) elif 0 in sys_ss.pole(): # Pole at the origin, but zero elsewhere => should get (inf + nanj) np.testing.assert_equal( @@ -470,7 +470,7 @@ def test_dcgain_consistency(): np.testing.assert_equal( sys_ss(0j, warn_infinite=False), complex(np.inf, np.nan)) np.testing.assert_equal( - sys_ss.dcgain(warn_infinite=False), np.inf) + sys_ss.dcgain(), np.inf) else: # Near pole/zero cancellation => nothing sensible to check pass