Source code for scitools.convergencerate

#!/usr/bin/env python
"""
Module for estimating convergence rate of numerical algorithms,
based on data from a set of experiments.

Recommended usage:
Vary only discretization parameter (h in spatial problems, h/dt**q for
some q so that h/dt**q=const, in time-dependent problems with time step dt).
Use class OneDiscretizationPrm, or the function convergence_rate,
or the analyze_filedata convenience function. Start with reading
the convergence_rate function (too see an easily adapted example).

(There is support for fitting more general error models, like
C1*h**r1 + C2*h*dt**r2, with nonlinear least squares, but sound
fits are more challenging to obtain.)
"""
from scitools.misc import import_module
from numpy import zeros, array, asarray, log10, transpose, linalg, linspace
import sys
from scitools.std import plot

__all__ = ['ManyDiscretizationPrm',
           'OneDiscretizationPrm',
           ]

log = log10
inv_log = lambda x: 10**x

# The classes in this module have only static methods, i.e.,
# classes are merely name spaces for related functions.


[docs]class OneDiscretizationPrm(object): """ Tools for fitting an error model with only one discretization parameter: e = C*h^2. """
[docs] def error_model(p, d): """Return e = C*h**a, where p=[C, a] and h=d[0].""" C, a = p h = d[0] return C*h**a
[docs] def loglog_error_model(p, d): """As error_model if log-log data was used in the estimation.""" C, a = p h = d[0] return log(C) + a*log(h)
[docs] def linear_loglog_fit(d, e): """ Linear least squares algorithm. Suitable for problems with only one distinct discretization parameter. *d* is the sequence of discretization parameter values, and *e* is the sequence of corresponding error values. The error model the data is supposed to fit reads ``log(e[i]) = log(C[i]) + a*log(d[i])``. """ A = transpose(array([d, zeros(len(d))+1])) sol = linalg.lstsq(A, e) a, logC = sol[0] C = inv_log(logC) return a, C
[docs] def nonlinear_fit(d, e, p0): """ ======== =========================================================== Name Description ======== =========================================================== d list of values of the (single) discretization parameter in each experiment: d[i] provides the values of the discretization, parameter in experiement no. i. e list of error values; e = (e_1, e_2, ...), e[i] is the error associated with the parameters d[i] p0 starting values for the unknown parameters vector return a, C; a is the exponent, C is the factor in front. ======== =========================================================== """ if len(d) != len(e): raise ValueError('d and e must have the same length') if not isinstance(d[0], (float,int)): raise TypeError('d must be an array of numbers, not %s' % \ str(type(d[0]))) # transform d and e to the data format required by # the Scientific package: data = [] for d_i, e_i in zip(d, e): data.append(((d_i,) , e_i)) # recall (a,) conversion to tuple leastSquaresFit = import_module('Scientific.Functions.LeastSquares', 'leastSquaresFit') sol = leastSquaresFit(OneDiscretizationPrm.error_model, p0, data) C = sol[0][0] a = sol[0][1] return a, C
[docs] def pairwise_rates(d, e): """ Compare convergence rates, where each rate is based on a formula for two successive experiments. """ if len(d) != len(e): raise ValueError('d and e must have the same length') rates = [] for i in range(1, len(d)): try: rates.append( log(e[i-1]/e[i])/log(float(d[i-1])/d[i]) ) except (ZeroDivisionError, OverflowError): rates.append(0) # estimate C from the last data point: try: C = e[-1]/d[-1]**rates[-1] except: C = 0 return rates, C
[docs] def analyze(d, e, initial_guess=None, plot_title='', filename='tmp.ps'): """ Run linear, nonlinear and successive rates models. d: list/array of discretization parameter. e: errors corresponding to d. Plot results for comparison of the three approaches. """ # convert to NumPy arrays: d = asarray(d, float); e = asarray(e, float) # linear least squares fit: a1, C1 = OneDiscretizationPrm.linear_loglog_fit(log(d), log(e)) print "linear LS fit: const=%e rate=%.1f" % (C1, a1) # nonlinear least squares fit (no log-log): a2, C2 = OneDiscretizationPrm.nonlinear_fit(d, e, initial_guess) print "nonlinear LS fit: const=%e rate=%.1f" % (C2, a2) # pairwise estimate of the rate: rates, C3 = OneDiscretizationPrm.pairwise_rates(d, e) a3 = rates[-1] print "pairwise fit: const=%e rate=%.1f" % (C3, a3) print "all rates:", rates if C1 < 0 or C2 < 0 or C3 < 0: raise ValueError('Some fits give negative const value! Cannot plot.') return # else log plot: log_d1 = linspace(log(d[0]), log(d[-1]), 2) log_e1 = log(C1) + a1*log_d1 log_e2 = log(C2) + a2*log_d1 log_e3 = log(C3) + a3*log_d1 plot(log(d), log(e), 'yo', log_d1, log_e1, 'r-', log_d1, log_e2, 'b-', log_d1, log_e3, 'g-', legend=('data', 'linear LS log-log fit: %.1f*h^%.1f' % (log(C1), a1), 'nonlinear LS fit: %.1f*h^%.1f' % (log(C2), a2), 'successive rates, last two experiments: %.1f*h^%.1f' % (log(C3), a3)), #axis=[log_d1[-1], log_d1[0], 1.3*log(e[-1]), 1.5*log(e[0])], hardcopy=filename)
analyze = staticmethod(analyze) error_model = staticmethod(error_model) loglog_error_model = staticmethod(loglog_error_model) linear_loglog_fit = staticmethod(linear_loglog_fit) nonlinear_fit = staticmethod(nonlinear_fit) pairwise_rates = staticmethod(pairwise_rates) # convenience function:
def convergence_rate(discretization_prm, error): """ Given two lists/arrays with discretization parameters and corresponding errors in a numerical method (element no. i in the two lists must correspond to each other), this function assumes an error formula of the form E=C*d^r, where E is the error and d is the discretization parameter. The function returns C and r. Method used: OneDiscretizationPrm.pairwise_rates is called and the final r value is used for return. A check that the rates are converging (the last three) is done. """ rates, C = OneDiscretizationPrm.pairwise_rates(discretization_prm, error) # check that there is no divergence at the end of # the series of experiments differences = [rates[i] - rates[i-1] for i in range(len(rates)-1)] # the differences between the rates should decrease, at least # toward the end try: if differences[-1] > differences[-2]: raise ValueError('The pairwise convergence rates do not '\ 'decrease toward the end:\n%s' % \ str(rates)) except IndexError: pass # not enough data to check the differences list return C, rates[-1]
[docs]class ManyDiscretizationPrm(object): """ General tool for fitting an error model containing an arbitrary number of discretization parameters. The error is a weighted sum of each discretization parameter raised to a real expoenent. The weights and exponents are the unknown parameters to be fitted by a nonlinear least squares procedure. """
[docs] def error_model(p, d): """ Evaluate the theoretical error model (sum of C*h^r terms): sum_i p[2*i]*d[i]**p[2*i+1] ====== ======================================================= Name Description ====== ======================================================= p sequence ofvalues of parameters (estimated) d sequence of values of (known) discretization parameters return error evaluated ====== ======================================================= Note that ``len(p)`` must be ``2*len(d)`` in this model since there are two parameters (constant and exponent) for each discretization parameter. """ if len(p) != 2*len(d): raise ValueError('len(p)=%d != 2*len(d)=%d' % (len(p),2*len(d))) sum = 0 for i in range(len(d)): sum += p[2*i] * d[i]**p[2*i+1] return sum
[docs] def nonlinear_fit(d, e, initial_guess): """ @param d: list of values of the set of discretization parameters in each experiment: d = ((d_1,d_2,d_3),(d_1,d_2,d_3,),...); d[i] provides the values of the discretization parameters in experiement no. i. @param e: list of error values; e = (e_1, e_2, ...): e[i] is the error associated with the parameters d[i]. @param initial_guess: the starting value for the unknown parameters vector. @return: list of fitted paramters. """ if len(d) != len(e): raise ValueError('len(d) != len(e)') # transform d and e to the data format required by # the Scientific package: data = [] for d_i, e_i in zip(d, e): if isinstance(d_i, (float, int)): data.append(((d_i,), e_i)) else: # d_i is tuple, list, array, NumArray, ... data.append((d_i, e_i)) leastSquaresFit = import_module('Scientific.Functions.LeastSquares', 'leastSquaresFit') sol = leastSquaresFit(ManyDiscretizationPrm.error_model, initial_guess, data) # return list of fitted parameters (p in error_model) # (sol[1] is a measure of the quality of the fit) return sol[0]
error_model = staticmethod(error_model) nonlinear_fit = staticmethod(nonlinear_fit)
def _test1(): """Single discretization parameter test.""" import random random.seed(1234) n = 7 h = 1 e = []; d = [] for i in range(7): h /= 2.0 error = OneDiscretizationPrm.error_model((4,2), (h,)) error += random.gauss(0, 0.1*error) # perturb data d.append(h) e.append(error) OneDiscretizationPrm.analyze(d, e, initial_guess=(3,2)) def analyze_filedata(): # read filename and initial guess of C and r in error formula E=C*h^r f = open(sys.argv[1], 'r') C = float(sys.argv[2]) r = float(sys.argv[3]) try: plot_title = sys.argv[4] except: plot_title = '' from scitools.filetable import read data = read(f) print data OneDiscretizationPrm.analyze(data[:,0], data[:,1], initial_guess=(C,r), plot_title=plot_title) # extensions: # example with dx, dy and dt # same example, but with factors to get a common rate # dx, dt tables and experiments with whole table, one # column and one row, and the diagonal if __name__ == '__main__': if len(sys.argv) == 1: print 'Usage: %s filename C-guess r-guess [plot-title]' % sys.argv[0] elif sys.argv[1] == 'example': _test1() else: analyze_filedata()