Repository: python-control/python-control

Issue:
Casting complex number to float
Code to reproduce the problem:
```python3
sys = ctrl.zpk([], [1j], 1)

plt.figure(figsize=(8, 6))
ctrl.root_locus(sys, color='red') #<-- Problem is here

plt.xlabel('Real Axis (σ)')
plt.ylabel('Imaginary Axis (jω)')
plt.grid(True)
plt.show()
```

In the diagram, you can see the pole is placed at the wrong location. but I looked into it and found a quick solution (though I am not familiar with the code, and it might be deeper than I found)

in the files xferfcn.py (lines 1068, and 1072) there are two arrays that are created of type `float`, that could later assigned to complex numbers if the input is of type `complex`. the fix is to just change the type of the array to `complex`

Then another issue arises in plocus.py (line 267), if the coefficients are complex, it is again casted into a float. I am not sure what this value is used for, but I believe this can be fixed by casting the input type to complex if it isn't already, then using the magnitude of the complex number instead of casting it, which I believe the abs() function will do for default python `complex` types.

Should be an easy fix, hope this helps


Discussion:
Thanks for the report.  Supporting complex coefficients for TFs, and equivalently complex matrices for SS, is brought up now and then.  It's unfortunately not such an easy change: see gh-371.
ah I see now, Might be worth looking into adding type checking to throw errors for such a case since the `complex` type is a default type in python, and the client is likely going to try to use it.



Patch:
diff --git a/control/xferfcn.py b/control/xferfcn.py
index 16d7c5054..02ba72df4 100644
--- a/control/xferfcn.py
+++ b/control/xferfcn.py
@@ -1954,6 +1954,7 @@ def _clean_part(data, name="<unknown>"):
 
     """
     valid_types = (int, float, complex, np.number)
+    unsupported_types = (complex, np.complexfloating)
     valid_collection = (list, tuple, ndarray)
 
     if isinstance(data, np.ndarray) and data.ndim == 2 and \
@@ -1998,8 +1999,11 @@ def _clean_part(data, name="<unknown>"):
     for i in range(out.shape[0]):
         for j in range(out.shape[1]):
             for k in range(len(out[i, j])):
-                if isinstance(out[i, j][k], (int, np.int32, np.int64)):
+                if isinstance(out[i, j][k], (int, np.integer)):
                     out[i, j][k] = float(out[i, j][k])
+                elif isinstance(out[i, j][k], unsupported_types):
+                    raise TypeError(
+                        f"unsupported data type: {type(out[i, j][k])}")
     return out
 
 
diff --git a/doc/linear.rst b/doc/linear.rst
index b7b8f7137..a9960feca 100644
--- a/doc/linear.rst
+++ b/doc/linear.rst
@@ -39,7 +39,7 @@ of linear time-invariant (LTI) systems:
   y &= C x + D u
 
 where :math:`u` is the input, :math:`y` is the output, and :math:`x`
-is the state.
+is the state.  All vectors and matrices must be real-valued.
 
 To create a state space system, use the :func:`ss` function:
 
@@ -94,7 +94,8 @@ transfer functions
               {b_0 s^n + b_1 s^{n-1} + \cdots + b_n},
 
 where :math:`n` is greater than or equal to :math:`m` for a proper
-transfer function.  Improper transfer functions are also allowed.
+transfer function.  Improper transfer functions are also allowed.  All
+coefficients must be real-valued.
 
 To create a transfer function, use the :func:`tf` function::
 
diff --git a/examples/cruise.ipynb b/examples/cruise.ipynb
index 16935b15e..08a1583ac 100644
--- a/examples/cruise.ipynb
+++ b/examples/cruise.ipynb
@@ -420,8 +420,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "system: a =  (0.010124405669387215-0j) , b =  (1.3203061238159202+0j)\n",
-      "pzcancel: kp = 0.5 , ki = (0.005062202834693608+0j) , 1/(kp b) =  (1.5148002148317266+0j)\n",
+      "system: a =  0.010124405669387215 , b =  1.3203061238159202\n",
+      "pzcancel: kp = 0.5 , ki = 0.005062202834693608 , 1/(kp b) =  1.5148002148317266\n",
       "sfb_int: K =  0.5 , ki = 0.1\n"
      ]
     },
@@ -442,7 +442,7 @@
     "\n",
     "# Construction a controller that cancels the pole\n",
     "kp = 0.5\n",
-    "a = -P.poles()[0]\n",
+    "a = -P.poles()[0].real\n",
     "b = np.real(P(0)) * a\n",
     "ki = a * kp\n",
     "control_pz = ct.TransferFunction(\n",


Test patch:
diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py
index e3d78bbdd..3c1411f04 100644
--- a/control/tests/statesp_test.py
+++ b/control/tests/statesp_test.py
@@ -136,6 +136,7 @@ def test_constructor(self, sys322ABCD, dt, argfun):
          ((np.ones((3, 3)), np.ones((3, 2)),
            np.ones((2, 3)), np.ones((2, 3))), ValueError,
           r"Incompatible dimensions of D matrix; expected \(2, 2\)"),
+         (([1j], 2, 3, 0), TypeError, "real number, not 'complex'"),
         ])
     def test_constructor_invalid(self, args, exc, errmsg):
         """Test invalid input to StateSpace() constructor"""
diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py
index 87c852395..d3db08ef6 100644
--- a/control/tests/xferfcn_test.py
+++ b/control/tests/xferfcn_test.py
@@ -49,6 +49,10 @@ def test_constructor_bad_input_type(self):
                               [[4, 5], [6, 7]]],
                              [[[6, 7], [4, 5]],
                               [[2, 3]]])
+
+        with pytest.raises(TypeError, match="unsupported data type"):
+            ct.tf([1j], [1, 2, 3])
+
         # good input
         TransferFunction([[[0, 1], [2, 3]],
                           [[4, 5], [6, 7]]],