Repository: python-control/python-control
Pull request #811 (2022-12-10 10:17:43)

Enable scalar division of state-space objects
I think this is a reasonable if minor improvement; it lets one write `gscaled = g / 2` instead of `gscaled = g * (1/2)`.  I'm less sure about division by arrays, but I see multiplication by arrays is allowed, and it seems natural to have symmetry between these operations.

I added a simple test for `StateSpace / scalar` and `StateSpace / array`, but not for `InputOutputSystem / scalar`. etc.

I also modified the type-conversion tests as required.


Patch:
diff --git a/control/frdata.py b/control/frdata.py
index a33775af..c78607a0 100644
--- a/control/frdata.py
+++ b/control/frdata.py
@@ -408,10 +408,6 @@ class FrequencyResponseData(LTI):
                    smooth=(self.ifunc is not None) and
                           (other.ifunc is not None))
 
-    # TODO: Remove when transition to python3 complete
-    def __div__(self, other):
-        return self.__truediv__(other)
-
     # TODO: Division of MIMO transfer function objects is not written yet.
     def __rtruediv__(self, other):
         """Right divide two LTI objects."""
@@ -429,10 +425,6 @@ class FrequencyResponseData(LTI):
 
         return other / self
 
-    # TODO: Remove when transition to python3 complete
-    def __rdiv__(self, other):
-        return self.__rtruediv__(other)
-
     def __pow__(self, other):
         if not type(other) == int:
             raise ValueError("Exponent must be an integer")
diff --git a/control/iosys.py b/control/iosys.py
index cbbd4cec..67cca74d 100644
--- a/control/iosys.py
+++ b/control/iosys.py
@@ -346,6 +346,19 @@ class InputOutputSystem(NamedIOSystem):
         # Return the newly created system
         return newsys
 
+    def __truediv__(sys2, sys1):
+        """Division of input/output systems
+
+        Only division by scalars and arrays of scalars is supported"""
+        # Note: order of arguments is flipped so that self = sys2,
+        # corresponding to the ordering convention of sys2 * sys1
+
+        if not isinstance(sys1, (LTI, NamedIOSystem)):
+            return sys2 * (1/sys1)
+        else:
+            return NotImplemented
+
+
     # Update parameters used for _rhs, _out (used by subclasses)
     def _update_params(self, params, warning=False):
         if warning:
diff --git a/control/statesp.py b/control/statesp.py
index 7843cb33..249c6f5e 100644
--- a/control/statesp.py
+++ b/control/statesp.py
@@ -64,7 +64,7 @@ from .exception import ControlSlycot
 from .frdata import FrequencyResponseData
 from .lti import LTI, _process_frequency_response
 from .namedio import common_timebase, isdtime, _process_namedio_keywords, \
-    _process_dt_keyword
+    _process_dt_keyword, NamedIOSystem
 from . import config
 from copy import deepcopy
 
@@ -794,17 +794,18 @@ class StateSpace(LTI):
             pass
         raise TypeError("can't interconnect systems")
 
-    # TODO: __div__ and __rdiv__ are not written yet.
-    def __div__(self, other):
-        """Divide two LTI systems."""
+    # TODO: general __truediv__, and  __rtruediv__; requires descriptor system support
+    def __truediv__(self, other):
+        """Division of StateSpace systems
 
-        raise NotImplementedError("StateSpace.__div__ is not implemented yet.")
-
-    def __rdiv__(self, other):
-        """Right divide two LTI systems."""
+        Only division by TFs, FRDs, scalars, and arrays of scalars is
+        supported.
+        """
+        if not isinstance(other, (LTI, NamedIOSystem)):
+            return self * (1/other)
+        else:
+            return NotImplemented
 
-        raise NotImplementedError(
-            "StateSpace.__rdiv__ is not implemented yet.")
 
     def __call__(self, x, squeeze=None, warn_infinite=True):
         """Evaluate system's transfer function at complex frequency.
diff --git a/control/xferfcn.py b/control/xferfcn.py
index 84188a63..a27b4662 100644
--- a/control/xferfcn.py
+++ b/control/xferfcn.py
@@ -702,10 +702,6 @@ class TransferFunction(LTI):
 
         return TransferFunction(num, den, dt)
 
-    # TODO: Remove when transition to python3 complete
-    def __div__(self, other):
-        return TransferFunction.__truediv__(self, other)
-
     # TODO: Division of MIMO transfer function objects is not written yet.
     def __rtruediv__(self, other):
         """Right divide two LTI objects."""
@@ -724,10 +720,6 @@ class TransferFunction(LTI):
 
         return other / self
 
-    # TODO: Remove when transition to python3 complete
-    def __rdiv__(self, other):
-        return TransferFunction.__rtruediv__(self, other)
-
     def __pow__(self, other):
         if not type(other) == int:
             raise ValueError("Exponent must be an integer")


Test patch:
diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py
index 41f0c893..fa837f30 100644
--- a/control/tests/statesp_test.py
+++ b/control/tests/statesp_test.py
@@ -333,6 +333,17 @@ class TestStateSpace:
         np.testing.assert_array_almost_equal(sys.C, C)
         np.testing.assert_array_almost_equal(sys.D, D)
 
+    @pytest.mark.parametrize("k", [2, -3.141, np.float32(2.718), np.array([[4.321], [5.678]])])
+    def test_truediv_ss_scalar(self, sys322, k):
+        """Divide SS by scalar."""
+        sys = sys322 / k
+        syscheck = sys322 * (1/k)
+
+        np.testing.assert_array_almost_equal(sys.A, syscheck.A)
+        np.testing.assert_array_almost_equal(sys.B, syscheck.B)
+        np.testing.assert_array_almost_equal(sys.C, syscheck.C)
+        np.testing.assert_array_almost_equal(sys.D, syscheck.D)
+
     @pytest.mark.parametrize("omega, resp",
                              [(1.,
                                np.array([[ 4.37636761e-05-0.01522976j,
diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py
index 7163f709..0deb68f8 100644
--- a/control/tests/type_conversion_test.py
+++ b/control/tests/type_conversion_test.py
@@ -88,11 +88,11 @@ conversion_table = [
     ('mul',     'flt', ['ss',  'tf',  'frd', 'lio', 'ios', 'arr', 'flt']),
     
     # op        left     ss     tf    frd    lio    ios    arr    flt
-    ('truediv', 'ss',  ['xs',  'tf',  'frd', 'xio', 'xos', 'xs',  'xs' ]),
+    ('truediv', 'ss',  ['xs',  'tf',  'frd', 'xio', 'xos', 'ss',  'ss' ]),
     ('truediv', 'tf',  ['tf',  'tf',  'xrd', 'tf',  'xos', 'tf',  'tf' ]),
     ('truediv', 'frd', ['frd', 'frd', 'frd', 'frd', 'E',   'frd', 'frd']),
-    ('truediv', 'lio', ['xio', 'tf',  'frd', 'xio', 'xio', 'xio', 'xio']),
-    ('truediv', 'ios', ['xos', 'xos', 'E',   'xos', 'xos'  'xos', 'xos']),
+    ('truediv', 'lio', ['xio', 'tf',  'frd', 'xio', 'xio', 'lio', 'lio']),
+    ('truediv', 'ios', ['xos', 'xos', 'E',   'xos', 'xos', 'ios', 'ios']),
     ('truediv', 'arr', ['xs',  'tf',  'frd', 'xio', 'xos', 'arr', 'arr']),
     ('truediv', 'flt', ['xs',  'tf',  'frd', 'xio', 'xos', 'arr', 'flt'])]