diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index e5e24b990..ae28975ee 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -1247,13 +1247,14 @@ def test_to_pandas(): np.testing.assert_equal(df['x[1]'], resp.states[1]) # Change the time points - sys = ct.rss(2, 1, 1) + sys = ct.rss(2, 1, 2) T = np.linspace(0, timepts[-1]/2, timepts.size * 2) - resp = ct.input_output_response(sys, timepts, np.sin(timepts), t_eval=T) + resp = ct.input_output_response( + sys, timepts, [np.sin(timepts), 0], t_eval=T) df = resp.to_pandas() np.testing.assert_equal(df['time'], resp.time) - np.testing.assert_equal(df['u[0]'], resp.inputs) - np.testing.assert_equal(df['y[0]'], resp.outputs) + np.testing.assert_equal(df['u[0]'], resp.inputs[0]) + np.testing.assert_equal(df['y[0]'], resp.outputs[0]) np.testing.assert_equal(df['x[0]'], resp.states[0]) np.testing.assert_equal(df['x[1]'], resp.states[1]) @@ -1265,6 +1266,33 @@ def test_to_pandas(): np.testing.assert_equal(df['u[0]'], resp.inputs) np.testing.assert_equal(df['y[0]'], resp.inputs * 5) + # Multi-trace data + # https://github.com/python-control/python-control/issues/1087 + model = ct.rss( + states=['x0', 'x1'], outputs=['y0', 'y1'], + inputs=['u0', 'u1'], name='My Model') + T = np.linspace(0, 10, 100, endpoint=False) + X0 = np.zeros(model.nstates) + + res = ct.step_response(model, T=T, X0=X0, input=0) # extract single trace + df = res.to_pandas() + np.testing.assert_equal( + df[df['trace'] == 'From u0']['time'], res.time) + np.testing.assert_equal( + df[df['trace'] == 'From u0']['u0'], res.inputs['u0', 0]) + np.testing.assert_equal( + df[df['trace'] == 'From u0']['y1'], res.outputs['y1', 0]) + + res = ct.step_response(model, T=T, X0=X0) # all traces + df = res.to_pandas() + for i, label in enumerate(res.trace_labels): + np.testing.assert_equal( + df[df['trace'] == label]['time'], res.time) + np.testing.assert_equal( + df[df['trace'] == label]['u1'], res.inputs['u1', i]) + np.testing.assert_equal( + df[df['trace'] == label]['y0'], res.outputs['y0', i]) + @pytest.mark.skipif(pandas_check(), reason="pandas installed") def test_no_pandas(): diff --git a/control/timeresp.py b/control/timeresp.py index 072db60de..67641d239 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -718,8 +718,10 @@ def __len__(self): def to_pandas(self): """Convert response data to pandas data frame. - Creates a pandas data frame using the input, output, and state - labels for the time response. + Creates a pandas data frame using the input, output, and state labels + for the time response. The column labels are given by the input and + output (and state, when present) labels, with time labeled by 'time' + and traces (for multi-trace responses) labeled by 'trace'. """ if not pandas_check(): @@ -727,16 +729,23 @@ def to_pandas(self): import pandas # Create a dict for setting up the data frame - data = {'time': self.time} + data = {'time': np.tile( + self.time, self.ntraces if self.ntraces > 0 else 1)} + if self.ntraces > 0: + data['trace'] = np.hstack([ + np.full(self.time.size, label) for label in self.trace_labels]) if self.ninputs > 0: data.update( - {name: self.u[i] for i, name in enumerate(self.input_labels)}) + {name: self.u[i].reshape(-1) + for i, name in enumerate(self.input_labels)}) if self.noutputs > 0: data.update( - {name: self.y[i] for i, name in enumerate(self.output_labels)}) + {name: self.y[i].reshape(-1) + for i, name in enumerate(self.output_labels)}) if self.nstates > 0: data.update( - {name: self.x[i] for i, name in enumerate(self.state_labels)}) + {name: self.x[i].reshape(-1) + for i, name in enumerate(self.state_labels)}) return pandas.DataFrame(data)