Skip to content

control.timeresp.TimeResponseData.to_pandas() failing #1087

Description

@joaoantoniocardoso

Hi, today I was using some step responses and noticed that the .to_pandas() is not actually working.

I managed to workaround it by creating my own function to translate the response into a dataframe.

Example of code not working:

import control as ct
import numpy as np

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()

Error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[140], line 6
      3 T = np.linspace(0, 10, 100, endpoint=False)
      4 X0 = np.zeros(model.nstates)
----> 6 res = ct.step_response(model, T=T, X0=X0, input=0).to_pandas()

File ~.env/lib/python3.10/site-packages/control/timeresp.py:723, in TimeResponseData.to_pandas(self)
    719 if self.nstates > 0:
    720     data.update(
    721         {name: self.x[i] for i, name in enumerate(self.state_labels)})
--> 723 return pandas.DataFrame(data)

File ~.env/lib/python3.10/site-packages/pandas/core/frame.py:778, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    772     mgr = self._init_mgr(
    773         data, axes={"index": index, "columns": columns}, dtype=dtype, copy=copy
    774     )
    776 elif isinstance(data, dict):
    777     # GH#38939 de facto copy defaults to False only in non-dict cases
--> 778     mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy, typ=manager)
    779 elif isinstance(data, ma.MaskedArray):
    780     from numpy.ma import mrecords

File ~.env/lib/python3.10/site-packages/pandas/core/internals/construction.py:503, in dict_to_mgr(data, index, columns, dtype, typ, copy)
    499     else:
    500         # dtype check to exclude e.g. range objects, scalars
    501         arrays = [x.copy() if hasattr(x, "dtype") else x for x in arrays]
--> 503 return arrays_to_mgr(arrays, columns, index, dtype=dtype, typ=typ, consolidate=copy)

File ~.env/lib/python3.10/site-packages/pandas/core/internals/construction.py:114, in arrays_to_mgr(arrays, columns, index, dtype, verify_integrity, typ, consolidate)
    111 if verify_integrity:
    112     # figure out the index, if necessary
    113     if index is None:
--> 114         index = _extract_index(arrays)
    115     else:
    116         index = ensure_index(index)

File ~.env/lib/python3.10/site-packages/pandas/core/internals/construction.py:664, in _extract_index(data)
    662         raw_lengths.append(len(val))
    663     elif isinstance(val, np.ndarray) and val.ndim > 1:
--> 664         raise ValueError("Per-column arrays must each be 1-dimensional")
    666 if not indexes and not raw_lengths:
    667     raise ValueError("If using all scalar values, you must pass an index")

ValueError: Per-column arrays must each be 1-dimensional

The code I'm using to workaround it is the following:

import matplotlib.pyplot as plt
import control as ct
import numpy as np

def step_response_to_pandas(step_response):
    return pd.DataFrame(
        {'trace_label': np.array([[label] * (len(res.time)) for label in res.trace_labels]).ravel()} |
        {'time': res.time.repeat(len(res.trace_labels))} |
        {label: res.inputs[i].ravel() for i,label in enumerate(res.input_labels)} |
        {label: res.outputs[i].ravel() for i,label in enumerate(res.output_labels)} |
        {label: res.states[i].ravel() for i,label in enumerate(res.state_labels)}
    )

def plot_step_response_dataframe(df):
    grouped = df.groupby(level='trace_label')
    row_size = 1

    for trace_label, group in grouped:
        fig, axes = plt.subplots(len(group.columns), 1, figsize=(6.4, len(group.columns) * row_size), sharex=True)
        fig.suptitle(f'Trace: {trace_label}', fontsize=16)
        
        if len(group.columns) == 1:
            axes = [axes]
        
        for ax, (signal_name, signal_data) in zip(axes, group.items()):
            ax.plot(group.index.get_level_values('time'), signal_data, label=signal_name)
            ax.grid(True)
            ax.set_ylabel(signal_name)
        
        axes[-1].set_xlabel('Time')
        
        plt.tight_layout()
        plt.show()


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)

df = step_response_to_pandas(res)
df = df.set_index(['trace_label', 'time'])

plot_step_response_dataframe(df)

display(df)

Example of output:

image
image
image


Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions