Skip to content

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

Closed

Description

@MatthiasFreiburghaus

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.

Activity

  1. slivingston commented on Apr 18, 2024

    @slivingston
    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()
  2. slivingston commented on Apr 18, 2024

    @slivingston
    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 ?

  3. MatthiasFreiburghaus commented on Apr 18, 2024

    @MatthiasFreiburghaus
    Author

    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.

  4. MatthiasFreiburghaus commented on Apr 18, 2024

    @MatthiasFreiburghaus
    Author

    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

  5. slivingston commented on Apr 18, 2024

    @slivingston
    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
    
  6. slivingston commented on Apr 18, 2024

    @slivingston
    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

  7. MatthiasFreiburghaus commented on Apr 19, 2024

    @MatthiasFreiburghaus
    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'.

  8. qleonardolp commented on Apr 29, 2024

    @qleonardolp

    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.

  9. murrayrm commented on May 5, 2024

    @murrayrm
    Member

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions