PARI/GP code to approximate Kneser tetration(base e) without using schroder/abel
#1
Hello, for the past few days I have succesfully developed an algorithm in PARI/GP to approximate Kneser tetration for Base \(e\) on \([0,1]\) with precision around ~\( 10^{-15} \) (numerically compared to fatou.gp) which is near the Double Machine Epsilon precision. The algorithm doesn't use standard computational objects like Schroder or Abel because I was frustrated with the difficult of numerically implementing those functions in PARI/GP so instead I have developed another method that does not build those computational objects. This is going to be a bit of a long post, but i will try to explain all of the theory behind this

This is my algorithm to approximate Kneser Tetration for Base \(e\) on \([0,1]\):
Code:
\\ Numerical algorithm to calculate an approximation of Kneser tetration on the interval [0,1]
\\ this is V2 which includes taking a pade approximant to make the error near ~10^-15 on the specificied interval
\\ fixed point for f(x) = exp(x) + b
default(seriesprecision, 30);
L(x) = x-lambertw(-exp(x),-1);
\\ faa-di-bruno reccurence series for the superfunction of f(x) = exp(x) + b.
\\ Works for b < -1 since the fixed point is real and satisfies the real-to-real function needed
c1(x,b)=exp(x*L(b));
c2(x,b)=((exp(x*L(b))*(exp(x*L(b))-1))/(exp(L(b))-1));
\\ here k basically recenters the superfunction so that superf(0)=k.
superfap(x,b,k) = L(b) + c1(x,b)*(k-L(b)) + (c2(x,b)/2!)*((k-L(b))^2);
\\ This recursion is needed to approximate the superfunction of f(x) = exp(x) + b better. its a numerical trick
r(x, b, n) = if (n<-1,log(r(x,b,n+1)-b),if(n==-1,log(x-b),if(n==0,x,if(n==1,exp(x)+b,if(n>1,exp(r(x,b,n-1))+b)))));
\\ the n for superfimp(x,b,n) recenters the superfunction superf, then recenters it back so that superfimp(0,b,n) = 1, this is to improve accuracy
superfimp(x,b,n) = r(superfap(x,b,r(1,b,-n)),b,n);
\\takes a taylor series centered at -1.05 to calculate the superfunction f(x) = exp(x) + b where b=0 so we can just get sexp(x)
\\ This code has an avg error of around ~10^-15 on the specified interval. its pretty good. the only limiting factor here is how we get from b = -1.05 to b = 0
sexp(o) = {
default(realprecision, 300);
my(L = subst(bestapprPade(Ser(superfimp(o,x-1.05,300),x,30)),x,1.05));
return(precision(L,38));
}

Let me explain the general process of how I get a superfunction. Also, I would like to mention that the things I mention here may or may not be accurate, I use the things i am explaining here to numerically solve for superfunctions, this is not a rigorous framework, it was built to compute.

Let \( \operatorname{g}(x) \) be the Super function of \( \operatorname{f}(x) \) which means that \( \operatorname{g}(x) = \operatorname{f}(\operatorname{g}(x-1)) \). Let \(\operatorname{g}(0) = k\).

For \(n \in \mathbb{C}\):

\[
\operatorname{g}(x) = \operatorname{f}^{\circ (n)}(\operatorname{g}(x-n))
\]

Using \( \operatorname{g}(x) = \operatorname{f}^{\circ (n)}(\operatorname{g}(x-n)) \) we can then:

\[
\operatorname{g}(x) = \operatorname{f}^{\circ (n)}(\operatorname{g}(x-n))  \Rightarrow  \operatorname{g}(x+n) = \operatorname{f}^{\circ (n)}(\operatorname{g}(x))
\]

And so substituting \(x\) with \(\operatorname{g}^{\circ (-1)}(x)\) in \(\operatorname{g}(x+n) = \operatorname{f}^{\circ (n)}(\operatorname{g}(x)) \) we get that:

\[
\operatorname{f}^{\circ (n)}(x) = \operatorname{g}(\operatorname{g}^{\circ (-1)}(x)+n)
\]

We know that \(\operatorname{g}(0) = k\) and so applying the function \(\operatorname{g}^{\circ (-1)}(x)\) on both sides for \(\operatorname{f}^{\circ (n)}(x) = \operatorname{g}(\operatorname{g}^{\circ (-1)}(x)+n)\) we get:

