Search code examples
python-3.xsympysymbolic-math

Why Sympy does not solve my nonlinear system? Python interpreter remains in execution until I kill the process


I need to solve a nonlinear system of equations in Python using Sympy. For this, I wrote the code below. However, when I run this code the Python remains busy without returning any error message and, additionally, does not return the solution.

For comparison, I did the same work in Matlab and within a few seconds, the program returns two solutions for this system.

How, using Sympy, I can solve the system?

Regards.

import sympy as sym
import numpy as np

# Variables of the system
S, V, E, A, I, R  = sym.symbols('S, V, E, A, I, R')

# Parameters of the system
N = sym.Symbol("N", positive = True)
mi = sym.Symbol("mi", positive = True)
v = sym.Symbol("v", positive = True)
epsilon = sym.Symbol("epsilon", positive = True)
alpha = sym.Symbol("alpha", positive = True)
gamma_as = sym.Symbol("gamma_as", positive = True)
gamma_s = sym.Symbol("gamma_s", positive = True)
gamma_a = sym.Symbol("gamma_a", positive = True)
lamb = sym.Symbol("lamb", positive = True)
tau = sym.Symbol("tau", positive = True)
beta = sym.Symbol("beta", positive = True)
x = sym.Symbol("x")

# Declaration of the system equations
system = [mi*N - v*S + R - (beta*(A+I)/N)*S - mi*S,\
          v*S - (1-epsilon)*(beta*(A+I)/N)*V - mi*V,\
          (beta*(A+I)/N)*S + (1-epsilon)*(beta*(A+I)/N)*V - sym.exp(-mi*tau)*(beta*(A+I)/N)*S - mi*E,\
          alpha*sym.exp(-mi*tau)*(beta*(A+I)/N)*S - (gamma_as + gamma_a + mi)*A,\
          (1-alpha)*sym.exp(-mi*tau)*(beta*(A+I)/N)*S + gamma_as*A - (gamma_s + mi)*I,\
          gamma_a*A + gamma_s*I - (1+mi)*R]

# Solution
solution_set = sym.nonlinsolve(system, [S, V, E, A, I, R])
pyS, pyV, pyE, pyA, pyI, pyR = solution_set[0]
````

