Repository: python-control/python-control

Issue:
`control.combine_time_responses` fails to combine time responses
Using `control.combine_time_responses` results in the following error:

```python
AttributeError: 'TimeResponseData' object has no attribute 'types'
```
This is because the code that adds new trace labels to the combined data has a bug where is trying to access a `types` attribute from the `TimeResponseData` objects instead of `trace_types`.

Discussion:

Patch:
diff --git a/control/timeplot.py b/control/timeplot.py
index 2eb7aec9..f257d2b3 100644
--- a/control/timeplot.py
+++ b/control/timeplot.py
@@ -738,7 +738,8 @@ def combine_time_responses(response_list, trace_labels=None, title=None):
             if generate_trace_labels:
                 trace_labels.append(response.title)
             trace_types.append(
-                None if response.trace_types is None else response.types[0])
+                None if response.trace_types is None
+                else response.trace_types[0])
 
         else:
             # Save the data
diff --git a/control/timeresp.py b/control/timeresp.py
index f844b1df..244d90c2 100644
--- a/control/timeresp.py
+++ b/control/timeresp.py
@@ -1674,19 +1674,17 @@ 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.nonzero(
+                tr_lower_index = np.where(
                     sgnInf * (yout - RiseTimeLimits[0] * InfValue) >= 0
                     )[0][0]
-                tr_upper_index = np.nonzero(
+                tr_upper_index = np.where(
                     sgnInf * (yout - RiseTimeLimits[1] * InfValue) >= 0
                     )[0][0]
                 rise_time = T[tr_upper_index] - T[tr_lower_index]
 
                 # SettlingTime
-                outside_threshold = np.nonzero(
-                    np.abs(yout/InfValue - 1) >= SettlingTimeThreshold)[0]
-                settled = 0 if outside_threshold.size == 0 \
-                    else outside_threshold[-1] + 1
+                settled = np.where(
+                    np.abs(yout/InfValue-1) >= SettlingTimeThreshold)[0][-1]+1
                 # MIMO systems can have unsettled channels without infinite
                 # InfValue
                 if settled < len(T):


Test patch:
diff --git a/control/tests/timeplot_test.py b/control/tests/timeplot_test.py
index 0fcc159b..58ef062a 100644
--- a/control/tests/timeplot_test.py
+++ b/control/tests/timeplot_test.py
@@ -258,7 +258,7 @@ def test_combine_time_responses():
     sys_mimo = ct.rss(4, 2, 2)
     timepts = np.linspace(0, 10, 100)
 
-    # Combine two response with ntrace = 0
+    # Combine two responses with ntrace = 0
     U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
     resp1 = ct.input_output_response(sys_mimo, timepts, U)
 
@@ -293,6 +293,7 @@ def test_combine_time_responses():
     combresp4 = ct.combine_time_responses(
         [resp1, resp2, resp3], trace_labels=labels)
     assert combresp4.trace_labels == labels
+    assert combresp4.trace_types == [None, None, 'step', 'step']
 
     # Automatically generated trace label names and types
     resp5 = ct.step_response(sys_mimo, timepts)
@@ -302,7 +303,13 @@ def test_combine_time_responses():
     combresp5 = ct.combine_time_responses([resp1, resp5])
     assert combresp5.trace_labels == [resp1.title] + \
         ["test, trace 0", "test, trace 1"]
-    assert combresp4.trace_types == [None, None, 'step', 'step']
+    assert combresp5.trace_types == [None, None, None]
+
+    # ntraces = 0 with trace_types != None
+    # https://github.com/python-control/python-control/issues/1025
+    resp6 = ct.forced_response(sys_mimo, timepts, U)
+    combresp6 = ct.combine_time_responses([resp1, resp6])
+    assert combresp6.trace_types == [None, 'forced']
 
     with pytest.raises(ValueError, match="must have the same number"):
         resp = ct.step_response(ct.rss(4, 2, 3), timepts)
diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py
index e2d93be0..73032c0a 100644
--- a/control/tests/timeresp_test.py
+++ b/control/tests/timeresp_test.py
@@ -1,7 +1,6 @@
 """timeresp_test.py - test time response functions"""
 
 from copy import copy
-from math import isclose
 
 import numpy as np
 import pytest
@@ -9,11 +8,11 @@ import scipy as sp
 
 import control as ct
 from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss
-from control.exception import pandas_check, slycot_check
+from control.exception import slycot_check, pandas_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:
@@ -1276,45 +1275,3 @@ 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)