\[
\operatorname{g}^{\circ {-1}}(k) = 0
\]

And so substituting \(x\) with \(k\) in \(\operatorname{f}^{\circ (n)}(x) = \operatorname{g}(\operatorname{g}^{\circ (-1)}(x)+n) \) we get for \(n \in \mathbb{C}\):

\[
\operatorname{f}^{\circ (n)}(k) = \operatorname{g}(n)
\]

This dissolves the problem into finding the nth functional iteration of \(\operatorname{f}(x)\). Now we can start solving for a local solution for \(\operatorname{f}^{\circ (n)}(x)\) around the fixed point of \(\operatorname{f}(x)\).
_________________________________________________________________________________________________________________________________________________________________________________________
Let \(L=\operatorname{f}(L)\) and \(\lambda = \operatorname{f}'(L)\) where \(\lambda\) must not equal \(0\).
For \(n \in \mathbb{C}\) we can actually solve for the first derivative of \(\operatorname{h}(x)\) where \(\operatorname{h}^{\circ (n)}(x) = \operatorname{f}(x)\) which means that \(\operatorname{h}(x) = \operatorname{f}^{\frac{1}{n}}(x)\).
But first we need to establish an identity where \(\operatorname{h}(L) = L\). Now we can continue.
For example for \(n=2\):
\[
\operatorname{h}(\operatorname{h}(x)) = \operatorname{f}(x)
\]
taking the derivative of both sides:
\[
\frac{d}{dx}\operatorname{h}(\operatorname{h}(x)) = \frac{d}{dx}\operatorname{f}(x)
\]
\[
\operatorname{h}'(x) \cdot \operatorname{h}'(\operatorname{h}(x)) = \operatorname{f}'(x)
\]
substituting \(x\) with \(L\):
\[
\operatorname{h}'(L) \cdot \operatorname{h}'(L) = \operatorname{f}'(L)
\]
and so:
\[
\operatorname{h}'(L) = \operatorname{f}'(L)^{\frac{1}{2}} = \lambda^{\frac{1}{2}}
\]
We can do the same for \(n=3\):
\[
\operatorname{h}(\operatorname{h}(\operatorname{h}(x))) = \operatorname{f}(x)
\]
taking the derivative of both sides:
\[
\frac{d}{dx}\operatorname{h}(\operatorname{h}(\operatorname{h}(x))) = \frac{d}{dx}\operatorname{f}(x)
\]
\[
\operatorname{h}'(x) \cdot \operatorname{h}'(\operatorname{h}(x)) \cdot \operatorname{h}'(\operatorname{h}(\operatorname{h}(x))) = \operatorname{f}'(x)
\]
substituting \(x\) with \(L\):
\[
\operatorname{h}'(L) \cdot \operatorname{h}'(L) \cdot \operatorname{h}'(L) = \operatorname{f}'(L)
\]
and so:
\[
\operatorname{h}'(L) = \operatorname{f}'(L)^{\frac{1}{3}} = \lambda^{\frac{1}{3}}
\]
We can do this many times for other \(n\) where \(n \in \mathbb{N}\), and we quickly find a pattern where:
\[
\operatorname{h}'(L) = \lambda^{\frac{1}{n}}
\]
And since \(\operatorname{h}(x) = \operatorname{f}^{\circ (\frac{1}{n})}(x)\) then:
\[
(\operatorname{f}^{\circ (\frac{1}{n})})'(L) = \lambda^{\frac{1}{n}}
\]
and so:
\[
(\operatorname{f}^{\circ (n)})'(L) = \lambda^{n}
\]

To get higher order derivatives we can use a general formula where \(\operatorname{B}_{k,n}\) are the Partial Bell Polynomials and \(\lambda \not= 1\)(i found this formula on some math forum i cant find anymore):
\[ 
(\operatorname{f}^{\circ (n)})^{c}(L) = \frac{\left(\sum_{m=2}^{c} \operatorname{f}^{(m)}(L) \cdot \operatorname{B}_{c,m}\left( (\operatorname{f}^{\circ (n)})'(L), (\operatorname{f}^{\circ (n)})''(L), \dots, (\operatorname{f}^{\circ (n)})^{(c-m+1)}(L) \right)\right) - \left(\sum_{m=1}^{c-1} \left((\operatorname{f}^{\circ (n)})^{(m)}(L) \cdot \operatorname{B}_{c,m}(\operatorname{f}'(L), \operatorname{f}''(L), \dots, \operatorname{f}^{(c-m+1)}(L))\right) \right)}{\lambda^{c} - \lambda} 
\]