Solution

  • SymPy generally solves a system of polynomial equations like this using Groebner bases. To compute the Groebner basis SymPy needs to identify each of the equations as a polynomial in the given unknowns with coefficients in a computable field (a "domain"). Your coefficients involve both mi and exp(-mi*tau) which SymPy's construct_domain doesn't like so it gives up constructing a computable domain and uses the "EX" domain instead which is very slow.

    The solution then is to replace exp(mi*tau) with another symbol (I'll just use tau) and then compute the Groebner basis explicitly yourself:

    In [103]: rep = {exp(-mi*tau):tau}
    
    In [104]: system2 = [eq.subs(rep) for eq in system]
    
    In [105]: for eq in system2: pprint(eq)
                            S⋅β⋅(A + I)
    N⋅mi + R - S⋅mi - S⋅v - ───────────
                                 N     
                 V⋅β⋅(1 - ε)⋅(A + I)
    S⋅v - V⋅mi - ───────────────────
                          N         
            S⋅β⋅τ⋅(A + I)   S⋅β⋅(A + I)   V⋅β⋅(1 - ε)⋅(A + I)
    -E⋅mi - ───────────── + ─────────── + ───────────────────
                  N              N                 N         
                         S⋅α⋅β⋅τ⋅(A + I)
    -A⋅(γₐ + γₐₛ + mi) + ───────────────
                                N       
                          S⋅β⋅τ⋅(1 - α)⋅(A + I)
    A⋅γₐₛ - I⋅(γₛ + mi) + ─────────────────────
                                    N          
    A⋅γₐ + I⋅γₛ - R⋅(mi + 1)
    

    Now we could use solve or nonlinsolve but it's faster to compute and solve the Groebner basis ourselves:

    In [106]: %time gb = groebner(system2, [S, V, E, A, I, R])
    CPU times: user 3min 1s, sys: 100 ms, total: 3min 1s
    Wall time: 3min 1s
    

    The Groebner basis puts the system of equations into an almost solved form known as a rational univariate representation (RUR). In this case it looks like

    S - a*R
    V - b*R
    E - c*R
    A - d*R
    I - e*R
    R**2 + f*R + g
    

    where the coefficients a, b, c, d, e, f, g are complicated rational functions of the symbolic parameters in the equations (alpha, beta etc). From here we can follow these steps to solve the Groebner basis:

    1. Solve the first 5 linear equations for S, V, E, A and I in terms of R.
    2. Solve the final quadratic equation for R giving two solutions R1 and R2.
    3. Substitute the the solutions for R1 and R2 into the solutions for S, V, E, A and I.
    4. Put it all together as two solution tuples.

    That is:

    In [115]: syms = [S, V, E, A, I, R]
    
    In [116]: [lsol] = linsolve(gb[:-1], syms[:-1])
    
    In [117]: R1, R2 = roots(gb[-1], R)
    
    In [118]: sol1 = lsol.subs(R, R1) + (R1,)
    
    In [119]: sol2 = lsol.subs(R, R2) + (R2,)
    

    Now we have the two solution tuples in the form that would have been returned by nonlinsolve. Unfortunately the solutions are quite complicated so I won't show them in full. You can get some idea of the complexity by seeing the length of their string representations:

    In [122]: print(len(str(sol1)))
    794100
    
    In [123]: print(len(str(sol2)))
    27850
    

    Now at this point it's worth considering what you actually wanted these solutions for. Maybe it's just that you wanted to substitute some explicit numeric values in for the symbolic parameters. It's worth noting here that potentially it would have been more efficient in the first place to substitute those values into the equations before attempting to solve them symbolically. If you want to see how your solutions depend on some particular parameters say just mi then you can substitute values for everything else and obtain a simpler form of the solution involving only that parameter more quickly:

    In [136]: rep = {alpha:1, beta:2, epsilon:3, gamma_as:4, gamma_s:5, gamma_a:6, exp(-mi*tau):7, N:8, v
         ...: :9}
    
    In [137]: system2 = [eq.subs(rep) for eq in system]
    
    In [138]: %time solve(system2, syms)
    CPU times: user 3.92 s, sys: 4 ms, total: 3.92 s
    Wall time: 3.92 s
    Out[138]: 
    ⎡                              ⎛                                                       ⎛  2        
    ⎢⎛ 8⋅mi     72              ⎞  ⎜4⋅(mi + 5)⋅(mi + 10)   36⋅(mi + 5)⋅(mi + 10)⋅(mi + 12)⋅⎝mi  + 4⋅mi 
    ⎢⎜──────, ──────, 0, 0, 0, 0⎟, ⎜────────────────────, ─────────────────────────────────────────────
    ⎢⎝mi + 9  mi + 9            ⎠  ⎜     7⋅(mi + 9)                  ⎛    4        3         2         
    ⎣                              ⎝                      7⋅(mi + 9)⋅⎝3⋅mi  + 38⋅mi  + 161⋅mi  + 718⋅mi
    
        ⎞                                   ⎛  2          ⎞ ⎛    3        2               ⎞            
    - 25⎠    24⋅(mi + 1)⋅(mi + 5)⋅(mi + 10)⋅⎝mi  + mi + 50⎠⋅⎝3⋅mi  + 41⋅mi  + 209⋅mi + 787⎠  -4⋅(mi + 1
    ───────, ──────────────────────────────────────────────────────────────────────────────, ──────────
          ⎞                 ⎛  2            ⎞ ⎛    4        3         2               ⎞                
     + 900⎠     7⋅(mi + 12)⋅⎝mi  + 4⋅mi - 25⎠⋅⎝3⋅mi  + 38⋅mi  + 161⋅mi  + 718⋅mi + 900⎠           (mi +
    
               ⎛  2          ⎞                ⎛  2          ⎞                  ⎛  2          ⎞ ⎞⎤
    )⋅(mi + 5)⋅⎝mi  + mi + 50⎠   -16⋅(mi + 1)⋅⎝mi  + mi + 50⎠   -8⋅(3⋅mi + 25)⋅⎝mi  + mi + 50⎠ ⎟⎥
    ───────────────────────────, ─────────────────────────────, ───────────────────────────────⎟⎥
         ⎛  2            ⎞                  ⎛  2            ⎞               ⎛  2            ⎞  ⎟⎥
     12)⋅⎝mi  + 4⋅mi - 25⎠        (mi + 12)⋅⎝mi  + 4⋅mi - 25⎠     (mi + 12)⋅⎝mi  + 4⋅mi - 25⎠  ⎠⎦
    

    If you substitute values for all parameters then it's a lot faster:

    In [139]: rep = {alpha:1, beta:2, epsilon:3, gamma_as:4, gamma_s:5, gamma_a:6, exp(-mi*tau):7, N:8, v
         ...: :9, mi:10}
    
    In [140]: system2 = [eq.subs(rep) for eq in system]
    
    In [141]: %time solve(system2, syms)
    CPU times: user 112 ms, sys: 0 ns, total: 112 ms
    Wall time: 111 ms
    Out[141]: 
    ⎡⎛1200  124200  5224320  -960   -256   -640 ⎞  ⎛80  72            ⎞⎤
    ⎢⎜────, ──────, ───────, ─────, ─────, ─────⎟, ⎜──, ──, 0, 0, 0, 0⎟⎥
    ⎣⎝133   55727    67459     23     23     23 ⎠  ⎝19  19            ⎠⎦