numpysympynumpy-ndarray

issue with sympy lambdify


I just want numerically integrate the Lorenz' famous 3 equations I create this code that seems perfectly work:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

import sympy as sp
from IPython.display import display
from sympy.interactive import printing
printing.init_printing(use_latex = True)
#%%
def runge_kutta(funzione,Xn,Dt):
    k1 = funzione(Xn) * Dt 
    k2 = funzione(Xn + (1/2)*k1) * Dt
    k3 = funzione(Xn + (1/2)*k2) * Dt
    k4 = funzione(Xn + k3) * Dt
    Xnuovo = Xn + (1/6)*(k1 + 2*k2 + 2*k3 + k4)
    return Xnuovo
#%%
# flux phase of the trajectory
def flusso_traiettoria_singola(condizioni_iniziali, dt, punti_di_tempo, FUNZA):
    dimensionalita = len(condizioni_iniziali)
    X = np.copy(condizioni_iniziali)
    risultati_integrazione = []
    for array_separati in range(dimensionalita):
        risultati_integrazione.append(np.array([condizioni_iniziali[array_separati]]))
    for r in range(1,punti_di_tempo):
        X = runge_kutta(FUNZA,X,dt)
        for array_cui_aggiungere in range(dimensionalita):
            risultati_integrazione[array_cui_aggiungere] = np.append(risultati_integrazione[array_cui_aggiungere], X[array_cui_aggiungere])
    return risultati_integrazione
#%%
sigma = 10
r = 28
b = 8/3
def Lorenz_mio(X):
    x = X[0]
    y = X[1]
    z = X[2]
    
    dxdt = sigma * (y - x)
    dydt = r*x - y - x*z
    dzdt = x*y - b*z
    vettorederivate = np.array([dxdt,dydt,dzdt])

    return vettorederivate
#%%
# INTEGRATION

condizione_iniziale = np.array([1, 1, 1]) # initial condition
dt = 0.01
numero_di_punti_tempo_voluti = 4000
tempo_finale = numero_di_punti_tempo_voluti * dt
t = np.linspace(0,tempo_finale,numero_di_punti_tempo_voluti)
risultati_integrazione_lorenz = flusso_traiettoria_singola(condizione_iniziale, dt, numero_di_punti_tempo_voluti, Lorenz_mio)

# PLOT
fig = plt.figure(figsize=(8, 6))
ax = Axes3D(fig)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.plot_wireframe(risultati_integrazione_lorenz[0], risultati_integrazione_lorenz[1], risultati_integrazione_lorenz[2], color='green', label='ritratto di fase')
plt.savefig()

ok, now I would like do the same thing but using Numpy (eg I can also calculate the fixed points..) and I would not create a function for Lorenz system, as before, but using instead the one I have create with Numpy, my code is:

# NOW SYMPY

x_simbolo, y_simbolo, z_simbolo = sp.symbols('x y z')
sigma_par, r_par, b_par = sp.symbols(r' \sigma r b')

x_dot_deriv_tempo = sigma_par * ( y_simbolo - x_simbolo )
y_dot_deriv_tempo = r_par * x_simbolo - y_simbolo - x_simbolo * z_simbolo
z_dot_deriv_tempo = x_simbolo * y_simbolo - b_par * z_simbolo
sistema_3_eq_lorenz_x_y_z = x_dot_deriv_tempo, y_dot_deriv_tempo, z_dot_deriv_tempo
display(sistema_3_eq_lorenz_x_y_z)

# FIXED POINTS
punti_fissi_sistema_3_eq_lorenz = sp.solve(sistema_3_eq_lorenz_x_y_z, x_simbolo, y_simbolo, z_simbolo, dict=True)
print('i punti fissi delle 3 equazioni sono')
display(punti_fissi_sistema_3_eq_lorenz)
#%%

sympify_integrazione_numerica_sistema_lorenz = sp.sympify( sistema_3_eq_lorenz_x_y_z )
sigma_int_num = 10
r_int_num = 28
b_int_num = 8/3
sympify_integrazione_numerica_sistema_lorenz = sympify_integrazione_numerica_sistema_lorenz.subs([ (sigma_par, sigma_int_num), (r_par, r_int_num), (b_par, b_int_num) ])
lambdifylorenz = sp.lambdify([x_simbolo, y_simbolo, z_simbolo], sympify_integrazione_numerica_sistema_lorenz, "numpy")