This formula seems to work well after manually checking it numerically for different derivatives and functions.
However for \(\lambda = 1\)(parabolic fixed point) the method is quite different since the formula above only works for a fixed point where \(\lambda \not= 1\).
I have made some python code for the fixed point where \(\lambda = 1\):
Code:
# this is a resource, basically for derivative when f'(L)=1. this is Version 56, lets hope i dont need to make more versions.
import math
from fractions import Fraction
def bellpepper(n, k):
    # Generates partial Bell polynomial(i forgot how this works again, if it works dont touch it)
    if n == 0 and k == 0: return [(1, ())]
    if n == 0 or k == 0: return []
    res = []
    for m in range(1, n - k + 2):
        for coeff, indices in bellpepper(n - m, k - 1):
            res.append((coeff * math.comb(n - 1, m - 1), (m,) + indices))
    return res
def evaluate_sequence(n_max):
# Simulates the true numerical steps of the derivative histat a fixed point where f(L)   L and f'(L) = 1, if not then it simulates something atleast
    hist = []
# KEEP IT CLEAN. KEEP IT CLEAN. KEEP IT CLEAN. REMEMBER YOU CANT READ THE CODE WHEN DEBUGGING  
    h0 = {1: {(): Fraction(1)}}
    for i in range(2, n_max + 1): h0[i] = {}
    hist.append(h0)
   
    for x in range(1, n_max + 2):
        prev = hist[-1]
        curr = {1: {(): Fraction(1)}}
        for n in range(2, n_max + 1):
            curr_n = {}
            for a in range(1, n + 1):
                bell_terms = bellpepper(n, a)
                for b_coeff, indices in bell_terms:
                    term_profile = {(): Fraction(b_coeff)}
                    for idx in indices:
                        new_profile = {}
                        for p1, c1 in term_profile.items():
                            for p2, c2 in prev[idx].items():
                                m = max(len(p1), len(p2))
                                e1 = p1 + (0,) * (m - len(p1))
                                e2 = p2 + (0,) * (m - len(p2))
                                f_res = tuple(a_val + b_val for a_val, b_val in zip(e1, e2))
                                new_profile[f_res] = new_profile.get(f_res, 0) + c1 * c2
                        term_profile = new_profile
                    for p, c in term_profile.items():
                        if a >= 2:
                            m = max(len(p), a - 1)
                            e = p + (0,) * (m - len(p))
                            e_list = list(e)
                            e_list[a - 2] += 1
                            final_p = tuple(e_list)
                        else:
                            final_p = p
                        curr_n[final_p] = curr_n.get(final_p, 0) + c
            curr[n] = {k: v for k, v in curr_n.items() if v != 0}
        hist.append(curr)
    return hist
def get_nth_derivative_expansion(n_max):
    #Performs Langrange interpolation, or atleast tries to
    hist = evaluate_sequence(n_max)
    formulas = {}
    x_points = list(range(len(hist)))
   
    for n in range(1, n_max + 1):
        all_profiles = set()
        for h in hist:
            for p in h[n].keys(): all_profiles.add(p)
               
        formulas[n] = {}
        for p in all_profiles:
            y_points = [h[n].get(p, Fraction(0)) for h in hist]
            poly = {0: Fraction(0)}
            for i, y in enumerate(y_points):
                if y == 0: continue
                term = {0: Fraction(1)}
                denom = 1
                for j in range(len(x_points)):
                    if j == i: continue
                    denom *= (i - j)
                    new_term = {}
                    for pow, coeff in term.items():
                        new_term[pow + 1] = new_term.get(pow + 1, 0) + coeff
                        new_term[pow] = new_term.get(pow, 0) - coeff * j
                    term = new_term
                scale = Fraction(y, denom)
                for pow, coeff in term.items():
                    poly[pow] = poly.get(pow, 0) + coeff * scale
            formulas[n][p] = {k: v for k, v in poly.items() if v != 0}
    return formulas
