Hello community,

here is the log from the commit of package python-control for openSUSE:Factory 
checked in at 2019-12-04 13:53:54
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-control (Old)
 and      /work/SRC/openSUSE:Factory/.python-control.new.4691 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-control"

Wed Dec  4 13:53:54 2019 rev:2 rq:753327 version:0.8.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-control/python-control.changes    
2019-11-06 14:07:07.736850276 +0100
+++ /work/SRC/openSUSE:Factory/.python-control.new.4691/python-control.changes  
2019-12-04 14:20:23.994439139 +0100
@@ -1,0 +2,6 @@
+Wed Nov 27 18:13:20 UTC 2019 - Benjamin Greiner <[email protected]>
+
+- python-control-pr345.patch: PR#345 to fix fails on some
+  architectures because of machine precision
+
+-------------------------------------------------------------------

New:
----
  python-control-pr345.patch

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-control.spec ++++++
--- /var/tmp/diff_new_pack.EbMgiB/_old  2019-12-04 14:20:24.522439583 +0100
+++ /var/tmp/diff_new_pack.EbMgiB/_new  2019-12-04 14:20:24.526439587 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-control
 #
-# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2019 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -26,6 +26,7 @@
 Source:         
https://files.pythonhosted.org/packages/source/c/control/control-%{version}.tar.gz
 Patch0:         python-control-fixtestaugw.patch
 Patch1:         python-control-pr317.patch
+Patch2:         python-control-pr345.patch
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
@@ -54,6 +55,7 @@
 %setup -q -n control-%{version}
 %patch0 -p1
 %patch1 -p1
+%patch2 -p1
 
 %build
 %python_build

++++++ python-control-pr345.patch ++++++
diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py
index 35e411b..ee779f9 100644
--- a/control/tests/xferfcn_test.py
+++ b/control/tests/xferfcn_test.py
@@ -403,8 +403,57 @@ class TestXferFcn(unittest.TestCase):
         np.testing.assert_array_equal(omega, true_omega)
 
     # Tests for TransferFunction.pole and TransferFunction.zero.
-    
-    @unittest.skipIf(not slycot_check(), "slycot not installed")
+
+    def test_common_den(self):
+        """ Test the helper function to compute common denomitators."""
+
+        # _common_den() computes the common denominator per input/column.
+        # The testing columns are:
+        # 0: no common poles
+        # 1: regular common poles
+        # 2: poles with multiplicity,
+        # 3: complex poles
+        # 4: complex poles below threshold
+
+        eps = np.finfo(float).eps
+        tol_imag = np.sqrt(eps*5*2*2)*0.9
+
+        numin = [[[1.], [1.], [1.], [1.], [1.]],
+                 [[1.], [1.], [1.], [1.], [1.]]]
+        denin = [[[1., 3., 2.],          # 0: poles: [-1, -2]
+                  [1., 6., 11., 6.],     # 1: poles: [-1, -2, -3]
+                  [1., 6., 11., 6.],     # 2: poles: [-1, -2, -3]
+                  [1., 6., 11., 6.],     # 3: poles: [-1, -2, -3]
+                  [1., 6., 11., 6.]],    # 4: poles: [-1, -2, -3],
+                 [[1., 12., 47., 60.],   # 0: poles: [-3, -4, -5]
+                  [1., 9., 26., 24.],    # 1: poles: [-2, -3, -4]
+                  [1., 7., 16., 12.],    # 2: poles: [-2, -2, -3]
+                  [1., 7., 17., 15.],    # 3: poles: [-2+1J, -2-1J, -3],
+                  np.poly([-2 + tol_imag * 1J, -2 - tol_imag * 1J, -3])]]
+        numref = np.array([
+                [[0.,  0.,  1., 12., 47., 60.],
+                 [0.,  0.,  0.,  1.,  4.,  0.],
+                 [0.,  0.,  0.,  1.,  2.,  0.],
+                 [0.,  0.,  0.,  1.,  4.,  5.],
+                 [0.,  0.,  0.,  1.,  2.,  0.]],
+                [[0.,  0.,  0.,  1.,  3.,  2.],
+                 [0.,  0.,  0.,  1.,  1.,  0.],
+                 [0.,  0.,  0.,  1.,  1.,  0.],
+                 [0.,  0.,  0.,  1.,  3.,  2.],
+                 [0.,  0.,  0.,  1.,  1.,  0.]]])
+        denref = np.array(
+                [[1., 15., 85., 225., 274., 120.],
+                 [1., 10., 35., 50., 24.,  0.],
+                 [1.,  8., 23., 28., 12.,  0.],
+                 [1., 10., 40., 80., 79., 30.],
+                 [1.,  8., 23., 28., 12.,  0.]])
+        sys = TransferFunction(numin, denin)
+        num, den, denorder = sys._common_den()
+        np.testing.assert_array_almost_equal(num[:2, :, :], numref)
+        np.testing.assert_array_almost_equal(num[2:, :, :],
+                                             np.zeros((3, 5, 6)))
+        np.testing.assert_array_almost_equal(den, denref)
+
     def test_pole_mimo(self):
         """Test for correct MIMO poles."""
 
