From 448fb620ac9b1c92bc09e4a39865ca13939c7189 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 30 Dec 2024 14:15:53 -0800 Subject: [PATCH 1/3] add unit test showing failure in #1087 --- control/tests/timeresp_test.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index e5e24b990..f3a0ecc15 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,17 @@ def test_to_pandas(): np.testing.assert_equal(df['u[0]'], resp.inputs) np.testing.assert_equal(df['y[0]'], resp.inputs * 5) + # 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) + df = res.to_pandas() + np.testing.assert_equal(df['time'], res.time) + np.testing.assert_equal(df['y1'], res.outputs['y1']) + @pytest.mark.skipif(pandas_check(), reason="pandas installed") def test_no_pandas(): From 96d32289c203333de8f007f5525ba5ec8797448f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 30 Dec 2024 20:40:30 -0800 Subject: [PATCH 2/3] fix TimeResponseData.to_pandas() for multi-trace responses --- control/tests/timeresp_test.py | 18 +++++++++++++++--- control/timeresp.py | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index f3a0ecc15..5949a4441 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -1266,16 +1266,28 @@ 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) + + 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']['y1'], res.outputs['y1', 0]) + + res = ct.step_response(model, T=T, X0=X0) # all traces df = res.to_pandas() - np.testing.assert_equal(df['time'], res.time) - np.testing.assert_equal(df['y1'], res.outputs['y1']) + 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]['u0'], res.inputs['u0', i]) @pytest.mark.skipif(pandas_check(), reason="pandas installed") 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) From 7707ea8a665bcd4b3936b9d66635ecd8288a99ce Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 2 Jan 2025 08:23:01 -0800 Subject: [PATCH 3/3] update unit test per @slivinsgston review --- control/tests/timeresp_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 5949a4441..ae28975ee 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -1278,6 +1278,8 @@ def test_to_pandas(): 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]) @@ -1287,7 +1289,9 @@ def test_to_pandas(): np.testing.assert_equal( df[df['trace'] == label]['time'], res.time) np.testing.assert_equal( - df[df['trace'] == label]['u0'], res.inputs['u0', i]) + 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")