def print_nth_expansion(target_n):
    # does something i forgot, im writing these comments 3 months later
    if target_n < 1: return
    all_formulas = get_nth_derivative_expansion(target_n)
   
    terms = []
    for p, x_poly in all_formulas[target_n].items():
        f_parts = []
        for idx, power in enumerate(p):
            if power == 0: continue
            deriv_order = idx + 2
            base_str = "f''" if deriv_order == 2 else "f'''" if deriv_order == 3 else f"f^({deriv_order})"
            f_parts.append(f"({base_str}(L))^{power}" if power > 1 else f"{base_str}(L)")
        f_label = " * ".join(f_parts) if f_parts else "1"
       
        x_parts = []
        for pow, coeff in sorted(x_poly.items()):
            coeff_str = str(coeff) if coeff.denominator == 1 else f"({coeff.numerator}/{coeff.denominator})"
            if pow == 0: x_parts.append(f"{coeff_str}")
            elif pow == 1: x_parts.append(f"{coeff_str}*x")
            else: x_parts.append(f"{coeff_str}*x^{pow}")
        x_str = " + ".join(x_parts)
        terms.append(f"[{f_label}] * ({x_str})")
       
    print(f"c(x, {target_n}) = " + " + ".join(terms))
if __name__ == "__main__":
    # change n to get derivative. yay
    target_derivative_order = 5
    print_nth_expansion(target_derivative_order)
And the code does seem to work very well. I have used this code to extend Product Tetration: https://tetrationforum.org/showthread.php?tid=1808.
Now that we can solve for \((\operatorname{f}^{\circ (n)})^{c}(L)\), we can approximate a local continuation for \(\operatorname{g}(x)\) using \((\operatorname{f}^{\circ (n)})^{c}(L)\):

\[
\operatorname{g}(x) \approx\ L + \sum_{n=1}^{\infty} (\operatorname{f}^{\circ (x)})^{n}(L)(k-L)^{n}
\]

or:

\[
\operatorname{g}(x) \approx\ L + \lambda^{x}(k-L) + \frac{\operatorname{f}''(L)(\lambda^{2x} - \lambda^{x})}{\lambda^{2} - \lambda}(k-L)^{2} ...
\]

 I have some important conditions when numerically calculating the local approximation of \(\operatorname{g}(x)\) to ensure stability(this is a work in progress and more conditions maybe added later on):

1. If \( \lim_{n \to -\infty} \operatorname{g}(n) \) diverges and \( \lim_{n \to \infty} \operatorname{g}(n) \) converges and is the fixed point of \( \operatorname{f}(x) \) then that should be L, equivalently if \( \lim_{n \to \infty} \operatorname{g}(n) \) diverges and \( \lim_{n \to -\infty} \operatorname{g}(n) \) converges and is the fixed point of \( \operatorname{f}(x) \) then that should be L. If both \( \lim_{n \to -\infty} \operatorname{g}(n) \) and \( \lim_{n \to \infty} \operatorname{g}(n) \) converges and is the fixed point of \(\operatorname{f}(x)\) then either of them could be used. 

2. The region where \(\operatorname{g}(x)\) converges to the fixed point \(L\) must be almost linear, if not then it is going to be a pain trying to use the local approximation for \(\operatorname{g}(x)\).

3. If the function \(\operatorname{g}(x)\) is meant to output real numbers for \(x \in \mathbb{R}\) then the fixed point of \(\operatorname{f}(x)\) should be real and \(\lambda \not= 1\), this condition is important because a real-to-real function with a complex fixed point is hard to force that real-to-real condition on.

4. \(\operatorname{g}(x)\) must not contain a lot of exploding singularities in the direction where the function \(\operatorname{f}(x)\) fixed point is, if it does then you cannot use a finite series and you must use another method to bypass those exploding singularities.

