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)
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.
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]
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]
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.
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
.
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)
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 wrtorder
andx
valuesfactory
function is a commodity to parametrize theorder
and avoid to rewrite the whole thing each time you want to investigate a specificorder
, this function returns thewrapped
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.
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]
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]
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.