Skip to content

Commit 483688e

Browse files
marko1olocodex
andcommitted
fix(xferfcn): normalize real zpk coefficients
Normalize floating coefficient arrays after zpk2tf so complex dtype zero/pole inputs that produce real polynomial coefficients do not leave float32 or tiny imaginary residue in TransferFunction internals. Continue rejecting actual complex transfer function coefficients. Co-authored-by: OpenAI Codex <codex@openai.com>
1 parent 146ccee commit 483688e

2 files changed

Lines changed: 34 additions & 7 deletions

File tree

control/tests/xferfcn_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def test_constructor_bad_input_type(self):
5252

5353
with pytest.raises(TypeError, match="unsupported data type"):
5454
ct.tf([1j], [1, 2, 3])
55+
for dtype in [np.complex64, np.complex128]:
56+
with pytest.raises(TypeError, match="unsupported data type"):
57+
ct.tf(np.array([1 + 1j], dtype=dtype), [1, 2, 3])
5558

5659
# good input
5760
TransferFunction([[[0, 1], [2, 3]],
@@ -1550,6 +1553,19 @@ def test_zpk(zeros, poles, gain, args, kwargs):
15501553
if kwargs.get('name'):
15511554
assert sys.name == kwargs.get('name')
15521555

1556+
1557+
@pytest.mark.parametrize("dtype", [np.complex64, np.complex128])
1558+
def test_zpk_complex_dtype_real_coefficients(dtype):
1559+
zeros = np.array([1 + 1j, 1 - 1j], dtype=dtype)
1560+
poles = np.array([-1 + 1j, -1 - 1j], dtype=dtype)
1561+
1562+
sys = ct.zpk(zeros, poles, gain=1, dt=0)
1563+
1564+
assert sys.num_array[0, 0].dtype == float
1565+
assert sys.den_array[0, 0].dtype == float
1566+
assert "float32" not in str(sys)
1567+
1568+
15531569
@pytest.mark.parametrize("create, args, kwargs, convert", [
15541570
(StateSpace, ([-1], [1], [1], [0]), {}, ss2tf),
15551571
(StateSpace, ([-1], [1], [1], [0]), {}, ss),

control/xferfcn.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,7 +1960,7 @@ def _clean_part(data, name="<unknown>"):
19601960
if isinstance(data, np.ndarray) and data.ndim == 2 and \
19611961
data.dtype == object and isinstance(data[0, 0], np.ndarray):
19621962
# Data is already in the right format
1963-
return data
1963+
out = data
19641964
elif isinstance(data, ndarray) and data.ndim == 3 and \
19651965
isinstance(data[0, 0, 0], valid_types):
19661966
out = np.empty(data.shape[0:2], dtype=np.ndarray)
@@ -1995,15 +1995,26 @@ def _clean_part(data, name="<unknown>"):
19951995
"The numerator and denominator inputs must be scalars or vectors "
19961996
"(for\nSISO), or lists of lists of vectors (for SISO or MIMO).")
19971997

1998-
# Check for coefficients that are ints and convert to floats
1998+
# Check for real numeric coefficients and normalize floating arrays
19991999
for i in range(out.shape[0]):
20002000
for j in range(out.shape[1]):
2001-
for k in range(len(out[i, j])):
2002-
if isinstance(out[i, j][k], (int, np.integer)):
2003-
out[i, j][k] = float(out[i, j][k])
2004-
elif isinstance(out[i, j][k], unsupported_types):
2001+
coefficients = np.asarray(out[i, j])
2002+
convert_to_float = np.issubdtype(coefficients.dtype, np.floating)
2003+
if np.iscomplexobj(coefficients):
2004+
real_coefficients = coefficients.real
2005+
zero_tol = 1000 * np.finfo(float).eps * max(
2006+
1, np.max(np.abs(real_coefficients)))
2007+
if np.any(np.abs(coefficients.imag) > zero_tol):
20052008
raise TypeError(
2006-
f"unsupported data type: {type(out[i, j][k])}")
2009+
f"unsupported data type: {type(coefficients.flat[0])}")
2010+
coefficients = real_coefficients
2011+
convert_to_float = True
2012+
for k in range(len(coefficients)):
2013+
if isinstance(coefficients[k], unsupported_types):
2014+
raise TypeError(
2015+
f"unsupported data type: {type(coefficients[k])}")
2016+
out[i, j] = np.asarray(
2017+
coefficients, dtype=float) if convert_to_float else coefficients
20072018
return out
20082019

20092020

0 commit comments

Comments
 (0)