%mathpiper,def="SumTaylorNum"

/// coded by Serge Winitzki. See essays documentation for algorithms.

//////////////////////////////////////////////////
/// Numerical method: Taylor series, rectangular summation
//////////////////////////////////////////////////

/// Fast summation of Taylor series using a rectangular scheme.
/// SumTaylorNum(x, nth'term'func, n'terms) = Sum(k, 0, n'terms, nth'term'func(k)*x^k)
/// Note that sufficient precision must be preset to avoid roundoff errors (these methods do not modify precision).
/// The only reason to try making these functions HoldArgument is to make sure that the closures nth'term'func and next'term'factor are passed intact. But it's probably not desired in most cases because a closure might contain parameters that should be evaluated.

/// The short form is used when only the nth term is known but no simple relation between a term and the next term.
/// The long form is used when there is a simple relation between consecutive terms. In that case, the n'th term function is not needed, only the 0th term value.

/// SumTaylorNum0 is summing the terms with direct methods (Horner's scheme or simple summation). SumTaylorNum1 is for the rectangular method.

/// nth'term'func and next'term'func must be functions applicable to one argument.

/// interface
SumTaylorNum0(_x, _nth'term'func, _n'terms) <-- SumTaylorNum0(x, nth'term'func, {}, n'terms);

SumTaylorNum1(_x, _nth'term'func, _n'terms) <-- SumTaylorNum1(x, nth'term'func, {}, n'terms);