Now that we have the numerical conditions laid out, I can show a numerical technique to get tons of precision out of a finite series for \(\operatorname{g}(x)\), where \(\operatorname{g}(x)\) follows the numerical conditions.
Now lets define a function where \(N\) is a finite real number:
\[
\operatorname{g}_{approx}(x,k) = L + \sum_{n=1}^{N} (\operatorname{f}^{\circ (x)})^{n}(k-L)^{n}
\]
I usually use \(N=2\) which is a quadratic series. Let \(dir\) be the direction where the function \(\operatorname{g}(x)\) converges to the fixed point \(L\)(for example: \(dir = -\infty\)).
If we set \(k\) in \(\operatorname{g}_{approx}(x,k)\) closer to the fixed point we see that the function is almost linear there. We can check how good the approximation is by doing \(\\operatorname{f}(\operatorname{g}_{approx}(0,k)) - \operatorname{g}_{approx}(1,k)\), as \(\operatorname{f}(\operatorname{g}_{approx}(0,k)) - \operatorname{g}_{approx}(1,k)\) gets closer to 0 the better the approximation is. And so a improved approximation of \(\operatorname{g}(x)\) is:
\[
\operatorname{f}^{\circ (-dir)}(\operatorname{g}_{approx}(x,\operatorname{f}^{\circ (dir)}(k)))
\]
This is what I call 'the re-centering trick'
Now that I have got the basics laid out, I can now explain how the code works.
First of all we cannot directly calculate \(\operatorname{g}(x) = e^{\operatorname{g}(x-1)}\) using this method, as it is a function where a large portion of its real plane is meant to be real(\(x \in [-2,\infty]\)) but its fixed point is complex(which violates numerical condition 3), and in the direction where the function converges to the fixed point there is a lot of singularities(which violates numerical condition 4).
So instead of looking at \(\operatorname{g}(x) = e^{\operatorname{g}(x-1)}\), instead for \(b < -1\) we look at:
\[
\operatorname{g}(x) = e^{\operatorname{g}(x)} - b
\]
This means that \(\operatorname{f}(x) = e^{x} - b\) 
The reason we look at the function \(\operatorname{g}(x) = e^{\operatorname{g}(x)} - b\) for \(b<-1\) is because the fixed point is real and the function is meant to output a real number for all reals which means it satisfies numerical condition 3, also for \(b < -1\) the function has no singularities in its real plane which satisfies numerical condition 4, and the region where the function \(\operatorname{g}(x)\) converges to the fixed point is almost linear which satisfies numerical condition 2.

According to Numerical condition 1 the fixed point would be:
\[
L = b - \operatorname{W}_{-1}(-e^{b})
\]
Where \(\operatorname{W}_{-1}\) is the -1th branch of the Lambert W function. The reason we are using the -1th branch is because that the direction where \(\operatorname{g}(x)\) converges to the fixed point is to the left of the real axis(meaning that \(dir = -\infty\)) and the -1th branch gets us that fixed point.

In my code the fixed point for \(b\) is defined as \(L(x) = x - \operatorname{W}_{-1}(-e^{x})\) with this i construct a quadratic approximation of \(\operatorname{g}(x) = e^{\operatorname{g}(x)} - b\):
\[
\operatorname{superfap}(x,b,k) = L(b) + e^{x \cdot L(b)}(k-L(b)) + \frac{e^{x \cdot L(b)} \cdot (e^{x \cdot L(b)} - 1)}{e^{L(b)} - 1}
\]
This is not accurate enough at \(k = 1\) so what we do is use our recentering trick from earlier to improve accuracy by a ton:
\[
\operatorname{superfimp}(x,b,n) = \operatorname{f}^{\circ (n)}(\operatorname{superfap}(x,b,\operatorname{f}^{\circ (-n)}(1)))
\]
In my code \(\operatorname{r}(x,b,n) = \operatorname{f}^{\circ (n)}(x)\). To get \(\operatorname{superfimp}(x,0,n)\)(this is just \(\operatorname{sexp}(x)\)) where in my code \(n\) is set to 300 we have to analytically continue from a point close to -1 to 0. In my code the point is hardcoded as -1.05 because at -1 \(\lambda = 1\) which makes the coefficient of \((k-L)^{2}\) equal to 0, and so the approximation becomes unstable near -1, and that is why i use -1.05 instead of something like -1.02. We can just take a taylor series at \(x=-1.05\), which is exactly what V1 does which achieves an accuracy of ~\(10^{-10}\)(V1 uses a 18 degree taylor series because that is the most accurate as it can get):
Code:
\\ Numerical algorithm to calculate an approximation of Kneser tetration on the interval [0,1]
\\ this is V1 which includes taking a series using Ser(expr,var,degree) to make the error near ~10^-10 on the specificied interval
\\ fixed point for f(x) = exp(x) + b