@@ -414,13 +463,12 @@ class TestXferFcn(unittest.TestCase):
 
         np.testing.assert_array_almost_equal(p, [-2., -2., -7., -3., -2.])
 
-    @unittest.skipIf(not slycot_check(), "slycot not installed")
     def test_double_cancelling_poles_siso(self):
-        
+
         H = TransferFunction([1, 1], [1, 2, 1])
         p = H.pole()
         np.testing.assert_array_almost_equal(p, [-1, -1])
-    
+
     # Tests for TransferFunction.feedback
     def test_feedback_siso(self):
         """Test for correct SISO transfer function feedback."""
diff --git a/control/xferfcn.py b/control/xferfcn.py
index 1ef0661..a913061 100644
--- a/control/xferfcn.py
+++ b/control/xferfcn.py
@@ -57,7 +57,6 @@ from numpy import angle, array, empty, finfo, ndarray, ones, \
     polyadd, polymul, polyval, roots, sqrt, zeros, squeeze, exp, pi, \
     where, delete, real, poly, nonzero
 import scipy as sp
-from numpy.polynomial.polynomial import polyfromroots
 from scipy.signal import lti, tf2zpk, zpk2tf, cont2discrete
 from copy import deepcopy
 from warnings import warn
@@ -774,8 +773,6 @@ class TransferFunction(LTI):
         output numerator array num is modified to use the common
         denominator for this input/column; the coefficient arrays are also
         padded with zeros to be the same size for all num/den.
-        num is an sys.outputs by sys.inputs
-        by len(d) array.
 
         Parameters
         ----------
@@ -786,17 +783,20 @@ class TransferFunction(LTI):
         Returns
         -------
         num: array
-            Multi-dimensional array of numerator coefficients. num[i][j]
-            gives the numerator coefficient array for the ith input and jth
-            output, also prepared for use in td04ad; matches the denorder
-            order; highest coefficient starts on the left.
+            n by n by kd where n = max(sys.outputs,sys.inputs)
+                              kd = max(denorder)+1
+            Multi-dimensional array of numerator coefficients. num[i,j]
+            gives the numerator coefficient array for the ith output and jth
+            input; padded for use in td04ad ('C' option); matches the
+            denorder order; highest coefficient starts on the left.
 
         den: array
+            sys.inputs by kd
             Multi-dimensional array of coefficients for common denominator
             polynomial, one row per input. The array is prepared for use in
             slycot td04ad, the first element is the highest-order polynomial
-            coefficiend of s, matching the order in denorder, if denorder <
-            number of columns in den, the den is padded with zeros
+            coefficient of s, matching the order in denorder. If denorder <
+            number of columns in den, the den is padded with zeros.
 
         denorder: array of int, orders of den, one per input
 
@@ -810,16 +810,18 @@ class TransferFunction(LTI):
 
         # Machine precision for floats.
         eps = finfo(float).eps
+        real_tol = sqrt(eps * self.inputs * self.outputs)
 
-        # Decide on the tolerance to use in deciding of a pole is complex
+        # The tolerance to use in deciding if a pole is complex
         if (imag_tol is None):
-            imag_tol = 1e-8     # TODO: figure out the right number to use
+            imag_tol = 2 * real_tol
 
         # A list to keep track of cumulative poles found as we scan
         # self.den[..][..]
         poles = [[] for j in range(self.inputs)]
 
         # RvP, new implementation 180526, issue #194
