Repository: python-control/python-control
Pull request #1029 (2024-07-13 04:06:44)

Fix step_info settling time calculation
This PR addresses issue #1014, where a constant input was generating an error in `step_info`.  Just needed to account for the case where np.where returned an empty array.

Also changed `np.where` to `np.nonzero`, as recommended in the [np.where documentation](https://numpy.org/doc/stable/reference/generated/numpy.where.html).


coveralls: 
[![Coverage Status](https://coveralls.io/builds/68625984/badge)](https://coveralls.io/builds/68625984)

coverage: 94.629% (+0.001%) from 94.628%
when pulling **813c93799197d3e0e4ab37dfadf0844fc8b569d5 on murrayrm:step_info_fix-12Jul2024**
into **6406868cf07b9decb3b22d824676449d70b163da on python-control:main**.


Patch:
diff --git a/control/timeresp.py b/control/timeresp.py
index 244d90c2..f844b1df 100644
--- a/control/timeresp.py
+++ b/control/timeresp.py
@@ -1674,17 +1674,19 @@ def step_info(sysdata, T=None, T_num=None, yfinal=None, params=None,
 
             if not np.isnan(InfValue) and not np.isinf(InfValue):
                 # RiseTime
-                tr_lower_index = np.where(
+                tr_lower_index = np.nonzero(
                     sgnInf * (yout - RiseTimeLimits[0] * InfValue) >= 0
                     )[0][0]
-                tr_upper_index = np.where(
+                tr_upper_index = np.nonzero(
                     sgnInf * (yout - RiseTimeLimits[1] * InfValue) >= 0
                     )[0][0]
                 rise_time = T[tr_upper_index] - T[tr_lower_index]
 
                 # SettlingTime
-                settled = np.where(
-                    np.abs(yout/InfValue-1) >= SettlingTimeThreshold)[0][-1]+1
+                outside_threshold = np.nonzero(
+                    np.abs(yout/InfValue - 1) >= SettlingTimeThreshold)[0]
+                settled = 0 if outside_threshold.size == 0 \
+                    else outside_threshold[-1] + 1
                 # MIMO systems can have unsettled channels without infinite
                 # InfValue
                 if settled < len(T):


Test patch:
diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py
index 73032c0a..e2d93be0 100644
--- a/control/tests/timeresp_test.py
+++ b/control/tests/timeresp_test.py
@@ -1,6 +1,7 @@
 """timeresp_test.py - test time response functions"""
 
 from copy import copy
+from math import isclose
 
 import numpy as np
 import pytest
@@ -8,11 +9,11 @@ import scipy as sp
 
 import control as ct
 from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss
-from control.exception import slycot_check, pandas_check
+from control.exception import pandas_check, slycot_check
 from control.tests.conftest import slycotonly
-from control.timeresp import (_default_time_vector, _ideal_tfinal_and_dt,
-                              forced_response, impulse_response,
-                              initial_response, step_info, step_response)
+from control.timeresp import _default_time_vector, _ideal_tfinal_and_dt, \
+    forced_response, impulse_response, initial_response, step_info, \
+    step_response
 
 
 class TSys:
@@ -1275,3 +1276,45 @@ def test_no_pandas():
     # Convert to pandas
     with pytest.raises(ImportError, match="pandas"):
         df = resp.to_pandas()
+
+
+# https://github.com/python-control/python-control/issues/1014
+def test_step_info_nonstep():
+    # Pass a constant input
+    timepts = np.linspace(0, 10, endpoint=False)
+    y_const = np.ones_like(timepts)
+
+    # Constant value of 1
+    step_info = ct.step_info(y_const, timepts)
+    assert step_info['RiseTime'] == 0
+    assert step_info['SettlingTime'] == 0
+    assert step_info['SettlingMin'] == 1
+    assert step_info['SettlingMax'] == 1
+    assert step_info['Overshoot'] == 0
+    assert step_info['Undershoot'] == 0
+    assert step_info['Peak'] == 1
+    assert step_info['PeakTime'] == 0
+    assert step_info['SteadyStateValue'] == 1
+
+    # Constant value of -1
+    step_info = ct.step_info(-y_const, timepts)
+    assert step_info['RiseTime'] == 0
+    assert step_info['SettlingTime'] == 0
+    assert step_info['SettlingMin'] == -1
+    assert step_info['SettlingMax'] == -1
+    assert step_info['Overshoot'] == 0
+    assert step_info['Undershoot'] == 0
+    assert step_info['Peak'] == 1
+    assert step_info['PeakTime'] == 0
+    assert step_info['SteadyStateValue'] == -1
+
+    # Ramp from -1 to 1
+    step_info = ct.step_info(-1 + 2 * timepts/10, timepts)
+    assert step_info['RiseTime'] == 3.8
+    assert step_info['SettlingTime'] == 9.8
+    assert isclose(step_info['SettlingMin'], 0.88)
+    assert isclose(step_info['SettlingMax'], 0.96)
+    assert step_info['Overshoot'] == 0
+    assert step_info['Peak'] == 1
+    assert step_info['PeakTime'] == 0
+    assert isclose(step_info['SteadyStateValue'], 0.96)