default(seriesprecision, 30);
L(x) = x-lambertw(-exp(x),-1);

\\ faa-di-bruno reccurence series for the superfunction of f(x) = exp(x) + b.
\\ Works for b < -1 since the fixed point is real and satisfies the real-to-real function needed
c1(x,b)=exp(x*L(b));
c2(x,b)=((exp(x*L(b))*(exp(x*L(b))-1))/(exp(L(b))-1));

\\ here k basically recenters the superfunction so that superf(0)=k.
superfap(x,b,k) = L(b) + c1(x,b)*(k-L(b)) + (c2(x,b)/2!)*((k-L(b))^2);

\\ This recursion is needed to approximate the superfunction of f(x) = exp(x) + b better. its a numerical trick
r(x, b, n) = if (n<-1,log(r(x,b,n+1)-b),if(n==-1,log(x-b),if(n==0,x,if(n==1,exp(x)+b,if(n>1,exp(r(x,b,n-1))+b)))));

\\ the n for superfimp(x,b,n) recenters the superfunction superf, then recenters it back so that superfimp(0,b,n) = 1, this is to improve accuracy
superfimp(x,b,n) = r(superfap(x,b,r(1,b,-n)),b,n);

\\takes a taylor series centered at -1.05 to calculate the superfunction f(x) = exp(x) + b where b=0 so we can just get sexp(x)
\\ This code has an avg error of around ~10^-10 on the specified interval. its pretty good. the only limiting factor here is how we get from b = -1.05 to b = 0
sexp(o) = {
default(realprecision, 300);
my(L = subst(Pol(Ser(superfimp(o,x-1.05,300),x,18)),x,1.05));
return(precision(L,38));
}

However in V2 it uses a 30 degree taylor series then applies a Pade approximant to it to accelerate convergence making the precision higher(around ~\(10^{-15}\)). Also to increase precision, you would need to use a better numerical technique to analytically continue from \(b=-1.05\) to \(b=0\). I know I didn't provide numerical error analysis in this post, that is because im kinda lazy to do it. Maybe in the near future I could provide some numerical error analysis for V2. Anyways for V3 i might generalize the algorithm for all bases except for 0 and 1 and maybe in a later version use a Borel-conformal resumption. This was a fun side project to do. If you do have any questions, feel free to ask!
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Code to calculate tetration using my method Shanghai46 10 16,637 12/17/2024, 01:02 PM
Last Post: MorgothV8
  Writing Kneser's super logarithm using values of Kneser at a single point JmsNxn 1 5,514 04/21/2023, 04:26 AM
Last Post: JmsNxn
  Terse Schroeder & Abel function code Daniel 1 4,360 10/16/2022, 07:03 AM
Last Post: Daniel
Question Computing Kneser's Super Logarithm and its Analytic Continuation Catullus 2 6,831 07/10/2022, 04:04 AM
Last Post: Catullus
  fast accurate Kneser sexp algorithm sheldonison 40 194,853 07/03/2022, 06:38 AM
Last Post: JmsNxn
  Quickest way to compute the Abel function on the Shell-Thron boundary JmsNxn 0 4,302 04/21/2022, 01:52 AM
Last Post: JmsNxn
  Brjuno number in Pari-GP Ember Edison 1 5,454 10/24/2021, 03:32 AM
Last Post: Ember Edison
  Creating Hue Plots in Pari-GP JmsNxn 4 9,952 09/25/2021, 01:20 AM
Last Post: JmsNxn
  Revisting my accelerated slog solution using Abel matrix inversion jaydfox 22 74,115 05/16/2021, 11:51 AM
Last Post: Gottfried
  Kneser-iteration on n-periodic-points (base say \sqrt(2)) Gottfried 11 23,303 05/05/2021, 04:53 AM
Last Post: Gottfried



Users browsing this thread: 1 Guest(s)