+        # BG, modification, issue #343, PR #354
 
         # pre-calculate the poles for all num, den
         # has zeros, poles, gain, list for pole indices not in den,
@@ -838,30 +840,37 @@ class TransferFunction(LTI):
                     poleset[-1].append([z, p, k, [], 0])
 
         # collect all individual poles
-        epsnm = eps * self.inputs * self.outputs
         for j in range(self.inputs):
             for i in range(self.outputs):
                 currentpoles = poleset[i][j][1]
                 nothave = ones(currentpoles.shape, dtype=bool)
                 for ip, p in enumerate(poles[j]):
-                    idx, = nonzero(
-                        (abs(currentpoles - p) < epsnm) * nothave)
-                    if len(idx):
-                        nothave[idx[0]] = False
+                    collect = (np.isclose(currentpoles.real, p.real,
+                                          atol=real_tol) &
+                               np.isclose(currentpoles.imag, p.imag,
+                                          atol=imag_tol) &
+                               nothave)
+                    if np.any(collect):
+                        # mark first found pole as already collected
+                        nothave[nonzero(collect)[0][0]] = False
                     else:
                         # remember id of pole not in tf
                         poleset[i][j][3].append(ip)
                 for h, c in zip(nothave, currentpoles):
                     if h:
+                        if abs(c.imag) < imag_tol:
+                            c = c.real
                         poles[j].append(c)
                 # remember how many poles now known
                 poleset[i][j][4] = len(poles[j])
 
         # figure out maximum number of poles, for sizing the den
-        npmax = max([len(p) for p in poles])
-        den = zeros((self.inputs, npmax + 1), dtype=float)
+        maxindex = max([len(p) for p in poles])
+        den = zeros((self.inputs, maxindex + 1), dtype=float)
         num = zeros((max(1, self.outputs, self.inputs),
-                     max(1, self.outputs, self.inputs), npmax + 1), 
dtype=float)
+                     max(1, self.outputs, self.inputs),
+                     maxindex + 1),
+                    dtype=float)
         denorder = zeros((self.inputs,), dtype=int)
 
         for j in range(self.inputs):
@@ -872,11 +881,10 @@ class TransferFunction(LTI):
                     num[i, j, 0] = poleset[i][j][2]
             else:
                 # create the denominator matching this input
-                # polyfromroots gives coeffs in opposite order from what we use
-                # coefficients should be padded on right, ending at np
-                np = len(poles[j])
-                den[j, np::-1] = polyfromroots(poles[j]).real
-                denorder[j] = np
+                # coefficients should be padded on right, ending at maxindex
+                maxindex = len(poles[j])
+                den[j, :maxindex+1] = poly(poles[j])
+                denorder[j] = maxindex
 
                 # now create the numerator, also padded on the right
                 for i in range(self.outputs):
@@ -885,22 +893,15 @@ class TransferFunction(LTI):
                     # add all poles not found in the original denominator,
                     # and the ones later added from other denominators
                     for ip in chain(poleset[i][j][3],
-                                    range(poleset[i][j][4], np)):
+                                    range(poleset[i][j][4], maxindex)):
                         nwzeros.append(poles[j][ip])
 
-                    numpoly = poleset[i][j][2] * polyfromroots(nwzeros).real
-                    # print(numpoly, den[j])
-                    # polyfromroots gives coeffs in opposite order => invert
+                    numpoly = poleset[i][j][2] * np.atleast_1d(poly(nwzeros))
                     # numerator polynomial should be padded on left and right
-                    #   ending at np to line up with what td04ad expects...
-                    num[i, j, np + 1 - len(numpoly):np + 1] = numpoly[::-1]
+                    #   ending at maxindex to line up with what td04ad expects.
+                    num[i, j, maxindex+1-len(numpoly):maxindex+1] = numpoly
                     # print(num[i, j])
 
-        if (abs(den.imag) > epsnm).any():
-            print("Warning: The denominator has a nontrivial imaginary part: 
%f"
-                  % abs(den.imag).max())
-        den = den.real
-
         return num, den, denorder
 
     def sample(self, Ts, method='zoh', alpha=None):

Reply via email to