/// interface
SumTaylorNum(_x, _nth'term'func, _n'terms) <--
If(
	n'terms >= 30,	// threshold for calculation with next'term'factor
	// use the rectangular algorithm for large enough number of terms
	SumTaylorNum1(x, nth'term'func, n'terms),
	SumTaylorNum0(x, nth'term'func, n'terms)
);

SumTaylorNum(_x, _nth'term'func, _next'term'factor, _n'terms) <--
If(
	n'terms >= 5,	// threshold for calculation with next'term'factor
	SumTaylorNum1(x, nth'term'func, next'term'factor, n'terms),
	SumTaylorNum0(x, nth'term'func, next'term'factor, n'terms)
);
//HoldArgumentNumber(SumTaylorNum, 3, 2);

/// straightforward algorithms for a small number of terms
1# SumTaylorNum0(_x, _nth'term'func, {}, _n'terms) <--
[
	Local(sum, k);
  N([
    // use Horner scheme starting from the last term
    x:=Eval(x);
    sum := 0;
    For(k:=n'terms, k>=0, k--)
      sum := AddN(sum*x, nth'term'func @ k);
  ]);
	sum;
];

//HoldArgumentNumber(SumTaylorNum0, 3, 2);

2# SumTaylorNum0(_x, _nth'term'func, _next'term'factor, _n'terms) <--
[
	Local(sum, k, term, delta);
  N([
    x:=Eval(x);	// x must be floating-point
    If (IsConstant(nth'term'func),
      term := nth'term'func,
      term := (nth'term'func @ {0}),
    );
    sum := term;	// sum must be floating-point
  ]);
  NonN([
    delta := 1;
    For(k:=1, k<=n'terms And delta != 0, k++)
    [
      term := MultiplyNum(term, next'term'factor @ {k}, x);	// want to keep exact fractions here, but the result is floating-point
      delta := sum;
      sum := sum + term;	// term must be floating-point
      delta := Abs(sum-delta);	// check for underflow
    ];
  ]);
	sum;
];

/// interface
SumTaylorNum0(_x, _nth'term'func, _n'terms) <-- SumTaylorNum0(x, nth'term'func, {}, n'terms);

//HoldArgumentNumber(SumTaylorNum0, 4, 2);
//HoldArgumentNumber(SumTaylorNum0, 4, 3);

/// this is to be used when a simple relation between a term and the next term is known.
/// next'term'factor must be a function applicable to one argument, so that if term = nth'term'func(k-1), then nth'term'func(k) = term / next'term'factor(k). (This is optimized for Taylor series of elementary functions.) In this case, nth'term'func is either a number, value of the 0th term, or a function.
/// A special case: when next'term'factor is an empty list; then we act as if there is no next'term'factor available.
/// In this case, nth'term'func must be a function applicable to one argument.
/// Need IntLog(n'terms, 10) + 1 guard digits due to accumulated roundoff error.
SumTaylorNum1(x, nth'term'func, next'term'factor, n'terms) :=
[
	// need Sqrt(n'terms/2) units of storage (rows) and Sqrt(n'terms*2) columns. Let's underestimate the storage.
	Local(sum, rows, cols, rows'tmp, last'power, i, j, x'power, term'tmp);
  N([ // want to keep exact fractions
    x:=Eval(x);	// x must be floating-point
    rows := IntNthRoot(n'terms+1, 2);
    cols := Quotient(n'terms+rows, rows);	// now: rows*cols >= n'terms+1
    Check(rows>1 And cols>1, "Argument", "SumTaylorNum1: Internal error: number of Taylor sum terms must be at least 4");
    rows'tmp := ArrayCreate(rows, 0);
    x'power := x ^ rows;	// do not use PowerN b/c x might be complex
    // initialize partial sums (array rows'tmp) - the 0th column (i:=0)
    // prepare term'tmp for the first element
    // if we are using next'term'factor, then term'tmp is x^(rows*i)*a[rows*i]
    // if we are not using it, then term'tmp is x^(rows*i)
    If(
      next'term'factor = {},
      term'tmp := 1,
  //		term'tmp := (nth'term'func @ 0)	// floating-point
      If (IsConstant(nth'term'func),
        term'tmp := nth'term'func,
        term'tmp := (nth'term'func @ {0}),
      )
    );
  ]);
  NonN([ // want to keep exact fractions below
    // do horizontal summation using term'tmp to get the first element
    For(i:=0, i<cols, i++)
    [
      // add i'th term to each row
      For(j:=0, j<rows And (i<cols-1 Or i*rows+j<=n'terms), j++)	// do this unless we are beyond the last term in the last column
      [
        // if we are using next'term'factor, then term'tmp is x^(rows*i)*a[rows*i]
        // if we are not using it, then term'tmp is x^(rows*i)
        If(
          next'term'factor = {},	// no next'term'factor, so nth'term'func must be given
          [
            rows'tmp[j+1] := rows'tmp[j+1] + MultiplyNum(term'tmp, nth'term'func @ {i*rows+j});
          ],
          [
            rows'tmp[j+1] := rows'tmp[j+1] + term'tmp;	// floating-point
            term'tmp := MultiplyNum(term'tmp, next'term'factor @ {i*rows+j+1});	// arguments may be rational but the result is floating-point
          ]
        );
      ];
      // update term'tmp for the next column
      term'tmp := term'tmp*x'power;	// both floating-point
    ];
    // do vertical summation using Horner's scheme
    // now x'power = x^cols
    For([j:=rows; sum:=0;], j>0, j--)
      sum := sum*x + rows'tmp[j];
  ]);
	sum;
];

//HoldArgumentNumber(SumTaylorNum, 4, 2);
//HoldArgumentNumber(SumTaylorNum, 4, 3);

/*
Examples:
In> SumTaylorNum(1,{{k}, 1/k!},{{k}, 1/k}, 10 )
Result: 2.7182818006;
In> SumTaylorNum(1,{{k},1/k!}, 10 )
Result: 2.7182818007;
*/

%/mathpiper



%mathpiper_docs,name="SumTaylorNum",categories="User Functions;Series"
*CMD SumTaylorNum --- optimized numerical evaluation of Taylor series
*STD
*CALL
	SumTaylorNum(x, NthTerm, order)
	SumTaylorNum(x, NthTerm, TermFactor, order)
	SumTaylorNum(x, ZerothTerm, TermFactor, order)

*PARMS

{NthTerm} -- a function specifying $n$-th coefficient of the series

{ZerothTerm} -- value of the $0$-th coefficient of the series

{x} -- number, value of the expansion variable

{TermFactor} -- a function specifying the ratio of $n$-th term to the previous one

{order} -- power of $x$ in the last term

*DESC

{SumTaylorNum} computes a Taylor series $Sum(k,0,n,a[k]*x^k)$
numerically. This function allows very efficient computations of
functions given by Taylor series, although some tweaking of the
parameters is required for good results.

The coefficients $a[k]$ of the Taylor series are given as functions of one integer variable ($k$). It is convenient to pass them to {SumTaylorNum} as closures.
For example, if a function {a(k)} is defined, then
	SumTaylorNum(x, {{k}, a(k)}, n)
computes the series $Sum(k, 0, n, a(k)*x^k)$.

Often a simple relation between successive coefficients $a[k-1]$,
$a[k]$ of the series is available; usually they are related by a
rational factor. In this case, the second form of {SumTaylorNum} should
be used because it will compute the series faster. The function
{TermFactor} applied to an integer $k>=1$ must return the ratio
$a[k]$/$a[k-1]$. (If possible, the function {TermFactor} should return
a rational number and not a floating-point number.) The function
{NthTerm} may also be given, but the current implementation only calls
{NthTerm(0)} and obtains all other coefficients by using {TermFactor}.
Instead of the function {NthTerm}, a number giving the $0$-th term can be given.

The algorithm is described elsewhere in the documentation.
The number of terms {order}+1
must be specified and a sufficiently high precision must be preset in
advance to achieve the desired accuracy.
(The function {SumTaylorNum} does not change the current precision.)

*E.G.
To compute 20 digits of $Exp(1)$ using the Taylor series, one needs 21
digits of working precision and 21 terms of the series. 

In> BuiltinPrecisionSet(21)
Result: True;
In> SumTaylorNum(1, {{k},1/k!}, 21)
Result: 2.718281828459045235351;
In> SumTaylorNum(1, 1, {{k},1/k}, 21)
Result: 2.71828182845904523535;
In> SumTaylorNum(1, {{k},1/k!}, {{k},1/k}, 21)
Result: 2.71828182845904523535;
In> RoundTo(N(Ln(%)),20)
Result: 1;


*SEE Taylor
%/mathpiper_docs