risultati_integrazione_lorenz = flusso_traiettoria_singola(condizione_iniziale, dt, numero_di_punti_tempo_voluti, lambdifylorenz)

# PLOT
fig = plt.figure(figsize=(8, 6))
ax = Axes3D(fig)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.plot_wireframe(risultati_integrazione_lorenz[0], risultati_integrazione_lorenz[1], risultati_integrazione_lorenz[2], color='green', label='ritratto di fase')
ax.legend(loc='lower center')
plt.savefig()

Here comes the problem:

lambdifylorenz(condizione_iniziale) gives error

TypeError: <lambda>() missing 2 required positional arguments: '_Dummy_32' and '_Dummy_33'

since I can't use it as I did with

Lorenz_mio(condizione_iniziale)

which correctly gives:

array([ 0. , 26. , -1.66666667])

Of course this will work

lambdifylorenz(condizione_iniziale[0],condizione_iniziale[1],condizione_iniziale[2])

or simply

lambdifylorenz(1,1,1)

but it isn't what I need since it must work with the call

funzione(Xn)

that appears in the functions runge_kutta() and flusso_traiettoria_singola().

I read the Lambdify webpage but I found nothing: there is an example similar to the following

a = np.array([1, 2, 3])
fxy = sp.lambdify([x,y], [x + 1,y**2], 'numpy')

with fxy(a,a) you have [array([2, 3, 4]), array([1, 4, 9])]


