Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

forced_response giving different output depending on Python and/or numpy version #993

Closed
MatthiasFreiburghaus opened this issue Apr 17, 2024 · 9 comments

Comments

@MatthiasFreiburghaus
Copy link

MatthiasFreiburghaus commented Apr 17, 2024

I have tested the following code on three different setups.

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

num = [2,4]
den = [1,2,4]
W = ct.tf(num,den)

timeVector = np.linspace(0,10,200)
inputVector = np.sin(2*timeVector) + np.ones(timeVector.shape)

x0 = np.array([[-0.4],[0.1]])

timeReturned, systemOutput = ct.forced_response(W,timeVector,inputVector,x0)`

Setup 1: Python 3.8, numpy 1.20.3, control 0.9.4
Setup 2: Python 3.11, numpy 1.26.4, control 0.9.4
Setup 3: Python 3.11, numpy 1.26.4, control 0.10.0

Setup 1 gives a different output than setup 2 and 3. The first value of the output of setup 1 is -0.4, and the output never reaches 2.5.
Setup 2 and 3 on the other hand produce -1.2 as their first output value, and the value of 2.5 is surpassed on the first peek.

I don't know which of the disagreeing paries is right, or if they are both wrong. And I dont know if the base installation, numpy or control are to blame for the discrepany. And I am sorry my code does not show up properly. I was using the "code" button, but this does not seem to do anything usefull.

@slivingston
Copy link
Member

I tried this with Setup 2 and and Setup 3, both of which produce the result that you described for Setup 1:
Figure_1

To test Setup 1, I had some difficulty installing numpy 1.20.3, but I tried Python 3.8, numpy 1.24.4, control 0.9.4. The result is the same as above.

To be certain, here exactly the code that I run:

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

num = [2,4]
den = [1,2,4]
W = ct.tf(num,den)

timeVector = np.linspace(0,10,200)
inputVector = np.sin(2*timeVector) + np.ones(timeVector.shape)

x0 = np.array([[-0.4],[0.1]])

timeReturned, systemOutput = ct.forced_response(W,timeVector,inputVector,x0)

plt.plot(timeReturned, systemOutput)
plt.xlabel('TIME')
plt.ylabel('OUTPUT')
plt.show()

@slivingston
Copy link
Member

I installed NumPy 1.20.3 and reproduced the same plot as above for Setup 1 (Python 3.8, numpy 1.20.3, control 0.9.4). As such, I cannot reproduce the different behavior that you described. Can you try to run your code on Google Colab or with Binder as described at https://github.com/python-control/python-control?tab=readme-ov-file#have-a-go-now ?

@MatthiasFreiburghaus
Copy link
Author

MatthiasFreiburghaus commented Apr 18, 2024

Screenshot 2024-04-18 132548
Thank you for your time. So far I've been unable to run any code on Binder. I can see the examples, I can upload files, but I can not figure out how to run them. The run button isn't doing anything. Sorry for beeing so slow and uneducated, I only was ever using Python with Spyder.

@MatthiasFreiburghaus
Copy link
Author

MatthiasFreiburghaus commented Apr 18, 2024

Meanwhile, I uninstalled the whole Anaconda distribution which is giving the wrong output (Setup 1 and 2 in the original post),
redownloaded and reinstalled it, and reinstalled the python control library (numpy 1.26.4, control 0.10.0.) I tested the code ( copy-pasted it from your post above to exlude typos) and... it still gives the wrong output, see the attached graph.
Figure 2024-04-18 123057

@slivingston
Copy link
Member

Did you install it following the instructions at https://python-control.readthedocs.io/en/0.10.0/intro.html#installation? In particular, with Anaconda, you should be able to

conda install -c conda-forge control slycot

@slivingston
Copy link
Member

As for Binder, go to https://mybinder.org/v2/gh/python-control/python-control/HEAD and then create a Python 3 Notebook; the correct icon is highlighted in red here:
Screenshot 2024-04-18 at 08 00 48-marked

In the first space, enter !pip install control, as shown here:
Screenshot 2024-04-18 at 08 01 18

Run it to install control and dependencies, including NumPy and Matplotlib. When it is completed, enter the code from above, and run it. The result will be similar to the following:
Screenshot 2024-04-18 at 08 04 29

@MatthiasFreiburghaus
Copy link
Author

Yes, I was using the conda comand to intstall the control systems library, as described in the installation section, and as pointed out in your response.
I've run the code we are talking about on mybinder according to your instructions. Here, it seems to do the right thing, producing the same output value as in your run of the program, and as I get with 'Setup 1'.

@qleonardolp
Copy link

qleonardolp commented Apr 29, 2024

Hi guys. I'm also facing issues with the forced_response method. I understand the proposed conda-based installation procedure however, I have reasons to stick with pip installations on my standard Python setup running on Windows 10. In my circumstance, after dozens of tests and evaluations simulating the exact transfer function on both Matlab and Simulink, Julia, and python-control I became doubtful about the numerical results from the latter.

To reproduce my setup here are the versions:
Python 3.11.5 (tags/v3.11.5:cce6ba9)
python-control: 0.10.0
numpy: 1.25.2
matplotlib: 3.7.2

MWE:

import matplotlib.pyplot as plt
import numpy as np
import control
from math import pi, sin, cos

wi = 2*pi*1.2    # input *angular frequency
Ai = 0.679       # input amplitude
Mr = 2.718       # actual mass
Kd = 400.0       # impedance stiffness
Dd = 2*sqrt(Kd)  # impedance damping

input_period = 2 * pi / wi
time = np.linspace(0, 2.0*input_period, 600)
x_ref_time = Ai * np.sin(wi * time)

s = control.tf('s')
Z = Dd*s + Kd  # s-domain impedance
ref_to_error_tf = (Mr * s**2)/(Z + Mr * s**2)

error_response = control.forced_response(ref_to_error_tf, time, x_ref_time, transpose=True)
plt.plot(error_response.t, error_response.x[0])

The error_response time series is scaled about the series observed in the reference simulations. I conducted tests with a range of different values for the parameters Ai, wi, Mr, Kd, Dd, and it was noted that while this time series preserved its shape, it exhibited an amplitude approximately eight to ten times lower than expected.

I'll reproduce the execution of this code snippet on my Ubuntu 22.04 soon.

@murrayrm
Copy link
Member

murrayrm commented May 5, 2024

I took a look at the above issue. I note that in all cases there is some type of access to the state of the underlying realization of the transfer function. In particular, in the original post the initial condition for the state is set and in the latest post the value of the first state is plotted.

This is a potential problem because the state space realization for a transfer function is non-unique and can depend on how the conversion to state space is done. In the current release of python-control, this is done in one of two ways:

  • If slycot is installed, then statespace._convert_to_statespace will use slycot.td04ad with whatever version of slycot is installed.
  • If slycot is not installed, then statespace._convert_to_statespace will use scipy.signal.tf2ss for whatever version of SciPy is installed.

Using the ct.ss factor function, you can control what method is used by using the method keyword.

Note that when you specify the initial condition, you get a warning that something funny may happen:

 UserWarning: Non-zero initial condition given for transfer function system. Internal conversion
to state space used; may not be consistent with given X0.

This is a hint that something funny may be happening.

With that as background, I took the original example that was posted and ran in Python 3.12 with slycot installed. I converted the transfer function to state space form using ct.ss and the method keyword to control the conversion, the plotted the forced response. Here are the results:

scipy_vs_slycot

We see that the initial output differs, because the state space representations are different and so setting x0 can create different results. The system is stable and so the transient eventually dies out and the responses match.

I suspect this also explains the second example, where the internal state x is being accessed. The only thing that is guaranteed to be consistent between various implementations (including comparisons to MATLAB or Octave) is the outputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants