How to build an expanding mathematical expression in Python?

Question:

I have a problem where I need to solve a polynomial of a degree that increases with each iteration (loop). The expression is

(1/(1+x)^1) + (1/(1+x)^2) + (1/(1+x)^3) + (1/(1+x)^4) + (1/(1+x)^5)

As you can see, the exponential grows with each iteration and this expression will be used in the function "fsolve". I DO NOT WANT to evaluate the expression, but rather to build it in order to use it in the function "fsolve". (I am using scipy.optimize to import fsolve)
Thanks in advance

Edit

def func(x):
    return["Here i will have my mathematical expression in the question"]
print(fsolve(func, [0.05]))

where 0.05 is a starting guess of the solution to the expression when put equal to zero.

EDIT2
The entire problem is :

C1 - (1-1/(1+x)^n - C2*sum[1/(1+x) + 1/(1+x)^2)+ ... + 1/(1+x)^n]

where C1 and C2 are two constants and n is the iterations. The boundaries of the sum-expression is from 1 to n.

Answers:

I would use a comprehension:

exp = " + ".join([f"(1/(1+x)^{i})" for i in range(1, 6)])
print(exp)

Giving you:

(1/(1+x)^1) + (1/(1+x)^2) + (1/(1+x)^3) + (1/(1+x)^4) + (1/(1+x)^5)
Answered By: JonSG

Scipy offers numerical solvers that expect numerical function as parameters. There is no point to write a function that return of string of the function representation, that will not work.

A simple way to implement what you are asking is making use of factory (here we will use a decorated function):

import numpy as np
from scipy import optimize
    
def factory(order=1):
    @np.vectorize
    def wrapped(x):
        return np.sum([1/np.power(1 + x, i + 1) for i in range(order)])
    return wrapped

The key idea is to pass order parameter to the decorator (factory) which will return the decorated function (wrapped) with a compliant signature for fsolve. For short:

  • wrapped function is your objective function which returns the desired quantity wrt order and x values
  • factory function is a commodity to parametrize the order and avoid to rewrite the whole thing each time you want to investigate a specific order, this function returns the wrapped decorated function.

The @np.vectorize decorator is a numpy commodity to vectorize function. Then the function behaves on array element-wise and returns the results for each value. Handy for plotting the function:

x = np.linspace(-20, 20, 2000)
fig, axe = plt.subplots()
for order in range(1, 6):
    y = factory(order)(x) # This call will return 1 value if not vectorized, returns 2000 values as expected when vectorized
    axe.plot(x, y, label="Order %d" % order)

Then simply iterate through your orders to get the functions you want to solve:

for order in range(1, 11):
    function = factory(order=order)
    solution = optimize.fsolve(function, x0=-2.1)
    print(order, solution)

# 1 [-1.93626047e+83]
# 2 [-2.]
# 3 [-1.64256077e+83]
# 4 [-2.]
# 5 [-1.47348643e+83]
# 6 [-2.]
# 7 [-1.42261052e+83]
# 8 [-2.]
# 9 [-1.43409773e+83]
# 10 [-2.]

Indeed as fsolve is looking for roots and in your setup there are orders with no real root. Analyzing the function of order 5 will show that there is no real roots but rather asymptotic zero when x goes to infinity (which is not a root) as we remains in the real domain. For even orders, the root is easily found by the above procedure.

enter image description here

So your are likely to have float overflow error and sometimes high numbers that are meaningless w.r.t. a root finding problem for odd orders.

OP Update

Based on the new function you have detailed in your post, you can add extra parameters to the objective function as follow:

def factory(order=1):
    @np.vectorize
    def wrapped(x, C1, C2):
        return C1 - (1 - 1/np.power(1 + x, order) - C2*np.sum([1/np.power(1 + x, i + 1) for i in range(order)]))
    return wrapped

Then we solve it for several orders as usual, I picked (C1, C2) = (10,10):

for order in range(1, 11):
    function = factory(order=order)
    solution = optimize.fsolve(function, x0=-2.1, args=(10, 10))
    print(order, solution)

# 1 [-2.22222222]
# 2 [-3.20176167]
# 3 [-2.10582151]
# 4 [-2.73423813]
# 5 [-2.06950043]
# 6 [-2.54187609]
# 7 [-2.0517491]
# 8 [-2.43340793]
# 9 [-2.04122279]
# 10 [-2.36505166]
Answered By: jlandercy

If you can relax the usage of fsolve you can solve this problem for complex numbers as well simply by using numpy.polynomials and doing a bit of algebra.

First realize the roots of your function are the roots of numerator which is a polynomial:

def numerator(order=1):
    term = Polynomial([1, 1])  # m(x) = x + 1
    return sum([term**i for i in range(order)])

Then just apply roots method for each order you want to analyze:

for i in range(1, 11):
    print(i, numerator(order=i).roots())

# 1 []
# 2 [-2.]
# 3 [-1.5-0.8660254j -1.5+0.8660254j]
# 4 [-2.+0.j -1.-1.j -1.+1.j]
# 5 [-1.80901699-0.58778525j -1.80901699+0.58778525j -0.69098301-0.95105652j
#    -0.69098301+0.95105652j]
# 6 [-2. +0.j        -1.5-0.8660254j -1.5+0.8660254j -0.5-0.8660254j
#    -0.5+0.8660254j]
# 7 [-1.90096887-0.43388374j -1.90096887+0.43388374j -1.22252093-0.97492791j
#     -1.22252093+0.97492791j -0.3765102 -0.78183148j -0.3765102 +0.78183148j]
# 8 [-2.        +0.j         -1.70710678-0.70710678j -1.70710678+0.70710678j
#    -1.        -1.j         -1.        +1.j         -0.29289322-0.70710678j
#    -0.29289322+0.70710678j]
# 9 [-1.93969262-0.34202014j -1.93969262+0.34202014j -1.5       -0.8660254j
#    -1.5       +0.8660254j  -0.82635182-0.98480775j -0.82635182+0.98480775j
#    -0.23395556-0.64278761j -0.23395556+0.64278761j]
# 10 [-2.        +0.j         -1.80901699-0.58778525j    -1.80901699+0.58778525j
#     -1.30901699-0.95105652j -1.30901699+0.95105652j -0.69098301-0.95105652j
#     -0.69098301+0.95105652j -0.19098301-0.58778525j -0.19098301+0.58778525j]
Answered By: jlandercy

Are you sure that you only want five terms? Analytically, with infinite terms your entire expression evaluates to 1 + 1/x, which is trivially invertible.

Answered By: Reinderien