Solution

  • Your code was broken in numerous ways; parameter packing was only one of them. Don't set risultati_integrazione to a list; it should be a pre-allocated Numpy array. Don't write an inner for-loop to append. Don't 8/3; this is symbolic math so you should be using a Rational. Don't call Axes3D directly. Don't call plot_wireframe; simply call plot. Don't call sympify; it's easier to work with this as a Matrix since Sympy will coerce the numerical function to an array instead of a tuple. Don't call .legend(); you only have one series.

    import typing
    
    import numpy as np
    import sympy
    import sympy as sp
    from matplotlib import pyplot as plt
    
    """
    Sono necessari più commenti in italiano. Il contenuto stampato dovrebbe essere in italiano. 
    I nomi delle variabili e delle funzioni dovrebbero essere in inglese.
    """
    
    
    def runge_kutta(
        function: typing.Callable,  # funzione
        Xn: np.ndarray,
        dt: float,
    ) -> np.ndarray:
        k1 = dt*function(Xn)
        k2 = dt*function(Xn + k1*0.5)
        k3 = dt*function(Xn + k2*0.5)
        k4 = dt*function(Xn + k3)
        Xnew = Xn + (k1 + 2*(k2 + k3) + k4)/6  # X nuovo
        return Xnew
    
    
    def trajectory_flux_phase(
        initial_conditions: np.ndarray,  # condizioni iniziali
        dt: float,
        time_points: int,  # punti di tempo
        function: typing.Callable,  # funza
    ) -> np.ndarray:
        """flusso traiettoria singola"""
    
        dimensionality = len(initial_conditions)  # dimensionalita
        X = np.copy(initial_conditions)
        integration_results = np.empty((dimensionality, time_points))  # risultati integrazione
        integration_results[:, 0] = initial_conditions
    
        for r in range(1, time_points):
            X[:] = runge_kutta(function, X, dt)
            integration_results[:, r] = X  # array cui aggiungere
    
        return integration_results
    
    
    def plot(integration_result) -> plt.Figure:
        fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
        ax.set_xlabel('x')
        ax.set_ylabel('y')
        ax.set_zlabel('z')
        ax.set_title('Ritratto di Fase')
        ax.plot(*integration_result)
        return fig
    
    
    def main() -> None:
        x, y, z, sigma, r, b = sp.symbols(r'x y z \sigma r b')  # simbolo
    
        # derivati temporali
        x_dot = sigma*(y - x)
        y_dot = r*x - y - x*z
        z_dot = x*y - b*z
    
        # sistema 3 eq lorenz x y z
        lorenz_system = x_dot, y_dot, z_dot
        print(lorenz_system)
    
        # punti fissi sistema 3 eq Lorenz
        fixed_point_system = sp.solve(lorenz_system, x, y, z, dict=True)
        print('i punti fissi delle 3 equazioni sono')
        print(fixed_point_system)
    
        # sympify integrazione numerica sistema lorenz
        system_tuple = sp.Matrix(lorenz_system).T.subs([
            (sigma, 10),
            (r, 28),
            (b, sympy.Rational(8, 3)),
        ])
        function = sp.lambdify([x, y, z], system_tuple)
    
        def vector_function(params: np.ndarray) -> np.ndarray:
            return function(*params).squeeze()
    
        # risultati integrazione lorenz
        integration_result = trajectory_flux_phase(
            initial_conditions=np.ones(3),  # condizione iniziale
            dt=0.01,
            time_points=4_000,  # numero di punti tempo voluti
            function=vector_function,
        )
    
        plot(integration_result)
        plt.show()
    
    
    if __name__ == '__main__':
        main()
    
    (\sigma*(-x + y), r*x - x*z - y, -b*z + x*y)
    i punti fissi delle 3 equazioni sono
    [{x: 0, y: 0, z: 0}, {x: -sqrt(b*r - b), y: -sqrt(b*(r - 1)), z: r - 1}, {x: sqrt(b*r - b), y: sqrt(b*(r - 1)), z: r - 1}]
    

    attractors

    More broadly, you should not be re-implementing Runge-Kutta; instead call into e.g. https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.RK45.html via solve_ivp. This is more accurate, flexible and fast, especially if you don't use Runge-Kutta and use instead e.g. LSODA with an exact Jacobian:

    import functools
    import time
    
    import numpy as np
    from matplotlib import pyplot as plt
    from scipy.integrate import solve_ivp
    from scipy.optimize import check_grad
    
    
    def lorenz_dt(
        t: float, v: np.ndarray, sigma: float, r: float, b: float,
    ) -> np.ndarray:
        """derivati temporali"""
        x, y, z = v
    
        return np.array((
            sigma*(y - x),  # xdot
            x*(r - z) - y,  # ydot
            x*y - b*z,      # zdot
        ))
    
    
    def lorenz_jacobian(
        t: float, v: np.ndarray, sigma: float, r: float, b: float,
    ) -> np.ndarray:
        x, y, z = v
    
        return np.array((
            #   /dx    /dy /dz
            (-sigma, sigma,  0),  # df[0]
            (   r-z,    -1, -x),  # df[1]
            (     y,     x, -b),  # df[2]
        ))
    
    
    def trajectory_flux_phase(
        sigma: float = 10, r: float = 28, b: float = 8/3,
    ) -> np.ndarray:
        # Queste quantità non sono esatte. L'algoritmo le modificherà in modo intelligente.
        equivalent_dt = 0.01
        equivalent_n = 4_000
        t_end = equivalent_dt*equivalent_n
    
        initial_value = np.ones(3)  # condizione iniziale
        error = check_grad(
            func=functools.partial(
                lorenz_dt,
                0, sigma=sigma, r=r, b=b,
            ),
            grad=functools.partial(
                lorenz_jacobian,
                0, sigma=sigma, r=r, b=b,
            ),
            x0=initial_value,
        )
        assert error < 1e-6
    
        t0 = time.perf_counter()
        result = solve_ivp(
            fun=functools.partial(lorenz_dt, sigma=sigma, r=r, b=b),
            jac=functools.partial(lorenz_jacobian, sigma=sigma, r=r, b=b),
            method='LSODA',
            t_span=(0, t_end),
            y0=initial_value,
            rtol=1e-8,
        )
        t1 = time.perf_counter()
        if not result.success:
            raise ValueError(result.message)
        print(result.message)
        print('numero di chiamate di funzione:', result.nfev)
        print("numero di punti temporali scelti dall'algoritmo:", result.y.shape[1])
        print(f'tempo di esecuzione: {t1 - t0:.4f} s')
        print()
    
        return result.y
    
    
    def plot(integration_result: np.ndarray) -> plt.Figure:
        fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
        ax.set_xlabel('x')
        ax.set_ylabel('y')
        ax.set_zlabel('z')
        ax.set_title('Ritratto di Fase')
        ax.plot(*integration_result)
        return fig
    
    
    def main() -> None:
        integration_result = trajectory_flux_phase()
        plot(integration_result)
        plt.show()
    
    
    if __name__ == '__main__':
        main()