Skip to content

Commit e1f8d3a

Browse files
committed
allow TimeResponseData to be converted to pandas
1 parent ad714fe commit e1f8d3a

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed

.github/workflows/python-package-conda.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ on: [push, pull_request]
44

55
jobs:
66
test-linux:
7-
name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }}
7+
name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.pandas && ', with pandas' || '' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }}
88
runs-on: ubuntu-latest
99

1010
strategy:
1111
max-parallel: 5
1212
matrix:
1313
python-version: [3.7, 3.9]
1414
slycot: ["", "conda"]
15+
pandas: [""]
1516
array-and-matrix: [0]
1617
include:
1718
- python-version: 3.9
1819
slycot: conda
20+
pandas: conda
1921
array-and-matrix: 1
2022

2123
steps:
@@ -41,6 +43,9 @@ jobs:
4143
if [[ '${{matrix.slycot}}' == 'conda' ]]; then
4244
conda install -c conda-forge slycot
4345
fi
46+
if [[ '${{matrix.pandas}}' == 'conda' ]]; then
47+
conda install -c conda-forge pandas
48+
fi
4449
4550
- name: Test with pytest
4651
env:

control/exception.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,16 @@ def slycot_check():
7171
except:
7272
slycot_installed = False
7373
return slycot_installed
74+
75+
76+
# Utility function to see if pandas is installed
77+
pandas_installed = None
78+
def pandas_check():
79+
global pandas_installed
80+
if pandas_installed is None:
81+
try:
82+
import pandas
83+
pandas_installed = True
84+
except:
85+
pandas_installed = False
86+
return pandas_installed

control/tests/timeresp_test.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import control as ct
1111
from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss
12-
from control.exception import slycot_check
12+
from control.exception import slycot_check, pandas_check
1313
from control.tests.conftest import slycotonly
1414
from control.timeresp import (_default_time_vector, _ideal_tfinal_and_dt,
1515
forced_response, impulse_response,
@@ -1180,3 +1180,55 @@ def test_response_transpose(
11801180
assert t.shape == (T.size, )
11811181
assert y.shape == ysh_no
11821182
assert x.shape == (T.size, sys.nstates)
1183+
1184+
1185+
@pytest.mark.skipif(not pandas_check(), reason="pandas not installed")
1186+
def test_to_pandas():
1187+
# Create a SISO time response
1188+
sys = ct.rss(2, 1, 1)
1189+
timepts = np.linspace(0, 10, 10)
1190+
resp = ct.input_output_response(sys, timepts, 1)
1191+
1192+
# Convert to pandas
1193+
df = resp.to_pandas()
1194+
1195+
# Check to make sure the data make senses
1196+
np.testing.assert_equal(df['time'], resp.time)
1197+
np.testing.assert_equal(df['u[0]'], resp.inputs)
1198+
np.testing.assert_equal(df['y[0]'], resp.outputs)
1199+
np.testing.assert_equal(df['x[0]'], resp.states[0])
1200+
np.testing.assert_equal(df['x[1]'], resp.states[1])
1201+
1202+
# Create a MIMO time response
1203+
sys = ct.rss(2, 2, 1)
1204+
resp = ct.input_output_response(sys, timepts, np.sin(timepts))
1205+
df = resp.to_pandas()
1206+
np.testing.assert_equal(df['time'], resp.time)
1207+
np.testing.assert_equal(df['u[0]'], resp.inputs[0])
1208+
np.testing.assert_equal(df['y[0]'], resp.outputs[0])
1209+
np.testing.assert_equal(df['y[1]'], resp.outputs[1])
1210+
np.testing.assert_equal(df['x[0]'], resp.states[0])
1211+
np.testing.assert_equal(df['x[1]'], resp.states[1])
1212+
1213+
# Change the time points
1214+
sys = ct.rss(2, 1, 1)
1215+
T = np.linspace(0, timepts[-1]/2, timepts.size * 2)
1216+
resp = ct.input_output_response(sys, timepts, np.sin(timepts), t_eval=T)
1217+
df = resp.to_pandas()
1218+
np.testing.assert_equal(df['time'], resp.time)
1219+
np.testing.assert_equal(df['u[0]'], resp.inputs)
1220+
np.testing.assert_equal(df['y[0]'], resp.outputs)
1221+
np.testing.assert_equal(df['x[0]'], resp.states[0])
1222+
np.testing.assert_equal(df['x[1]'], resp.states[1])
1223+
1224+
1225+
@pytest.mark.skipif(pandas_check(), reason="pandas installed")
1226+
def test_no_pandas():
1227+
# Create a SISO time response
1228+
sys = ct.rss(2, 1, 1)
1229+
timepts = np.linspace(0, 10, 10)
1230+
resp = ct.input_output_response(sys, timepts, 1)
1231+
1232+
# Convert to pandas
1233+
with pytest.raises(ImportError, match="pandas"):
1234+
df = resp.to_pandas()

control/timeresp.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
from copy import copy
8080

8181
from . import config
82+
from .exception import pandas_check
8283
from .lti import isctime, isdtime
8384
from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso
8485
from .xferfcn import TransferFunction
@@ -638,6 +639,23 @@ def __getitem__(self, index):
638639
def __len__(self):
639640
return 3 if self.return_x else 2
640641

642+
# Convert to pandas
643+
def to_pandas(self):
644+
if not pandas_check():
645+
ImportError('pandas not installed')
646+
import pandas
647+
648+
# Create a dict for setting up the data frame
649+
data = {'time': self.time}
650+
data.update(
651+
{name: self.u[i] for i, name in enumerate(self.input_labels)})
652+
data.update(
653+
{name: self.y[i] for i, name in enumerate(self.output_labels)})
654+
data.update(
655+
{name: self.x[i] for i, name in enumerate(self.state_labels)})
656+
657+
return pandas.DataFrame(data)
658+
641659

642660
# Process signal labels
643661
def _process_labels(labels, signal, length):

0 commit comments

Comments
 (0)