C2Functions.py

Go to the documentation of this file.
00001 """A group of classes which make it easy to manipulate smooth functions, including cubic splines. 
00002 
00003 C2Functions know how to keep track of the first and second derivatives of functions, and to use this information in, for example, find_root()
00004 to allow much more efficient solutions to problems for which the general solution may be expensive.
00005 
00006 The two primary classes are 
00007     C2Function, which represents an unevaluted function and its derivatives, and 
00008     InterpolatingFunction,  which represent a cubic spline of provided data.
00009 
00010 C2Functions can be combined with unary operators (nested functions) or binary operators (+-*/ etc.)
00011 
00012 Developed by Marcus H. Mendenhall, Vanderbilt University Keck Free Electron Laser Center, Nashville, TN USA
00013 email: mendenhall@users.sourceforge.net
00014 Work supported by the US DoD  MFEL program under grant FA9550-04-1-0045
00015 version $Id: C2Functions.py,v 1.66 2007/11/21 16:18:00 mendenhall Exp $
00016 """
00017 _rcsid="$Id: C2Functions.py,v 1.66 2007/11/21 16:18:00 mendenhall Exp $"
00018 
00019 ##\file
00020 ## \brief Provides the analysis.C2Functions package.
00021 ##\package analysis.C2Functions
00022 # \brief A group of classes which make it easy to manipulate smooth functions, including cubic splines. 
00023 #\version $Id: C2Functions.py,v 1.66 2007/11/21 16:18:00 mendenhall Exp $
00024 #
00025 #C2Functions know how to keep track of the first and second derivatives of functions, and to use this information in, for example, C2Function.find_root() and 
00026 #C2Function.partial_integrals()
00027 #to allow much more efficient solutions to problems for which the general solution may be expensive.
00028 #
00029 #The two primary classes are 
00030 #   C2Function which represents an unevaluated function and its derivatives, and 
00031 #   InterpolatingFunction which represent a cubic spline of provided data.
00032 #
00033 #C2Functions can be combined with unary operators (nested/composed functions) or binary operators (+-*/ etc.)
00034 #
00035 #Note that if SciPy is available, the C2Functions package will adopt useful bits from it to speed things up, but is otherwise not dependent on it.
00036 #
00037 #Developed by Marcus H. Mendenhall, Vanderbilt University Keck Free Electron Laser Center, Nashville, TN USA
00038 #
00039 #email: mendenhall@users.sourceforge.net
00040 #
00041 #Work supported by the US DoD  MFEL program under grant FA9550-04-1-0045
00042 #
00043 
00044 import math
00045 import operator
00046 import types
00047 
00048 try:
00049     #prefer numpy over Numeric since it knows how to handle Numeric arrays, too (maybe, but not reliably!)
00050     import numpy as _numeric 
00051     _numeric_float=_numeric.float64
00052 
00053     def native(y, yp, ypp):
00054         "make sure returned scalar arguments are not numpy scalars"
00055         if not operator.isSequenceType(y):
00056             return float(y), float(yp), float(ypp)
00057         else:
00058             return y, yp, ypp
00059 
00060 except:
00061     import Numeric as _numeric
00062     _numeric_float=_numeric.Float64
00063 
00064     def native(y, yp, ypp):
00065         "make sure returned scalar arguments are not numpy scalars... not needed with Numeric"
00066         return y, yp, ypp
00067 
00068 _myfuncs=_numeric
00069 
00070 ##
00071 ## \brief Our own exception class
00072 class   C2Exception(Exception):
00073     pass
00074 
00075 ##
00076 ## \brief Raised if an abscissa is out of range
00077 class   RangeError(IndexError):
00078     pass
00079 
00080 ##
00081 ## \brief Raised if the base class C2Function is called without a valid value_with_derivatives()
00082 class   C2NakedFunction(C2Exception):
00083     pass
00084 
00085 ##
00086 ## \brief Provides support for the entire C2Function hierarchy
00087 #
00088 class   C2Function(object):
00089     "if f is a C2Function, f(x) returns the value at x, and f.value_with_derivatives returns y(x), y'(x), y''(x)"   
00090     ClassName='C2Function'
00091     name='Unnamed'
00092     ##
00093     ##Construct a new C2Function.  
00094     #\param args The default (no args) constructs with a range of (1e-308, 1e308).  
00095     #Otherwise,  C2Function(\a another_C2Function) copies the range.  \n
00096     # or \n
00097     #C2Function( \a first_C2, \a second_C2) sets the range to the intersection of the ranges
00098     def __init__(self, *args) : 
00099         if not args:
00100             self.xMin, self.xMax=-1e308, 1e308
00101         elif len(args)==1: #constructing from a single C2Function
00102             self.xMin, self.xMax=args[0].xMin, args[0].xMax
00103         else: #it should be two C2Functions, and we take the inner bounds of the two
00104             l,r=args
00105             self.xMin=max(l.xMin, r.xMin)
00106             self.xMax=min(l.xMax, r.xMax)
00107         
00108         self.sampling_grid=None
00109     
00110     ##
00111     ##Return our name   
00112     def __str__(self): 
00113         return '<%s %s, Domain=[%g, %g]>' % ((self.ClassName, self.name,)+ self.GetDomain())
00114     
00115     ##
00116     ## Set the name of this function to be returned by str()    
00117     #\param name the string to set the name to      
00118     def SetName(self, name): 
00119         "set the short name of the function for __str__"
00120         self.name=name; return self
00121 
00122     ##
00123     ## \brief get the value of the function, and its first & second derivative
00124     # \param x the point at which to evaluate the function
00125     # \return (f(x), f'(x), f''(x))
00126     def value_with_derivatives(self, x):
00127                 raise C2NakedFunction
00128         
00129     ##
00130     ## get f(val) if val is numeric, otherwise generate the composed fonction f(val())
00131     #\param arg the independent variable, if numeric, otherise the inner function
00132     def __call__(self, arg): 
00133         "f(x) evaluates f(arg) without derivatives if arg is numeric, otherwise returns the composed function f(arg())"
00134         if isinstance(arg, C2Function): return C2ComposedFunction(self, arg)
00135         else: return self.value_with_derivatives(arg)[0] 
00136     
00137     ##
00138     ## Create a new interpolating function which is the evaluated composition of this with the original
00139     #\param interpolator an InterpolatingFunction object
00140     def apply(self, interpolator):
00141         "creates a new InterpolatingFunction which has this function applied to the original interpolater"
00142         return interpolator.UnaryOperator(self)
00143 
00144     ##
00145     ## solve f(x)==value very efficiently, with explicit knowledge of derivatives of the function
00146     # find_root solves by iterated inverse quadratic extrapolation for a solution to f(x)=y.  It
00147     # includes checks against bad convergence, so it should never be able to fail.  Unlike typical
00148     # secant method or fancier Brent's method finders, this does not depend in any strong wasy on the
00149     # brackets, unless the finder has to resort to successive approximations to close in on a root.
00150     # Often, it is possible to make the brackets equal to the domain of the function, if there is
00151     # any clue as to where the root lies, as given by the parameter \a start.  
00152     # \param lower_bracket the lower bound which is ever permitted in the search.
00153     # \param upper_bracket the upper bound. Note that the function have opposite signs at these two points
00154     # \param start an initial guess for where to start the search
00155     # \param value the target value of the function
00156     # \param trace print debugging info to sys.stderr if True
00157     def find_root(self, lower_bracket, upper_bracket, start, value, trace=False): 
00158         "solve f(x)==value very efficiently, with explicit knowledge of derivatives of the function"
00159         # can use local f(x)=a*x**2 + b*x + c 
00160         # and solve quadratic to find root, then iterate
00161 
00162         ftol=(5e-16*abs(value)+2e-308);
00163         xtol=(5e-16*(abs(upper_bracket)+abs(lower_bracket))+2e-308);
00164 
00165         root=start  #start looking in the middle
00166         cupper, yp, ypp=self.value_with_derivatives(upper_bracket)
00167         cupper -= value
00168         if abs(cupper) < ftol: return upper_bracket
00169         clower, yp, ypp=self.value_with_derivatives(lower_bracket)
00170         clower -= value
00171         if abs(clower) < ftol: return lower_bracket;
00172 
00173         if cupper*clower >0:
00174             # argh, no sign change in here!
00175             raise C2Exception("unbracketed root in find_root at xlower=%g, xupper=%g, bailing" %
00176                     (lower_bracket, upper_bracket))
00177 
00178         delta=upper_bracket-lower_bracket   #first error step
00179         c, b, ypp=self.value_with_derivatives(root) # compute initial values
00180         c -= value
00181         
00182         while abs(delta) > xtol: # can allow quite small steps, since we have exact derivatives!
00183             a=ypp/2 #second derivative is 2*a
00184             disc=b*b-4*a*c
00185             if disc >= 0:
00186                 if b>=0:
00187                     q=-0.5*(b+math.sqrt(disc))
00188                 else:
00189                     q=-0.5*(b-math.sqrt(disc))
00190 
00191                 if q*q > abs(a*c):
00192                     delta=c/q   #since x1=q/a, x2=c/q, x1/x2=q^2/ac, this picks smaller step
00193                 else: delta=q/a
00194                 root+=delta;
00195 
00196             if disc < 0 or root<=lower_bracket or root>=upper_bracket or abs(delta) >= 0.5*(upper_bracket-lower_bracket):   
00197                 #if we jump out of the bracket, or if the step is not smaller than half the bracket, bisect
00198                 root=0.5*(lower_bracket+upper_bracket)  
00199                 delta=upper_bracket-lower_bracket
00200 
00201             c, b, ypp=self.value_with_derivatives(root) #get local curvature and value
00202             c -= value
00203 
00204             if trace: 
00205                 import sys
00206                 print >> sys.stderr, "find_root x, dx, c, b, a", ( (5*"%10.4g ") % 
00207                     (root, delta, c, b, ypp/2) )
00208                 
00209             if abs(c) < ftol or abs(c) < xtol*abs(b):   
00210                 return root #got it close enough
00211 
00212             # now, close in bracket on whichever side this still brackets
00213             if c*clower < 0.0:
00214                 cupper=c
00215                 upper_bracket=root
00216             else:
00217                 clower=c
00218                 lower_bracket=root
00219 
00220         return root
00221 
00222     ##
00223     ## \brief Set the domain of the function to (xmin, xmax).
00224     # Mostly, the domain is informative, and used by other functions to figure out where the function should be applied.
00225     # many functions can be evaluated without error outside their marked domain.
00226     # However, some functions for which it is wrong to do so (InterpolatingFunction, e.g.) will raise an exception
00227     # \param xmin the lower bound of the domain
00228     # \param xmax the upper bound of the domain
00229     def SetDomain(self, xmin, xmax): 
00230         "Set the domain of the function. This is mostly an advisory range, except for InterpolatingFunctions, where it matters"
00231         self.xMin=xmin; self.xMax=xmax; return self
00232 
00233     ##
00234     ## \brief get a tuple of the domain of the function
00235     # \return (xmin, xmax)
00236     def GetDomain(self):
00237         "returns xMin, xMax"
00238         return self.xMin, self.xMax
00239 
00240     ##
00241     ## \brief return the value of self(inner_function(x)) without explicitly creating a composed function
00242     #
00243     # If you really want the composed function self(inner), use C2ComposedFunction
00244     # \param inner the inner function
00245     # \param x the point at which to evaluate the composed function
00246     # \return self(inner(x)) and its two derivatives
00247     def compose(self, inner, x):
00248         "y=f.compose(g, x) returns f(g(x)), f'(g(x)), f''(g(x)) where the derivatives are with respect to x"
00249         y0, yp0, ypp0=inner.value_with_derivatives(x)
00250         y1, yp1, ypp1=self.value_with_derivatives(y0)
00251         return y1, yp1*yp0, ypp0*yp1+yp0*yp0*ypp1
00252 
00253     ##
00254     ## \brief utility to make sure python constants get converted to C2Constant objects in subclasses
00255     # \param arg something which might be a number or another C2Function
00256     # \return a C2Function
00257     def convert_arg(self, arg): 
00258         "convert constants to C2Constants"
00259         import operator
00260         if type(arg) is types.FloatType or type(arg) is types.IntType:
00261             return C2Constant(arg)
00262         else: 
00263             return arg
00264     ##
00265     ## \brief Python operator to return a function \a self +\a right 
00266     # \param right the right-hand term of the sum
00267     # \return the C2Function a+b
00268     def __add__(self, right):
00269         "a+b returns a new C2Function which represents the sum"
00270         return C2Sum(self, right)
00271     ##
00272     ## \brief Python operator to return a function \a self -\a right 
00273     # \param right the right-hand term of the difference
00274     # \return the C2Function a-b
00275     def __sub__(self, right):
00276         "a-b returns a new C2Function which represents the difference"
00277         return C2Diff(self, right)
00278     ##
00279     ## \brief Python operator to return a function \a self *\a right 
00280     # \param right the right-hand term of the product
00281     # \return the C2Function a*b
00282     def __mul__(self, right):
00283         "a*b returns a new C2Function which represents the product"
00284         return C2Product(self, right)
00285     ##
00286     ## \brief Python operator to return a function \a self /\a right 
00287     # \param right the denominator term of the ratio
00288     # \return the C2Function a/b
00289     def __div__(self, right):
00290         "a/b returns a new C2Function which represents the ratio"
00291         return C2Ratio(self, right)
00292     ##
00293     ## \brief Python operator to return a function \a self ^\a right
00294     # \note This includes an optimization for numerical powers.
00295     # \param right the exponent
00296     # \return the C2Function a^b
00297     def __pow__(self, right):
00298         "a**b returns a new C2Function which represents the power law, with an optimization for numerical powers"
00299         return C2Power(self, right)
00300 
00301     ##
00302     ## \brief For points in \a recur_data, compute {\f$ \int_{x_i}^{x_{i+1}} f(x) dx \f$} .
00303     #
00304     # Note: The length of the return is one less than the length of \a recur_data. 
00305     # partial_integrals() uses a method with an error O(dx**10) with full  information from the derivatives.
00306     #
00307     # the integration is adaptive, starting from the grid provided, and returning data in the intervals
00308     # between the grid points. 
00309     # \param recur_data grid defining the subdomains over which the integral is computed, and hints for the adaptive algorithm (on initial call)
00310     # \param absolute_error_tolerance total absolute error allowed on each explicit subdomain
00311     # \param relative_error_tolerance total relative error allowed on each explicit subdomain
00312     # \param derivs how much continuity?  derivs=2 => full method, 1 => less smooth function, 0 => Simpson's rule
00313     # \param allow_recursion allow adaptive behavior if true, otherwise just use subdomains provided
00314     # \param extrapolate carry out simple Richardson-Romberg extrapolation if true
00315     # \return  vector in which results from subdomains are returned.
00316     def partial_integrals(self, recur_data, **args):
00317         """def partial_integrals(self, xgrid, relative_error_tolerance=1e-12, derivs=2, 
00318             absolute_error_tolerance=1e-12, depth=0, debug=0, extrapolate=1, allow_recursion=True)
00319             Return the integrals of a function between the sampling points xgrid.  
00320             The sum is the definite integral.
00321             The choices for derivs are 0, 1 or 2, anything else is an error.
00322             The derivs parameter is used as follows: 
00323                 derivs=0 uses Simpsons rule (no derivative information).   
00324                 derivs=1 uses a 6th order technique based the first derivatives, but no second derivatives
00325                 derivs=2 uses a 9th (really 10th, since the 9th order error vanishes by symmetry) 
00326                     order tehcnique based the first and second derivatives.
00327                 Be very aware that the 9th order method will only really benefit with very smooth functions, 
00328                     but then it is magic!
00329         """
00330     
00331         if type(recur_data[1]) is not tuple:    #this is an initialization call, set everything up
00332             funcgrid=[ ( (x, )+ self.value_with_derivatives(x) ) for x in recur_data ] 
00333             self.total_func_evals=len(recur_data)
00334             
00335             derivs= args.get('derivs', 2)
00336             if derivs==0:
00337                 eps_scale, extrap_coef= 0.1, 16.0
00338             elif derivs==1:
00339                 eps_scale, extrap_coef = 0.1, 64.0
00340             elif derivs==2:
00341                 eps_scale, extrap_coef = 0.01, 1024.0
00342             
00343             else:
00344                 raise C2Exception("derivs passed to partial_integrals, must be 0, 1 or 2, got %d" % derivs)
00345 
00346             allow_rec= args.get('allow_recursion', True)
00347             extrapolate = args.get('extrapolate', True) and allow_rec #can only extrapolate after recursion
00348             
00349             recur_data=[0, funcgrid, ( ),
00350                     args.get('absolute_error_tolerance', 1e-12),
00351                     args.get('relative_error_tolerance', 1e-12),
00352                     args.get('debug', 0),
00353                     extrapolate, derivs, eps_scale, extrap_coef, allow_rec
00354                 ]           
00355         
00356         (depth, funcgrid, old_integrals, absolute_error_tolerance, relative_error_tolerance, 
00357                 debug, extrapolate, derivs, eps_scale, extrap_coef, allow_rec)=recur_data
00358         
00359         retvals=[0.0]*(len(funcgrid)-1)
00360         
00361         for i in range(len(funcgrid)-1):
00362             x0, y0, yp0, ypp0=funcgrid[i]
00363             x2, y2, yp2, ypp2=funcgrid[i+1]
00364             x1=0.5*(x0+x2)
00365             y1, yp1, ypp1=self.value_with_derivatives(x1)
00366             self.total_func_evals+=1
00367             dx=x2-x0
00368             dx2=0.5*dx
00369 
00370             #check for underflow on step size, which prevents us from achieving specified accuracy. 
00371             if abs(dx) < abs(x1)*relative_error_tolerance:
00372                 raise C2Exception("Step size underflow in partial_integrals, depth = %d, x = %.4f" %
00373                         (depth, x1))
00374             
00375             #if we are below the top level, the previous term is already computed.  
00376             # Otherwise, compute it to one lower order
00377             if  old_integrals:
00378                 total=old_integrals[i]
00379             elif derivs==2:
00380                 total=( (14*y0 + 32*y1 + 14*y2) +  dx * (yp0 - yp2) ) * dx /60.
00381 
00382             elif derivs==1:
00383                 total=(y0+4.0*y1+y2)*dx/6.0
00384             else:
00385                 total=0.5*(y0+y2)*dx
00386 
00387             if derivs==2:
00388                 #use ninth-order estimates for each side. (Thanks, Mathematica!)
00389                 left=   ( ( (169*ypp0 + 1024*ypp1 - 41*ypp2)*dx2 + (2727*yp0 - 5040*yp1 + 423*yp2) )*dx2 + 
00390                         (17007*y0 + 24576*y1 - 1263*y2) )* (dx2/40320.0)
00391                 right=  ( ( (169*ypp2 + 1024*ypp1 - 41*ypp0)*dx2 - (2727*yp2 - 5040*yp1 + 423*yp0) )*dx2 + 
00392                         (17007*y2 + 24576*y1 - 1263*y0) )* (dx2/40320.0)
00393             elif derivs==1:
00394                 left=   ( (202*y0 + 256*y1 + 22*y2) + dx*(13*yp0 - 40*yp1 - 3*yp2) ) * dx /960.
00395                 right=  ( (202*y2 + 256*y1 + 22*y0) - dx*(13*yp2 - 40*yp1 - 3*yp0) ) * dx /960.
00396             else:
00397                 left=   (5*y0 + 8*y1 - y2)*dx/24.
00398                 right=  (5*y2 + 8*y1 - y0)*dx/24.
00399 
00400             eps= abs(total-(left+right))*eps_scale 
00401             #the real error should be 2**order times smaller on this iteration, be conservative
00402             if extrapolate:
00403                 eps=eps*eps_scale 
00404                 #gain another factor of 2**order if extrapolating (typical), but be conservative
00405                 
00406             if  (not allow_rec) or eps < absolute_error_tolerance or eps < abs(total)*relative_error_tolerance:
00407                 if not extrapolate:
00408                     retvals[i]=left+right
00409                 else:
00410                     retvals[i]=(extrap_coef*(left+right) - total) / (extrap_coef-1) 
00411                     #since h fell by 2, h**6=64, and we are extrapolating in h**6
00412                 if debug==1: 
00413                     print "accepted results at depth ", depth,  "x, dx = %7.3f, %10.6f" % (x1, dx), 
00414                     print "scaled error = ", eps/ (abs(total)*relative_error_tolerance)
00415             else:
00416                 if debug==1: 
00417                     print "rejected results at depth ", depth,  "x, dx = %7.3f, %10.6f" % (x1, dx), 
00418                     print "scaled error = ", eps/ (abs(total)*relative_error_tolerance)
00419                 recur_data[0:4]=(depth+1, (funcgrid[i], (x1, y1, yp1, ypp1), funcgrid[i+1]), 
00420                         (left, right), absolute_error_tolerance/2 )
00421                 l, r =self.partial_integrals(recur_data)
00422                 retvals[i]=l+r
00423         if debug ==2:
00424             print "\nintegrating at depth ", depth
00425             print xgrid
00426             print funcgrid
00427             import operator
00428             print retvals
00429             if old_integrals: 
00430                 print map(operator.sub, old_integrals, retvals)
00431             print "\nreturning from depth ", depth
00432         return retvals
00433 
00434     ##
00435     ## \brief give a C2Function hints about interesting points to sample for integration, etc.
00436     # \param grid an indexable list of points which is a starting point for any recursive sampling
00437     # \note typically, this grid can be quite coarse.  For periodic functions, it may be just more frequent
00438     # than 1 point per period.  DO NOT make it have the same period as the function, since this will certainly
00439     # break any recursive measures of convergence.  It will see the function the same at every point, and think
00440     # it is dealing with a constant.
00441     #
00442     def SetSamplingGrid(self, grid):
00443         "provide a set of 'interesting' points for starting to sample this function"
00444         self.sampling_grid=_numeric.array(grid)
00445 
00446     ##
00447     ## \brief Extract the list of 'interesting' points from the sampling grid which lie in the requested domain,
00448     # including q point at each endpoint.
00449     # \param xmin the lower bound for the grid.
00450     # \param xmax the upper bound for the grid.
00451     # \return a list of points at which to start looking at the function.
00452     def GetSamplingGrid(self, xmin, xmax):
00453         "get a set of points, including xmin and xmax, which are reasonable points to evaluate the function between them"
00454         if self.sampling_grid is None or xmin > self.sampling_grid[-1] or xmax < self.sampling_grid[0]:
00455             pass
00456             return (xmin, xmax) #dont really have any information on the funciton in this interval.
00457         else:
00458 
00459             sg=self.sampling_grid
00460             
00461             firstindex, lastindex=_numeric.searchsorted(sg, (xmin, xmax))
00462     
00463             grid=[xmin]+list(sg[firstindex:lastindex])+[xmax] #insert points  from source grid to destination into the middle of our grid
00464             
00465             if len(grid) > 2 : #attempt to clear out points put too close together at the beginning, if we have at least three points
00466                 x0, x1, x2 = grid[:3]
00467                 ftol=10.0*(1e-14*(abs(x0)+abs(x1))+1e-300)
00468                 if (x1-x0) < ftol or (x1-x0) < 0.1*(x2-x0): del grid[1] #remove collision
00469             
00470             if len(grid) > 2 : #attempt to clear out points put too close together at the end, if we have at least three points
00471                 x0, x1, x2 = grid[-3:]
00472                 ftol=10.0*(1e-14*(abs(x0)+abs(x2))+1e-300)
00473                 if (x2-x1) < ftol or (x2-x1) < 0.1*(x2-x0): del grid[-2] #remove collision
00474         
00475             return grid
00476 
00477     ##
00478     ## \brief return the definite integral from xmin to xmax of this function using our sampling_grid for hints.
00479     # 
00480     # \param xmin the lower bound
00481     # \param xmax the upper bound
00482     # \param args see partial_integrals() for meaning of these arguments 
00483     # \return the integral
00484     def integral(self, xmin, xmax, **args):
00485         "carry out integration using our sampling grid"
00486         grid=self.GetSamplingGrid(xmin, xmax)
00487         return sum(self.partial_integrals(grid, **args))
00488 
00489     ##
00490     ## \brief return a new C2Function which has integral \a norm on (\a xmin, \a xmax )
00491     # \param xmin the lower bound for the normalization
00492     # \param xmax the upper bound for the normalization
00493     # \param norm the desired integral
00494     # \return the new function
00495     def NormalizedFunction(self, xmin, xmax, norm=1):
00496         "return a function whose integral on [xmin, xmax] is norm"
00497         intg=self.integral(xmin, xmax)
00498         return C2ScaledFunction(self, norm/intg)
00499     ##
00500     ## \brief return a new C2Function which has square-integral \a norm on (\a xmin, \a xmax ) with weight function \a weight
00501     # \param xmin the lower bound for the normalization
00502     # \param xmax the upper bound for the normalization
00503     # \param weight the weight function.  Unity if omitted.
00504     # \param norm the desired integral
00505     # \return the new function
00506     def SquareNormalizedFunction(self, xmin, xmax, weight=None, norm=1):
00507         "return a function whose square integral on [xmin, xmax] with optional weight function is norm"
00508         mesquared=C2Quadratic(a=1.0)(self)
00509         if weight is not None:
00510             mesquared=mesquared*weight
00511             
00512         grid=self.GetSamplingGrid(xmin, xmax)
00513         intg=sum(mesquared.partial_integrals(grid))
00514         return C2ScaledFunction(self, math.sqrt(norm/intg))
00515     
00516 
00517 ##
00518 # \brief Create a function which is a simple scalar multiple of the parent
00519 class C2ScaledFunction(C2Function):
00520     "a lightweight class to create a function scaled vertically by a scale factor"
00521     ClassName='Scaled'
00522     ##
00523     # \brief construct the scaled function
00524     # \param fn the function to scale
00525     # \param yscale the mutiplicative factor to apply
00526     def __init__(self, fn, yscale):
00527         C2Function.__init__(self)
00528         self.fn=fn
00529         self.yscale=yscale
00530         self.name=fn.name+'* %g' % yscale
00531 
00532     ##
00533     def value_with_derivatives(self, x): 
00534         y, yp, ypp = self.fn.value_with_derivatives(x)
00535         ys=self.yscale
00536         return  native(y*ys, yp*ys, ypp*ys)
00537 
00538 ##
00539 # \brief Create a function which is a constant
00540 class C2Constant(C2Function):
00541     "a constant and its derivatives"
00542     ClassName='Constant'
00543     ##
00544     # \brief construct the constant
00545     # \param val the constant
00546     def __init__(self, val):
00547         C2Function.__init__(self)
00548         self.val=val
00549         self.name='%g' % val
00550 
00551     ##
00552     # \brief reset the value of the constant
00553     # \param val the new constant
00554     def reset(self, val):
00555         "reset the value to a new value"
00556         self.val=val
00557 
00558         ##
00559     def value_with_derivatives(self, x): 
00560         return self.val, 0., 0.
00561 
00562 ##
00563 # \brief Create a function which is the sine.  Use the singleton C2Functions.C2sin to access this.
00564 class _fC2sin(C2Function):
00565     "sin(x)"
00566     name='sin'
00567     def value_with_derivatives(self, x):
00568         q=_myfuncs.sin(x)
00569         return  native(q, _myfuncs.cos(x), -q)
00570 
00571 ##
00572 # \brief a pre-constructed singleton
00573 C2sin=_fC2sin() #make singleton
00574 
00575 ##
00576 # \brief Create a function which is the cosine. Use the singleton C2Functions.C2cos to access this.
00577 class _fC2cos(C2Function):
00578     "cos(x)"
00579     name='cos'
00580     def value_with_derivatives(self, x):
00581         q=_myfuncs.cos(x)
00582         return  native(q, -_myfuncs.sin(x), -q)
00583 ##
00584 # \brief a pre-constructed singleton
00585 C2cos=_fC2cos() #make singleton
00586 
00587 ##
00588 # \brief Create a function which is the natural log. Use the singleton C2Functions.C2log to access this.
00589 class _fC2log(C2Function):
00590     "log(x)"
00591     name='log'
00592     def value_with_derivatives(self, x):
00593         return  native(_myfuncs.log(x), 1/x, -1/(x*x))
00594 ##
00595 # \brief a pre-constructed singleton
00596 C2log=_fC2log() #make singleton
00597 
00598 ##
00599 # \brief Create a function which is the e^x. Use the singleton C2Functions.C2exp to access this.
00600 class _fC2exp(C2Function):
00601     "exp(x)"
00602     name='exp'
00603     def value_with_derivatives(self, x):
00604         q=_myfuncs.exp(x)
00605         return  native(q, q, q)
00606 ##
00607 # \brief a pre-constructed singleton
00608 C2exp=_fC2exp() #make singleton
00609 
00610 ##
00611 # \brief Create a function which is the square root. Use the singleton C2Functions.C2sqrt to access this.
00612 class _fC2sqrt(C2Function):
00613     "sqrt(x)"
00614     name='sqrt'
00615     def value_with_derivatives(self, x):
00616         q=_myfuncs.sqrt(x)
00617         return  native(q, 0.5/q, -0.25/(q*x))
00618 ##
00619 # \brief a pre-constructed singleton
00620 C2sqrt=_fC2sqrt() #make singleton
00621 
00622 ##
00623 # \brief Create a function which is the \a scale /\a x. Use the singleton C2Functions.C2recip to access this as 1/x.
00624 class C2ScaledRecip(C2Function):
00625     "1/x"
00626     name='1/x'
00627     ##
00628     # \brief construct the function, and set the scale factor.
00629     def __init__(self, scale=1.0):
00630                 self.scale=scale
00631                 
00632         ##
00633     def value_with_derivatives(self, x):
00634         q=1.0/x
00635         return  native(self.scale*q, -self.scale*q*q, 2*self.scale*q*q*q)
00636     
00637 ##
00638 # \brief a pre-constructed singleton for 1/x
00639 C2recip=C2ScaledRecip() #make singleton
00640 
00641 ##
00642 # \brief Create a function which is \a x. Use the singleton C2Functions.C2identity to access this.
00643 class _fC2identity(C2Function):
00644     name='Identity'
00645     def value_with_derivatives(self, x):
00646         return x, 1.0, 0.0
00647 ##
00648 # \brief a pre-constructed singleton identity
00649 C2identity=_fC2identity() #make singleton
00650 
00651 ##
00652 # \brief Create a function which is (\a x - \a x0)*\a slope + \a y0.
00653 class C2Linear(C2Function):
00654     "slope*(x-x0) + y0"
00655     ##
00656     ## \brief construct the function
00657     # \param x0 the \a x offset at which f(\a x) = \a y0
00658     # \param slope the slope
00659     # \param y0 the value of the function at \a x0
00660     def __init__(self, x0=0.0, slope=1.0, y0=0.0):
00661         C2Function.__init__(self)
00662         self.x0=x0
00663         self.slope=slope
00664         self.y0=y0
00665         self.name="(%g * (x - %g) + %g)" % (slope, x0, y0)
00666     
00667     ##
00668     ## \brief reset the parameters of the function
00669     # \param x0 the \a x offset at which f(\a x) = \a y0, None if not reset
00670     # \param slope the slope, None if not reset
00671     # \param y0 the value of the function at \a x0, None if not reset
00672     def reset(self, x0=None, slope=None, y0=None):
00673         "reset the x0, slope or intercept to a new value"
00674         if slope is not None:
00675             self.slope=slope
00676         if y0 is not None:
00677             self.y0=y0
00678         if x0 is not None:
00679             self.x0=x0
00680             
00681     def value_with_derivatives(self, x):
00682         return native((x-self.x0)*self.slope+self.y0, self.slope, 0.0)
00683 
00684 ##
00685 # \brief Create a function which is \a a *(\a x - \a x0)**2 + \a b *(\a x - \a x0) + \a c 
00686 class C2Quadratic(C2Function):
00687     "a*(x-x0)**2 + b*(x-x0) + c"
00688     ##
00689     ## \brief construct the function
00690     # \param x0 the \a x offset at which f(\a x) = \a c
00691     # \param a the coefficient of (\a x - \a x0)**2
00692     # \param b the coefficient of (\a x - \a x0)
00693     # \param c the value of the function at \a x0
00694     def __init__(self, x0=0.0, a=1.0, b=0.0, c=0.0):
00695         C2Function.__init__(self)
00696         self.x0, self.a, self.b, self.c = x0, a, b, c
00697         self.name="(%g*(x-x0)^2 + %g*(x-x0) + %g, x0=%g)" % (a, b, c, x0)
00698         
00699     ##
00700     ## \brief reset the parameters of the function
00701     # \param x0 the \a x offset at which f(\a x) = \a c, None if not reset
00702     # \param a the coefficient of (\a x - \a x0)**2, None if not reset
00703     # \param b the coefficient of (\a x - \a x0), None if not reset
00704     # \param c the value of the function at \a x0, None if not reset
00705     def reset(self, x0=None, a=None, b=None, c=None):
00706         "reset the parameters to new values"
00707         if x0 is not None:
00708             self.x0=x0
00709         if a is not None:
00710             self.a=a
00711         if b is not None:
00712             self.b=b
00713         if b is not None:
00714             self.c=c
00715 
00716     def value_with_derivatives(self, x):
00717         dx=x-self.x0
00718         return native(self.a*dx*dx+self.b*dx+self.c, 2*self.a*dx+self.b, 2*self.a)
00719 
00720 ##
00721 # \brief Create a function which is \a a\a x**\a b 
00722 class C2PowerLaw(C2Function):
00723     "a*x**b where a and b are constant"
00724     ##
00725     ## \brief construct the function \a a\a x**\a b 
00726     # \param a the scale factor
00727     # \param b the exponent
00728     def __init__(self, a=1.0, b=2.0):
00729         C2Function.__init__(self)
00730         self.a, self.b = a, b
00731         self.b2=b-2
00732         self.bb1=b*(b-1)
00733         self.name='%g*x^%g' % (a,b)
00734         
00735     ##
00736     ## \brief reset the parameters of the function \a a\a x**\a b 
00737     # \param a the scale factor, None if not reset
00738     # \param b the the exponent, None if not reset
00739     def reset(self, a=None, b=None):
00740         "reset the parameters to new values"
00741         if a is not None:
00742             self.a=a
00743         if b is not None:
00744             self.b=b
00745 
00746     def value_with_derivatives(self, x):
00747         xp=self.a*x**self.b2
00748         return  native(xp*x*x, self.b*xp*x, self.bb1*xp)
00749 
00750 ##
00751 # \brief Create a function which is c0 + c1 (x-x0) + c2 (x-x0)^2 + ...
00752 class C2Polynomial(C2Function):
00753     "poly(x)"   
00754     ##
00755     ## \brief construct the function c0 + c1 (x-x0) + c2 (x-x0)^2 + ...
00756     # \param coefs the coefficients c_n (first coefficient in list is the conatant term)
00757     # \param x0 the center for expansion of the polyomial
00758     def __init__(self, coefs, x0=0.0):
00759         "initialize the polynomial with coefficients specified, expanding around xcenter.  Constant coefficient is coefs[0]"
00760         
00761         self.coefs=tuple(coefs)
00762         self.rcoefs=tuple(enumerate(self.coefs))[::-1] #record power index with coef
00763         self.xcenter=x0
00764     
00765     def value_with_derivatives(self, x):
00766         x-=self.xcenter
00767         xsum=0.0
00768         for n,c in self.rcoefs: xsum=xsum*x+c
00769         xpsum=0.0
00770         for n,c in self.rcoefs[:-1]: xpsum=xpsum*x+c*n
00771         xp2sum=0.0
00772         for n,c in self.rcoefs[:-2]: xp2sum=xp2sum*x+c*n*(n-1)
00773         
00774         return  native(xsum, xpsum, xp2sum)
00775 
00776         
00777 ##
00778 # \brief create a composed function outer(inner(...)).  The functions can either be unbound class names or class instances
00779 class C2ComposedFunction(C2Function):
00780     "create a composed function outer(inner(...)).  The functions can either be unbound class names or class instances"
00781     ##
00782     ## \brief construct the function outer(inner)
00783     # \param outer the outer function of the composition
00784     # \param inner the inner function of the composition
00785     # \note The functions can either be unbound class names or class instances.
00786     # If they are class names, they will be instantiated with default arguments
00787     def __init__(self, outer, inner):
00788         if type(inner) is types.ClassType: inner=inner() #instatiate unbound class
00789         if type(outer) is types.ClassType: outer=outer() #instatiate unbound class
00790         C2Function.__init__(self, inner) #domain is that of inner function
00791         self.outer=outer
00792         self.inner=inner
00793         self.name=outer.name+'('+inner.name+')'
00794         
00795     def value_with_derivatives(self, x): return self.outer.compose(self.inner, x)
00796 
00797 ##
00798 # \brief abstract class to create a binary function f (operator) g
00799 class C2BinaryFunction(C2Function):
00800     "create a binary function using a helper function which computes the derivatives"
00801     ##
00802     # \brief construct the function, making sure if an argument is passed as a constant that it is converted to a C2Constant
00803     # \param left the left function in the binary
00804     # \param right the right function in the binary
00805     def __init__(self, left,  right):
00806         self.left=self.convert_arg(left)
00807         self.right=self.convert_arg(right)
00808         C2Function.__init__(self, self.left, self.right)
00809         
00810         if isinstance(left, C2BinaryFunction): p1, p2 = '(', ')'
00811         else: p1, p2='', ''
00812         self.name=p1+self.left.name+p2+self.name+self.right.name #put on parentheses to kepp hierachy obvious
00813         
00814 ##
00815 # \brief class to create function f + g
00816 class C2Sum(C2BinaryFunction):
00817     "C2Sum(a,b) returns a new C2Function which evaluates as a+b"
00818     name='+'
00819     def value_with_derivatives(self, x):
00820         y0, yp0, ypp0=self.left.value_with_derivatives(x)
00821         y1, yp1, ypp1=self.right.value_with_derivatives(x)
00822         return y0+y1, yp0+yp1, ypp0+ypp1
00823 
00824 ##
00825 # \brief  class to create function f - g
00826 class C2Diff(C2BinaryFunction):
00827     "C2Diff(a,b) returns a new C2Function which evaluates as a-b"
00828     name='-'
00829     def value_with_derivatives(self, x):
00830         y0, yp0, ypp0=self.left.value_with_derivatives(x)
00831         y1, yp1, ypp1=self.right.value_with_derivatives(x)
00832         return y0-y1, yp0-yp1, ypp0-ypp1
00833 
00834 ##
00835 # \brief class to create function f * g
00836 class C2Product(C2BinaryFunction):
00837     "C2Product(a,b) returns a new C2Function which evaluates as a*b"
00838     name='*'
00839     def value_with_derivatives(self, x):
00840         y0, yp0, ypp0=self.left.value_with_derivatives(x)
00841         y1, yp1, ypp1=self.right.value_with_derivatives(x)
00842         return y0*y1, y1*yp0+y0*yp1, ypp0*y1+2.0*yp0*yp1+ypp1*y0
00843 
00844 ##
00845 # \brief class to create function f / g
00846 class C2Ratio(C2BinaryFunction):
00847     "C2Ratio(a,b) returns a new C2Function which evaluates as a/b"
00848     name='/'
00849     def value_with_derivatives(self, x):
00850         y0, yp0, ypp0=self.left.value_with_derivatives(x)
00851         y1, yp1, ypp1=self.right.value_with_derivatives(x)
00852         return y0/y1, (yp0*y1-y0*yp1)/(y1*y1), (y1*y1*ypp0+y0*(2*yp1*yp1-y1*ypp1)-2*y1*yp0*yp1)/(y1*y1*y1)
00853 ##
00854 # \brief class to create function f ** g with optimization if g is a constant
00855 class C2Power(C2BinaryFunction):
00856     "C2power(a,b) returns a new C2Function which evaluates as a^b where neither is necessarily constant.  Checks if b is constant, and optimizes if so"
00857     name='^'
00858     def value_with_derivatives(self, x):
00859         y0, yp0, ypp0=self.left.value_with_derivatives(x)
00860         y1, yp1, ypp1=self.right.value_with_derivatives(x)
00861         if isinstance(self.right, C2Constant): #all derivatives of right side are zero, save some time
00862             ab2=_myfuncs.power(y0, (y1-2.0)) #this if a^(b-2), which appears often
00863             ab1=ab2*y0 #this is a^(b-1)
00864             ab=ab1*y0 #this is a^b
00865             ab1 *= yp0
00866             ab1 *= y1 #ab1 is now the derivative
00867             ab2 *= y1*(y1-1)*yp0*yp0+y0*y1*ypp0 #ab2 is now the second derivative
00868             return  native(ab,  ab1, ab2)
00869         else:
00870             loga=_myfuncs.log(y0)
00871             ab2=_myfuncs.exp(loga*(y1-2.0)) #this if a^(b-2), which appears often
00872             ab1=ab2*y0 #this is a^(b-1)
00873             ab=ab1*y0 #this is a^b
00874             yp=ab1*(yp0*y1+y0*yp1*loga)
00875             ypp=ab2*(y1*(y1-1)*yp0*yp0+2*y0*yp0*yp1*(1.0+y1*loga)+y0*(y1*ypp0+y0*(ypp1+yp1*yp1*loga)*loga))
00876             return  native(ab, yp, ypp)
00877 
00878 ##
00879 
00880 #the following spline utilities are directly from pythonlabtools.analysis.spline, version "spline.py,v 1.23 2005/07/13 14:24:58 mendenhall Release-20050805"
00881 #and are copied here to reduce paxckage interdependency
00882 try:
00883     from scipy import linalg as _linalg
00884     import numpy as _numpy
00885     _has_linalg=True
00886 except:
00887     _has_linalg=False
00888 ## \brief solve for the spline coefficients y''
00889 ##
00890 ## If we have scipy support, use a fast tridiagonal algorithm and numpy arrays internally to compute spline coefficients.
00891 ## Without scipy support, use a raw python code to compute coefficients.
00892 ## \param x array of abscissas
00893 ## \param y array of ordinates
00894 ## \param yp1 first derivative at lower edge, None if natural spline (y''(0) -> 0)
00895 ## \param ypn first derivative at upper edge, None if natural spline (y''(n) -> 0)
00896 ## \return y'' array (spline coefficients)
00897 def _spline(x, y, yp1=None, ypn=None):
00898     if _has_linalg:
00899         "y2 = spline(x_vals,y_vals, yp1=None, ypn=None) returns the y2 table for the spline as needed by splint()"
00900     
00901         n=len(x)
00902         u=_numpy.zeros(n,_numpy.float64)
00903         
00904         x=_numpy.asarray(x, _numpy.float64)
00905         y=_numpy.asarray(y, _numpy.float64)
00906         
00907         dx=x[1:]-x[:-1]
00908         dx2=(x[2:]-x[:-2])
00909         dy=(y[1:]-y[:-1])
00910         dydx=dy/dx
00911         
00912         # u[i]=(y[i+1]-y[i])/float(x[i+1]-x[i]) - (y[i]-y[i-1])/float(x[i]-x[i-1])
00913         u[1:-1]=dydx[1:]
00914         u[1:-1]-=dydx[:-1] #this is an incomplete rendering of u... the rest requires recursion in the loop
00915 
00916         trimat=_numpy.zeros((3, n), _numpy.float64)
00917 
00918         trimat[0, 1:]=dx
00919         trimat[1, 1:-1]=dx2
00920         trimat[2, :-1]=dx
00921         trimat[0] *= (1.0/6.0)
00922         trimat[1] *= (1.0/3.0)
00923         trimat[2] *= (1.0/6.0)
00924 
00925         if yp1 is None:
00926             u[0]=0.0
00927             trimat[1,0]=1
00928             trimat[0,1]=0
00929         else:
00930             u[0]=dydx[0]-yp1
00931             trimat[1,0] = dx[0]/(3.0)
00932             
00933         if ypn is None:
00934             u[-1]=0.0
00935             trimat[1,-1]=1
00936             trimat[2,-2]=0
00937         else:
00938             u[-1]=ypn-dydx[-1]
00939             trimat[1,-1] = dx[-1]/(3.0)
00940         
00941         y2=_linalg.solve_banded((1,1), trimat, u, debug=0)
00942         return _numeric.asarray(y2, _numeric_float)
00943     else:
00944         n=len(x)
00945         u=_numeric.zeros(n,_numeric_float)
00946         y2=_numeric.zeros(n,_numeric_float)
00947         
00948         x=_numeric.asarray(x, _numeric_float)
00949         y=_numeric.asarray(y, _numeric_float)
00950         
00951         dx=x[1:]-x[:-1]
00952         dxi=1.0/dx
00953         dx2i=1.0/(x[2:]-x[:-2])
00954         dy=(y[1:]-y[:-1])
00955         siga=dx[:-1]*dx2i
00956         dydx=dy*dxi
00957         
00958         # u[i]=(y[i+1]-y[i])/float(x[i+1]-x[i]) - (y[i]-y[i-1])/float(x[i]-x[i-1])
00959         u[1:-1]=dydx[1:]
00960         u[1:-1]-=dydx[:-1] #this is an incomplete rendering of u... the rest requires recursion in the loop
00961         
00962         if yp1 is None:
00963             y2[0]=u[0]=0.0
00964         else:
00965             y2[0]= -0.5
00966             u[0]=(3.0*dxi[0])*(dy[0]*dxi[0] -yp1)
00967     
00968         for i in range(1,n-1):
00969             sig=siga[i-1]
00970             p=sig*y2[i-1]+2.0
00971             y2[i]=(sig-1.0)/p
00972             u[i]=(6.0*u[i]*dx2i[i-1] - sig*u[i-1])/p
00973     
00974         if ypn is None:
00975             qn=un=0.0
00976         else:
00977             qn= 0.5
00978             un=(3.0*dxi[-1])*(ypn- dy[-1]*dxi[-1] )
00979             
00980         y2[-1]=(un-qn*u[-2])/(qn*y2[-2]+1.0)
00981         for k in range(n-2,-1,-1):
00982             y2[k]=y2[k]*y2[k+1]+u[k]
00983     
00984         return y2
00985 ##
00986 ## \brief compute the correct coefficients and insert them to allow spline extrapolation
00987 # \param x the array of abscissas for an already existing spline
00988 # \param y the array of ordinates for an already existing spline
00989 # \param x the array of spline coefficients for an already existing spline
00990 # \param xmin the lower bound to which extrapolation should be permitted (None if no lower extension)
00991 # \param xmax the upper bound to which extrapolation should be permitted (None if no upper extension)
00992 # \return new arrays x, y, y'' with extesions added
00993 def _spline_extension(x, y, y2, xmin=None, xmax=None):
00994     """x, y, y2 = spline_extension(x_vals,y_vals, y2vals, xmin=None, xmax=None) 
00995     returns the x, y, y2 table for the spline as needed by splint() with adjustments to allow quadratic extrapolation 
00996     outside the range x[0]-x[-1], from xmin (or x[0] if xmin is None) to xmax (or x[-1] if xmax is None),
00997     working from x, y, y2 from an already-created spline"""
00998 
00999     xl=[x]
01000     yl=[y]
01001     y2l=[y2]
01002     
01003     if xmin is not None:
01004         h0=x[1]-x[0]
01005         h1=xmin-x[0]
01006         yextrap=y[0]+((y[1]-y[0])/h0 - h0*(y2[0]+2.0*y2[1])/6.0)*h1+y2[0]*h1*h1/2.0
01007         yl.insert(0, (yextrap,))
01008         xl.insert(0, (xmin,))
01009         y2l.insert(0, (y2[0],))
01010 
01011     if xmax is not None:
01012         h0=x[-1]-x[-2]
01013         h1=xmax-x[-1]
01014         yextrap=y[-1]+((y[-1]-y[-2])/h0 + h0*(2.0*y2[-2]+y2[-1])/6.0)*h1+y2[-1]*h1*h1/2.0
01015         yl.append((yextrap,))
01016         xl.append((xmax,))
01017         y2l.append((y2[-1],))
01018 
01019     return _numeric.concatenate(xl), _numeric.concatenate(yl), _numeric.concatenate(y2l)
01020 
01021 ##
01022 import operator
01023 
01024 ## \brief compute the interpolated value for a set of spline coefficients and either a scalar \a x or an array of \a x values
01025 # \param xa the abscissa table for the spline
01026 # \param ya the ordinate table for the spline
01027 # \param y2a the spline coefficients (second derivatives) for the spline
01028 # \param x the value or values at which to do the interpolation
01029 # \param derivs if True, also compute and return derivatives.  If False, return value or values only
01030 # \return (\a y, \a y', \a y'') if \a derivs is True, \a y if \a derivs is False.  Each item will be an array if \a x was an array.
01031 def _splint(xa, ya, y2a, x, derivs=False):
01032     """returns the interpolated from from the spline
01033     x can either be a scalar or a listable item, in which case a Numeric Float array will be
01034     returned and the multiple interpolations will be done somewhat more efficiently.
01035     If derivs is not False, return y, y', y'' instead of just y."""
01036     if not operator.isSequenceType(x):
01037         x=float(x) #this will throw an exception if x is an array!
01038         if (x<xa[0] or x>xa[-1]):
01039             raise RangeError, "%f not in range (%f, %f) in splint()" % (x, xa[0], xa[-1])
01040              
01041         khi=max(_numeric.searchsorted(xa,x),1)
01042         klo=khi-1
01043         #convert everything which came out of an array to a float, since numpy arrays return numpy scalars, rather than native scalars
01044         #this speeds things up, and makes sure numbers coming back from here are native
01045         h=float(xa[khi]-xa[klo])
01046         a=float(xa[khi]-x)/h; b=1.0-a
01047         ylo=float(ya[klo]); yhi=float(ya[khi]); y2lo=float(y2a[klo]); y2hi=float(y2a[khi]) 
01048     else:
01049         #if we got here, we are processing a list, and should do so more efficiently
01050         if (min(x)<xa[0] or max(x)>xa[-1]):
01051             raise RangeError, "(%f, %f) not in range (%f, %f) in splint()" % (min(x), max(x), xa[0], xa[-1])
01052     
01053         npoints=len(x)
01054         khi=_numeric.clip(_numeric.searchsorted(xa,_numeric.asarray(x)),1,len(xa)) 
01055         
01056         klo=khi-1
01057         xhi=_numeric.take(xa, khi)
01058         xlo=_numeric.take(xa, klo)
01059         yhi=_numeric.take(ya, khi)
01060         ylo=_numeric.take(ya, klo)
01061         y2hi=_numeric.take(y2a, khi)
01062         y2lo=_numeric.take(y2a, klo)
01063         
01064         h=(xhi-xlo).astype(_numeric_float)
01065         a=(xhi-x)/h
01066         b=1.0-a
01067         
01068     y=a*ylo+b*yhi+((a*a*a-a)*y2lo+(b*b*b-b)*y2hi)*(h*h)/6.0
01069     if derivs:
01070         return y, (yhi-ylo)/h+((3*b*b-1)*y2hi-(3*a*a-1)*y2lo)*h/6.0, b*y2hi+a*y2lo
01071     else:
01072         return y
01073 ##
01074 
01075 #these are functions to bypass the need for lambdas in the conversion functions.  This is compatible with pickling. 
01076 def _identity(x): return x
01077 def _one(x): return 1.0
01078 def _zero(x): return 0.0
01079 def _recip(x): return 1.0/x
01080 def _mrecip2(x): return -1.0/(x*x)
01081 def _myexp(x): return _myfuncs.exp(x)
01082 def _mylog(x): return _myfuncs.log(x)
01083 
01084 ##
01085 ##\class InterpolatingFunction
01086 # \brief the parent class for various interpolators. Does untransformed cubic splines by default.
01087 #
01088 #An InterpolatingFunction stores a cubic spline representation of a set of x,y pairs.
01089 #   It can also transform the variable on input and output, so that the underlying spline may live in log-log space, 
01090 #   but such transforms are transparent to the setup and use of the function.  This makes it possible to
01091 #   store splines of, e.g., data which are very close to a power law, as a LogLogInterpolatingFunction, and
01092 #   to then have very accurate interpolation and extrapolation, since the curvature of such a function is small in log-log space.
01093 #   
01094 #   InterpolatingFunction(x, y, lowerSlope, upperSlope, XConversions, YConversions) sets up a spline.  
01095 #   If lowerSlope or upperSlope is None, the corresponding boundary is set to 'natural', with zero second derivative.
01096 #   XConversions is a list of g, g', g'' to evaluate for transforming the X axis.  
01097 #   YConversions is a list of f, f', f'', f(-1) to evaluate for transforming the Y axis.
01098 #       Note that the y transform f and f(-1) MUST be exact inverses, or the system will melt.
01099 #   
01100 class InterpolatingFunction(C2Function):
01101     """An InterpolatingFunction stores a cubic spline or piecewise linear representation of a set of x,y pairs.
01102         It can also transform the variable on input and output, so that the underlying spline may live in log-log space, 
01103         but such transforms are transparent to the setup and use of the function.  This makes it possible to
01104         store splines of, e.g., data which are very close to a power law, as a LogLogInterpolatingFunction, and
01105         to then have very accurate interpolation and extrapolation, since the curvature of such a function is small in log-log space.
01106         
01107         InterpolatingFunction(x, y, lowerSlope, upperSlope, XConversions, YConversions, cubic_spline) sets up a spline.  
01108         If lowerSlope or upperSlope is None, the corresponding boundary is set to 'natural', with zero second derivative.
01109         XConversions is a list of g, g', g'' to evaluate for transforming the X axis.  
01110         YConversions is a list of f, f', f'', f(-1) to evaluate for transforming the Y axis.
01111             Note that the y transform f and f(-1) MUST be exact inverses, or the system will melt.
01112         If cubic_spline is True (default), create a cubic spline, otherwise, create a piecewise linear interpolator.
01113                 
01114     """ 
01115     YConversions=None
01116     XConversions=None
01117     name='data'
01118     ClassName='InterpolatingFunction'
01119     
01120     ##
01121     ## create the InterpolatingFunction
01122     #\param x the array of abscissas
01123     #\param y the array of ordinates
01124     #\param lowerSlope if not None, the slope at the lower end of the spline.  If None, use 'natural' spline with zero second derivative
01125     #\param upperSlope if not None, the slope at the lower end of the spline.  If None, use 'natural' spline with zero second derivative
01126     #\param XConversions a list of functions used for transforming the abscissa
01127     #\param YConversions a list of functions used for tranforming the ordinate
01128     #\param cubic_spline is true for a normal cubic spline, false for piecewise linear interpolation
01129     def __init__(self, x, y, lowerSlope=None, upperSlope=None, XConversions=None, YConversions=None, cubic_spline=True):
01130         C2Function.__init__(self) #just on general principle, right now this does nothing
01131         self.SetDomain(min(x), max(x))
01132         self.Xraw=_numeric.array(x) #keep a private copy
01133         self.xInverted=False
01134         
01135         if YConversions is not None: self.YConversions=YConversions #inherit from class if not passed       
01136         if self.YConversions is None:
01137             self.fYin, self.fYinP, self.fYinPP, self.fYout = self.YConversions = _identity, _one, _zero, _identity
01138             self.yNonLin=False
01139             F=self.F=_numeric.array(y)
01140         else:
01141             self.fYin, self.fYinP, self.fYinPP, self.fYout = self.YConversions
01142             self.yNonLin=True
01143             self.F=_numeric.array([self.fYin(q) for q in y])
01144             if lowerSlope is not None: lowerSlope *= self.fYinP(y[0])
01145             if upperSlope is not None: upperSlope *= self.fYinP(y[-1])
01146 
01147         if XConversions is not None: self.XConversions=XConversions #inherit from class if not passed       
01148         if self.XConversions is None:
01149             self.fXin, self.fXinP, self.fXinPP, self.fXout = self.XConversions =  _identity, _one, _zero, _identity
01150             self.xNonLin=False
01151             self.X=_numeric.array(x)
01152         else:
01153             self.fXin, self.fXinP, self.fXinPP, self.fXout = self.XConversions
01154             self.xNonLin=True
01155             self.X=_numeric.array([self.fXin(q) for q in x])
01156             if lowerSlope is not None: lowerSlope /= self.fXinP(x[0])
01157             if upperSlope is not None: upperSlope /= self.fXinP(x[-1])
01158 
01159             if self.X[0] > self.X[-1]: #make sure our transformed X array is increasing
01160                 self.Xraw=self.Xraw[::-1]
01161                 self.X=self.X[::-1]
01162                 self.F=self.F[::-1]
01163                 self.xInverted=True
01164                 lowerSlope, upperSlope=upperSlope, lowerSlope
01165                     
01166         dx=self.X[1:]-self.X[:-1]
01167         if min(dx) <  0 or min(self.X) < self.X[0] or max(self.X) > self.X[-1]:
01168             raise C2Exception("monotonicity error in X values for interpolating function: "  + 
01169                 _numeric.array_str(self.X))
01170         if cubic_spline:
01171             self.y2=_spline(self.X, self.F, yp1=lowerSlope, ypn=upperSlope)
01172         else:
01173             #use a dummy y2 table if we are not really cubic splining
01174             self.y2=_numeric.zeros(len(self.X), _numeric_float)
01175     ##
01176     
01177     def value_with_derivatives(self, x): 
01178         if self.xNonLin or self.yNonLin: #skip this if this is a completely untransformed spline
01179             y0, yp0, ypp0=_splint(self.X, self.F, self.y2, self.fXin(x), derivs=True)
01180             y=self.fYout(y0)
01181             fpi=1.0/self.fYinP(y)
01182             gp=self.fXinP(x)
01183 
01184             # from Mathematica Dt[InverseFunction[f][g[y[x]]], x]
01185             yprime=gp*yp0*fpi # the real derivative of the inverse transformed output 
01186             fpp=self.fYinPP(y)
01187             gpp=self.fXinPP(x)
01188             #also from Mathematica Dt[InverseFunction[f][g[y[x]]], {x,2}]
01189             yprimeprime=(gp*gp*ypp0 + yp0*gpp - gp*gp*yp0*yp0*fpp*fpi*fpi)*fpi
01190             return native(y, yprime, yprimeprime)
01191         else:
01192             return _splint(self.X, self.F, self.y2, x, derivs=True)
01193     ##
01194     ## \brief Set extrapolation on left end of data set.
01195     #
01196     # This will be dynamically assigned to either SetLowerExtrapolation or SetUpperExtrapolation by the constructor
01197     # depending on whether the transformed spline is running in increasing or decreasing order.
01198     # This is necessary since the arrays may have been  reversed because of a transformation of \a x.
01199     # \param bound the bound to which the left edge of the data should be extrapolated
01200     def SetLeftExtrapolation(self, bound):
01201         """Set extrapolation on left end of data set.  
01202         This will be dynamically assigned to either SetLowerExtrapolation or SetUpperExtrapolation by the constructor
01203         """
01204         xmin=self.fXin(bound)
01205         if xmin >= self.X[0]: 
01206             raise C2Exception("Attempting to extrapolate spline within its current range: bound = %g, bounds [%g, %g]" % 
01207                 ((bound,) + self.GetDomain()) )
01208         
01209         self.X, self.F, self.y2=_spline_extension(self.X, self.F, self.y2, xmin=xmin)
01210         self.Xraw=_numeric.concatenate(((bound, ), self.Xraw))
01211         self.SetDomain(min(self.Xraw), max(self.Xraw))
01212         
01213     ##
01214     ## \brief Set extrapolation on right end of data set.
01215     #
01216     # This will be dynamically assigned to either SetLowerExtrapolation or SetUpperExtrapolation by the constructor
01217     # depending on whether the transformed spline is running in increasing or decreasing order.
01218     # This is necessary since the arrays may have been  reversed because of a transformation of \a x.
01219     # \param bound the bound to which the right edge of the data should be extrapolated
01220     def SetRightExtrapolation(self, bound):
01221         """Set extrapolation on right end of data set.  
01222         This will be dynamically assigned to either SetLowerExtrapolation or SetUpperExtrapolation by the constructor
01223         """
01224         xmax=self.fXin(bound)
01225         if xmax <= self.X[-1]: 
01226             raise C2Exception("Attempting to extrapolate spline within its current range: bound = %g, bounds [%g, %g]" % 
01227                 ((bound,) + self.GetDomain()) )
01228 
01229         self.X, self.F, self.y2=_spline_extension(self.X, self.F, self.y2, xmax=xmax)
01230         self.Xraw=_numeric.concatenate((self.Xraw, (bound, )))
01231         self.SetDomain(min(self.Xraw), max(self.Xraw))
01232 
01233     ##
01234     ## \brief set the extrapolation permitted on the left edge of the original data set (lowest \a x value)
01235     # \param bound the bound to which the left edge of the data should be extrapolated      
01236     def SetLowerExtrapolation(self, bound):
01237         if not self.xInverted:
01238             self.SetLeftExtrapolation(bound)
01239         else:
01240             self.SetRightExtrapolation(bound)
01241 
01242     ##
01243     ## \brief set the extrapolation permitted on the right edge of the original data set (hightest \a x value)
01244     # \param bound the bound to which the right edge of the data should be extrapolated     
01245     def SetUpperExtrapolation(self, bound):
01246         if self.xInverted:
01247             self.SetLeftExtrapolation(bound)
01248         else:
01249             self.SetRightExtrapolation(bound)
01250 
01251     ##
01252     ## \brief legacy...
01253     def YtoX(self):
01254         "returns a new InterpolatingFunction with our current grid of Y values as the X values"
01255         
01256         yv=self.fYout(self.F) #get current Y values transformed out
01257         if yv[1] < yv[0]: yv=yv[::-1]
01258         f=InterpolatingFunction(yv, yv, XConversions=self.XConversions, YConversions=self.YConversions)
01259         f.SetName("x values: "+self.name)
01260         return f
01261     ##
01262     ## \brief create new InterpolatingFunction C2source(self) evaluated pointwise
01263     # \param C2Source the outer member of the composition
01264     # \return new InterpolatingFunction using the same transformation rules as the parent
01265     # \note This has different derivatives than C2ComposedFunction(\a self, \a right) since it is evaluated pointwise
01266     # and then re-splined.  If you want the full derivative information, use C2ComposedFunction()
01267     def UnaryOperator(self, C2source):
01268         "return new InterpolatingFunction C2source(self)"
01269         y=[C2source(self.fYout(yy)) for yy in self.F] #get array of y values efficiently
01270         #get exact derivative of composition at each end to seed new spline.
01271         junk, yp0, junk = C2source.compose(self, self.Xraw[0]) 
01272         junk, ypn, junk = C2source.compose(self, self.Xraw[-1]) 
01273 
01274         f=InterpolatingFunction(self.Xraw, y, lowerSlope=yp0, upperSlope=ypn,
01275             XConversions=self.XConversions, YConversions=self.YConversions)
01276         f.name=C2source.name+'('+self.name+')'
01277         return f
01278 
01279     ##
01280     ## \brief create new InterpolatingFunction self +-*/ rhs (or any other binary operator) evaluated pointwise
01281     # \param rhs the right hand function for the binary
01282     # \param c2binary the class (NOT an instance) for the binary operator
01283     # \return new InterpolatingFunction using the same transformation rules as the parent
01284     # \note This has different derivatives than a regular binary operator such as C2Product since it is evaluated pointwise
01285     # and then re-splined.  If you want the full derivative information, use C2Product() (e.g.)
01286     def BinaryOperator(self, rhs, c2binary):
01287         "return new InterpolatingFunction self +-*/ rhs (or any other binary operator)"
01288         bf=c2binary(self, rhs)
01289 
01290         yv=[bf.value_with_derivatives(x) for x in self.Xraw] #get array of ( ( y, y', y''), ...)
01291         y=[yy[0] for yy in yv] #get y values only
01292 
01293         f=InterpolatingFunction(self.Xraw, y, lowerSlope=yv[0][1], upperSlope=yv[-1][1],
01294             XConversions=self.XConversions, YConversions=self.YConversions)
01295         f.name=bf.name
01296         return f
01297     ##
01298     ## \brief python operator to return a new InterpolatingFunction \a self +\a right evaluated pointwise
01299     # \param right the function to add
01300     # \return a new InterpolatingFunction with the same transformations as the parent
01301     def __add__(self, right):
01302         return self.BinaryOperator(right, C2Sum)
01303     ##
01304     ## \brief python operator to return a new InterpolatingFunction \a self -\a right evaluated pointwise
01305     # \param right the function to subtract
01306     # \return a new InterpolatingFunction with the same transformations as the parent
01307     def __sub__(self, right):
01308         return self.BinaryOperator(right, C2Diff)
01309     ##
01310     ## \brief python operator to return a new InterpolatingFunction \a self *\a right evaluated pointwise
01311     # \param right the function to multiply
01312     # \return a new InterpolatingFunction with the same transformations as the parent
01313     # \note This has different derivatives than C2Product(\a self, \a right) since it is evaluated pointwise
01314     # and then re-splined.  If you want the full derivative information, use C2Product()
01315     def __mul__(self, right):
01316         return self.BinaryOperator(right, C2Product)
01317     ##
01318     ## \brief python operator to return a new InterpolatingFunction \a self /\a right evaluated pointwise
01319     # \param right the denominator function 
01320     # \return a new InterpolatingFunction with the same transformations as the parent
01321     # \note This has different derivatives than C2Ratio(\a self, \a right) since it is evaluated pointwise
01322     # and then re-splined.  If you want the full derivative information, use C2Ratio()
01323     def __div__(self, right):
01324         return self.BinaryOperator(right, C2Ratio)
01325 ##
01326     
01327 LogConversions=_mylog, _recip, _mrecip2, _myexp
01328 ##
01329 ## \brief An InterpolatingFunction which stores log(x) vs. y
01330 class LogLinInterpolatingFunction(InterpolatingFunction):
01331     "An InterpolatingFunction which stores log(x) vs. y"
01332     ClassName='LogLinInterpolatingFunction'
01333     XConversions=LogConversions
01334 ##
01335 ## \brief An InterpolatingFunction which stores x vs. log(y)
01336 class LinLogInterpolatingFunction(InterpolatingFunction):
01337     "An InterpolatingFunction which stores x vs. log(y), useful for functions with exponential-like behavior"
01338     ClassName='LinLogInterpolatingFunction'
01339     YConversions=LogConversions
01340 ##
01341 ## \brief An InterpolatingFunction which stores log(x) vs. log(y)
01342 class LogLogInterpolatingFunction(InterpolatingFunction):
01343     "An InterpolatingFunction which stores log(x) vs. log(y), useful for functions with power-law-like behavior"
01344     ClassName='LogLogInterpolatingFunction'
01345     XConversions=LogConversions
01346     YConversions=LogConversions
01347 ##
01348 ## \brief legacy...
01349 def LinearInterpolatingGrid(xmin, dx, count):
01350     """create a linear-linear interpolating grid with both x & y set to (xmin, xmin+dx, ... xmin + dx*(count -1) )
01351         very useful for transformaiton with other functions e.g. 
01352         f=C2sin(LinearInterpolatingGrid(-0.1,0.1, 65)) creates a spline table of sin(x) slightly beyond the first period
01353     """
01354     x=[xmin + dx*i for i in xrange(count)]
01355     return InterpolatingFunction(x,x).SetName('x')
01356 ##
01357 ## \brief legacy...
01358 def LogLogInterpolatingGrid(xmin, dx, count):
01359     "create a log-log interpolating grid with both x & y set to (xmin, xmin*dx, ... xmin * dx**(count -1) )"
01360     x=[xmin]
01361     for i in xrange(count-1):
01362         x.append(x[-1]*dx)
01363     return LogLogInterpolatingFunction(x,x).SetName('x')
01364 
01365 class AccumulatedHistogram(InterpolatingFunction):
01366     """An InterpolatingFunction which is the cumulative integral of the (histogram) specified by binedges and binheights.
01367         Note than binedges should be one element longer than binheights, since the lower & upper edges are specified. 
01368         Note that this is a malformed spline, since the second derivatives are all zero, so it has less continuity.
01369         Also, note that the bin edges can be given in backwards order to generate the reversed accumulation (starting at the high end) 
01370     """
01371     ClassName='AccumulatedHistogram'
01372 
01373     def __init__(self, binedges, binheights, normalize=False, inverse_function=False, drop_zeros=True, **args):
01374         be=_numeric.array(binedges, _numeric_float)
01375         bh=_numeric.array(binheights, _numeric_float)
01376 
01377         if drop_zeros or inverse_function: #invese functions cannot have any zero bins or they have vertical sections
01378         
01379             nz=_numeric.not_equal(bh, 0) #mask of non-empty bins, or lower edges
01380             if not inverse_function: nz[0]=nz[-1]=1 #always preserve end bins to keep X range, but don't dare do it for inverses
01381             bh=_numeric.compress(nz, bh)
01382             be=_numeric.compress(_numeric.concatenate( (nz, (nz[-1],) ) ), be)
01383                 
01384         cum=_numeric.concatenate( ( (0,), _numeric.cumsum( (be[1:]-be[:-1])*bh ) ))
01385         
01386         if be[1] < be[0]: #fix backwards bins, if needed.
01387             be=be[::-1]
01388             cum=cum[::-1]
01389             cum*=-1 #the dx values were all negative if the bins were backwards, so fix the sums
01390 
01391         if normalize:
01392             cum *= (1.0/max(cum[0], cum[-1])) #always normalize on the big end
01393 
01394         if inverse_function:
01395             be, cum = cum, be #build it the other way around
01396             
01397         InterpolatingFunction.__init__(self, be, cum, **args)
01398         self.y2 *=0 #clear second derivatives... we know nothing about them
01399 
01400 class LogLogAccumulatedHistogram(AccumulatedHistogram):
01401     "same as AccumulatedHistogram, but log-log axes"
01402     ClassName='LogLogAccumulatedHistogram'
01403     XConversions=LogConversions
01404     YConversions=LogConversions
01405 
01406 class InverseIntegratedDensity(InterpolatingFunction):
01407     """InverseIntegratedDensity starts with a probability density function, generates the integral, 
01408     and generates a LinLogInterpolatingFunction which, when evaluated using a uniform random on [0,1] returns values
01409     with a density distribution equal to the input distribution
01410     If the data are passed in reverse order (large X first), the integral is carried out from the big end,
01411     and then the data are reversed to the result in in increasing X order.  
01412     """
01413     
01414     IntermediateInterpolator=InterpolatingFunction
01415     
01416     def __init__(self, bincenters, binheights):
01417         
01418         if not isinstance(binheights, C2Function): #must be a data table, create interpolator
01419             np=len(binheights)  
01420             be=_numeric.array(bincenters)
01421             bh=_numeric.array(binheights)
01422                 
01423             reversed = be[1] < be[0]    #// check for backwards  channels
01424         
01425             if reversed:
01426                 be=be[::-1]
01427                 bh=bh[::-1]
01428         
01429             temp=self.IntermediateInterpolator(be, bh)      #// create a temporary InterpolatingFunction to integrate
01430         else:
01431             temp=binheights
01432             
01433         # integrate from first to last bin in original order, leaving results in integral
01434         # ask for relative error of 1e-6 on each bin, with absolute error set to 0 (since we don't know the data scale).
01435         integral=[0] + temp.partial_integrals(bincenters, 
01436                 absolute_error_tolerance=0, 
01437                 relative_error_tolerance=1e-6) 
01438     
01439         scale=1.0/sum(integral) 
01440 
01441         for i in range(1,len(integral)):
01442             integral[i]=integral[i]*scale + integral[i-1]
01443         integral[-1]=1.0 #force exact value on the boundary 
01444         
01445         InterpolatingFunction.__init__(self, integral, bincenters, 
01446                             lowerSlope=1.0/(scale*temp(bincenters[0])), upperSlope=1.0/(scale*temp(bincenters[-1]))
01447                 )   # use integral as x axis in inverse function
01448 
01449 class LinLogInverseIntegratedDensity(InverseIntegratedDensity):
01450     YConversions=LogConversions
01451     IntermediateInterpolator=LogLogInterpolatingFunction
01452 
01453 class C2InverseFunction(C2Function):
01454     """C2InverseFunction creates a C2Function h(x) which is the solution to g(h)=x.  It is different than just finding the 
01455     root, because it provides the derivatives, so it is a first-class C2Function"""
01456     
01457     def __init__(self, sourceFunction):
01458         "create the C2InverseFunction from the given function, and set the initial search to be from the center of the domain"
01459         self.fn=sourceFunction
01460         l,r=sourceFunction.GetDomain()
01461         self.start_hint=(l+r)*0.5
01462         # compute our domain assuming the function is monotonic so its values on its domain boundaries are our domain
01463         ly=sourceFunction(l)
01464         ry=sourceFunction(r)
01465         if ly>ry: ly, ry = ry, ly 
01466         self.SetDomain(ly, ry)
01467 
01468     def set_start_hint(self, hint):
01469         "set a hint for where to start looking for the inverse solution.  Each time a solutions is found, this is automatically updated"
01470         self.start_hint=hint
01471         
01472     def value_with_derivatives(self, x): 
01473         "return the sought value, and update the start_hint so evaluations at nearby points will be very fast"
01474         l,r=self.fn.GetDomain()
01475         y = self.fn.find_root(l, r, self.start_hint, x)
01476         y0, yp, ypp=self.fn.value_with_derivatives(y)
01477         self.start_hint=y #always start looking near previous solution, unless value is reset explicitly
01478         return y, 1.0/yp, -ypp/yp**3 #appropriate inverse derivs from MMA
01479 
01480 class C2ConnectorFunction(C2Function):
01481     """C2ConnectorFunction generates a smooth conection between two other C2Function instances.
01482     """
01483     def __init__(self, f1, f2, x0, x2, auto_center=False, y1=0.0):
01484         """ connect f1(x0) to f2(x2) with a very smooth polynomial.  If auto_center is False,
01485             function(midpoint)=y1 at midpoint of the join and poly is 6th order.
01486             If auto_center is True, poly is 5th order, and y(midpoint) is whatever it has to be.""" 
01487         fdx=self.fdx=(x2-x0)/2.0
01488         self.fhinv=1.0/fdx
01489         self.fx1=(x0+x2)/2.0
01490         sef.fx0=x0
01491         self.fx2=x2
01492         
01493         y0, yp0, ypp0=f1.value_with_derivatives(x0) # get left wall values from conventional computation
01494         y2, yp2, ypp2=f2.value_with_derivatives(x2) # get right wall values from conventional computation
01495 
01496         # scale derivs to put function on [-1,1] since mma  solution is done this way
01497         yp0*=fdx
01498         yp2*=fdx
01499         ypp0*=fdx*fdx
01500         ypp2*=fdx*fdx
01501         
01502         ff0=(8*(y0 + y2) + 5*(yp0 - yp2) + ypp0 + ypp2)*0.0625
01503         if auto_center: y1=ff0 # forces ff to be 0 if we are auto-centering
01504         
01505         # y[x_] = y1 + x (a + b x) + x [(x-1) (x+1)] (c + d x) + x (x-1)^2 (x+1)^2 (e + f x)
01506         # y' = a + 2 b x + d x [(x+1)(x-1)] + (c + d x)(3x^2-1) + f x [(x+1)(x-1)]^2 + (e + f x)[(x+1)(x-1)](5x^2-1)  
01507         # y'' = 2 b + 6x(c + d x) + 2d(3x^2-1) + 4x(e + f x)(5x^2-3) + 2f(x^2-1)(5x^2-1)
01508         self.fy1=y1 
01509         self.fa=(y2 - y0)*0.5
01510         self.fb=(y0 + y2)*0.5 - y1
01511         self.fc=(yp2+yp0-2.*self.fa)*0.25
01512         self.fd=(yp2-yp0-4.*self.fb)*0.25
01513         self.fe=(ypp2-ypp0-12.*self.fc)*0.0625
01514         self.ff=(ff0 - y1)
01515         self.SetDomain(x0,x2) # this is where the function is valid
01516     def value_with_derivatives(self, x):
01517         fhinv=self.fhinv
01518         dx=(x-self.fx1)*fhinv
01519         q1=(x-self.fx0)*(x-self.fx2)*fhinv*fhinv # exactly vanish all bits at both ends
01520         q2=dx*q1
01521         
01522         r1=self.fa+self.fb*dx
01523         r2=self.fc+self.fd*dx
01524         r3=self.fe+self.ff*dx
01525         
01526         y=self.fy1+dx*r1+q2*r2+q1*q2*r3
01527         
01528         q3=3*q1+2
01529         q4=5*q1+4
01530         yprime=(self.fa+2*self.fb*dx+self.fd*q2+r2*q3+self.ff*q1*q2+q1*q4*r3)*fhinv
01531         yprime2=2*(self.fb+self.fd*q3+3*dx*r2+self.ff*q1*q4+r3*(2*dx*(5*q1+2)))*fhinv*fhinv
01532         return y, yprime, yprime2
01533     
01534 class C2LHopitalRatio(C2Ratio):
01535     """C2LHopitalRatio(a,b) returns a new C2Function which evaluates as a/b with special care near zeros of the denominator.
01536         It caches coefficients once it has found a zero, and evaluates very quickly afterwards near that point.
01537     """
01538     name='/'
01539     
01540     #these are tuning parameters.  Mostly, epsx1 is the touchiest, to get the right second derivative at the crossing
01541     #eps0 sets the range around a denominator zero which triggers L'Hopital's rule.  If abs(dx) < eps0*x, the rule is used
01542     epsx1=1e-4
01543     epsx2=1e-4
01544     eps0=1e-5
01545     eps1=1e-14
01546     dblmin=2e-308
01547     cache=None
01548     
01549     def dxsolve(self, x, y, yp, ypp):
01550         "find a very nearby zero of a function based on its derivatives"
01551         a=ypp/2 #second derivative is 2*a
01552         b=yp
01553         c=y
01554         
01555         disc=b*b-4*a*c
01556         if disc >= 0:
01557             if b>=0:
01558                 q=-0.5*(b+math.sqrt(disc))
01559             else:
01560                 q=-0.5*(b-math.sqrt(disc))
01561 
01562             if q*q > abs(a*c): delta=c/q    #since x1=q/a, x2=c/q, x1/x2=q^2/ac, this picks smaller step first
01563             else: delta=q/a
01564         else:
01565             raise C2Exception("Thought a root should be near x= %g, didn't find one" % x)
01566         
01567         return delta
01568     
01569     def value_with_derivatives(self, x):
01570         "combine left and right functions into ratio, being very careful about zeros of the denominator"
01571         cache=self.cache
01572         if cache is None or x < cache[0] or x > cache[2]:  #can't get it out of cache, must compute something
01573             y0, yp0, ypp0=self.left.value_with_derivatives(x)
01574             y1, yp1, ypp1=self.right.value_with_derivatives(x)
01575             
01576             if  ( abs(y1) < self.eps0*abs(yp1)*(abs(x)+self.dblmin) ): # paranoid check for relative proximity to a simple denominator zero
01577                 dx0=self.dxsolve(x, y0, yp0, ypp0)
01578                 dx1=self.dxsolve(x, y1, yp1, ypp1)
01579                                     
01580                 if abs(dx1-dx0) > self.eps1*abs(x):
01581                     raise C2Exception("y/0 found at x=%g in LHopitalRatio" % x)
01582                         
01583                 dx=abs(x)*self.epsx1+self.epsx2
01584                 
01585                 #compute the walls of the region to be fixed with l'Hopital
01586                 x1=x+dx1
01587                 x0=x1-dx
01588                 x2=x1+dx 
01589         
01590                 y0, yp0, ypp0=C2Ratio.value_with_derivatives(self, x0) #get left wall values from conventional computation
01591                 y2, yp2, ypp2=C2Ratio.value_with_derivatives(self, x2) #get right wall values                       
01592                 
01593                 yy0, yyp0, yypp0=self.left.value_with_derivatives(x1)
01594                 yy1, yyp1, yypp1=self.right.value_with_derivatives(x1)
01595                 #now use L'Hopital's rule to find function at the center
01596                 y1=yyp0/yyp1
01597                                 
01598                 #scale derivs to put function on [-1,1] since mma  solution is done this way
01599                 yp0*=dx
01600                 yp2*=dx
01601                 ypp0*=dx*dx
01602                 ypp2*=dx*dx
01603                 
01604                 #y[x_] = y1 + x (a + b x) + (x-1) x (x+1) (c + d x + e x^2 + f x^3)
01605                 coefs=( y1, 
01606                     -(y0 - y2)/2.,
01607                     (y0 - 2*y1 + y2)/2.,
01608                     (7*y0 - 7*y2 + 7*yp0 + 7*yp2 + ypp0 - ypp2)/16.,
01609                     (-16*y0 + 32*y1 - 16*y2 - 9*yp0 + 9*yp2 - ypp0 - ypp2)/16.,
01610                     (-3*y0 + 3*y2 - 3*yp0 - 3*yp2 - ypp0 + ypp2)/16.,
01611                     (8*y0 - 16*y1 + 8*y2 + 5*yp0 - 5*yp2 + ypp0 + ypp2)/16.
01612                 )
01613                 #y'[x] = a + 2 b x + (3x^2 - 1)   (c + d x + e x^2 + f x^3) + (x-1) x (x+1) (d + 2 e x + 3 f x^2 )
01614                 #y''[x] = 2b + (x-1) x (x+1) (2 e + 6 f x) + 2 (3 x^2 -1) (d + 2 e x + 3 f x^2 ) + 6 x (c + d x + e x^2 + f x^3)
01615                 
01616                 self.cache=x0,x1,x2, coefs
01617                 
01618             else: #not close to a zero of the denominator... compute conventional answer for ratio
01619                 return y0/y1, (yp0*y1-y0*yp1)/(y1*y1), (y1*y1*ypp0+y0*(2*yp1*yp1-y1*ypp1)-2*y1*yp0*yp1)/(y1*y1*y1)
01620 
01621         #if we get here, the poly coefficients are ready to go. 
01622         x0, x1, x2, (y1, a,b,c,d,e,f)=self.cache
01623         
01624         dx0=x1-x0
01625         dx=(x-x1)/dx0
01626         
01627         q1=c + dx*(d + dx*(e + dx*f))
01628         q2 =d + dx*(2*e + dx*3*f)   
01629         q3=2*e+6*f*dx
01630         
01631         xp1=(dx-1)*(dx+1)*dx
01632         xp2=(3*dx*dx-1)
01633         
01634         y= y1 + dx*(a+b*dx) + xp1*q1
01635         yp=a + 2*b*dx + xp2*q1 + xp1*q2
01636         ypp=2*b+xp1*q3+2*xp2*q2+6*dx*q1
01637             
01638         return y, yp/dx0, ypp/dx0/dx0
01639 
01640 if __name__=="__main__":
01641     print _rcsid
01642     def as(x): return _numeric.array_str(x, precision=3)
01643     
01644     if 1:
01645         ag=ag1=LinearInterpolatingGrid(1, 1.0,4)    
01646         print ag    
01647         try:
01648             ag.SetLowerExtrapolation(2)
01649         except:
01650             import sys
01651             print "***got expected error on bad extrapolation: ", sys.exc_value
01652         else:
01653             print "***failed to get expected exception on bad extrapolation"
01654             
01655         ag.SetLowerExtrapolation(-2)
01656         ag.SetUpperExtrapolation(15)
01657         print ag
01658         
01659         print C2Constant(11.5)
01660         print C2Quadratic(x0=5, a=2, b=1, c=0)
01661         print C2PowerLaw(a=1.5, b=-2.3)
01662         print LogLogInterpolatingGrid(0.1, 1.1, 20)
01663         
01664         print C2Linear(1.3,2.5).apply(ag1)
01665         print C2Quadratic(0,1,0,0).apply(ag1)
01666         print ag1*ag1*ag1
01667         print (ag1*ag1*ag1).YtoX()
01668             
01669         try:
01670             ag13=(ag1*ag1).YtoX()
01671         except:
01672             import sys
01673             print "***got expected error on bad X axis: ", sys.exc_value
01674         else:
01675             print "***failed to get expected exception on bad X axis"
01676             
01677         fn=C2sin(C2sqrt(ag1*ag1*ag1)).SetDomain(0, ag1.GetDomain()[1]) #cut off sqrt(negative)
01678         print fn
01679         
01680         for i in range(10):
01681             print i, "%20.15f %20.15f" % (math.sin((i+0.01)**(3./2.)), fn(i+0.01) )
01682         
01683         x1=fn.find_root(0.0, 1.35128, 0.1, 0.995, trace=True)
01684         print x1, math.sin(x1**(3./2.)), fn(x1)-0.995
01685         
01686         print fn([1., 2., 3., 4.])
01687         
01688         print "\nPower law sin(x)**log(x) tests"
01689         fn=C2sin**C2log
01690         for i in range(10):
01691             x=0.1*i + 6.4
01692             print ( "%10.3f "+3*"%15.12f ")%( (x, )+fn.value_with_derivatives(x))
01693             
01694         print "\nPower law sin(x)**3.2 tests"
01695         fn=C2sin**3.2
01696         for i in range(10):
01697             x=0.1*i + 6.4
01698             print ( "%10.3f "+3*"%15.12f ")%( (x, )+fn.value_with_derivatives(x))
01699 
01700         import math
01701         print "\nIntegration tests"
01702         sna=C2sin(C2PowerLaw(1,2)) #integrate sin(x**2)
01703         for sample in (5, 11, 21, 41, 101):
01704             xg=_numeric.array(range(sample), _numeric_float)*(2.0/(sample-1))
01705             partials=sna.partial_integrals(xg)
01706             if sample==10: print _numeric.array_str(partials, precision=8, suppress_small=False, max_line_width=10000)
01707             sumsum=sum(partials)
01708             yg=sna(xg)
01709             simp=sum(sna.partial_integrals(xg, derivs=1)) 
01710             exact=0.804776489343756110
01711             print ("%3d "+6*"%18.10g") %  (sample, 
01712                 simp, simp-exact, (exact-simp)*sample**4, 
01713                 sumsum, sumsum-exact, (exact-sumsum)*sample**6) #the comparision is to the Fresnel Integral from Mathematica
01714     
01715     if 0:
01716         print "\nBessel Functions by integration"
01717         print """Warning... the adaptive integrator looks worse than the non-adaptive one here. 
01718         There is some subtle cancellation which makes uniform sampling give extremely accurate answers for Bessel's integral, 
01719         so it isn't the fault of the adaptive integrator.  The simple one works way too well, here, by accident.
01720         """
01721             
01722         def bessj(n, z, point_density=2):
01723             f=C2cos(C2Linear(slope=n) - C2Constant(z) * C2sin)
01724             pc=int((abs(z)+abs(n)+2)*point_density)
01725             g=_numeric.array(range(pc), _numeric_float)*(math.pi/(pc-1))
01726             return sum(f.partial_integrals(g, allow_recursion=False))/math.pi, f.total_func_evals
01727         
01728         def bessj_adaptive(n, z, derivs=1):
01729             f=C2cos(C2Linear(slope=n) - C2Constant(z) * C2sin)
01730             pc=8
01731             g=_numeric.array(range(pc), _numeric_float)*(math.pi/(pc-1))
01732             return sum(f.partial_integrals(g, absolute_error_tolerance=1e-14, relative_error_tolerance=1e-14, debug=0, derivs=derivs))/math.pi, f.total_func_evals
01733 
01734         for order, x in ( (0, 0.1), (0,5.5), (2,3.7), (2,30.5)):
01735             v1, n1=bessj(order, x)
01736             v2, n2=bessj_adaptive(order, x, 1)
01737             v3, n3=bessj_adaptive(order, x, 2)
01738             print ("%6.2f %6.2f %6d %6d %6d "+3*"%20.15f ") % (order, x, n1, n2, n3, v1, v2, v3 )
01739         
01740 
01741     if 0:
01742         print "\nLogarithms  by integration"
01743         
01744         pc=3
01745         for lv in (0.1, 1.0, 2.0, 5.0, 10.0):
01746             b=math.exp(lv)
01747             np=int(pc*b)+4
01748             g=_numeric.array(range(np), _numeric_float)*(b-1)/(np-1)+1
01749 
01750             v0=C2recip.partial_integrals(g, allow_recursion=False)
01751             n0=C2recip.total_func_evals
01752             
01753             
01754             v1=C2recip.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)), absolute_error_tolerance=1e-6, 
01755                     extrapolate=1, debug=0, derivs=0)
01756             n1=C2recip.total_func_evals
01757 
01758             v2=C2recip.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)), absolute_error_tolerance=1e-12, 
01759                     extrapolate=1, debug=0, derivs=1)
01760             n2=C2recip.total_func_evals
01761             
01762             v3=C2recip.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)), absolute_error_tolerance=1e-12, 
01763                     extrapolate=1, debug=0, derivs=2)
01764             n3=C2recip.total_func_evals
01765 
01766             print ("%20.15f %10.2f %6d %6d %6d %6d "+4*"%20.15f ") % (lv, b, n0, n1, n2, n3, sum(v0), sum(v1), sum(v2), sum(v3) )
01767 
01768             
01769     if 0:
01770         print "\nPowers  by integration"
01771         
01772         fn=C2recip(C2Quadratic(a=1.0, b=0.01, c=0.01)) #make approximate power law
01773         
01774         pc=5
01775         for lv in (0.1, 1.0, 2.0, 5.0, 10.0):
01776             b=1.0+lv
01777             np=int(pc*b)+4
01778             g=_numeric.array(range(np), _numeric_float)*(b-1)/(np-1)+1
01779 
01780             v0=fn.partial_integrals(g, allow_recursion=False)
01781             n0=fn.total_func_evals
01782                         
01783             v1=fn.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)),
01784                     absolute_error_tolerance=1e-8,  relative_error_tolerance=1e-8,
01785                     extrapolate=0, debug=0, derivs=0)
01786             n1=fn.total_func_evals
01787 
01788             v2=fn.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)),
01789                     absolute_error_tolerance=1e-14,  relative_error_tolerance=1e-14,
01790                     extrapolate=1, debug=0, derivs=1)
01791             n2=fn.total_func_evals
01792         
01793             v3=fn.partial_integrals(_numeric.array((1.0,math.sqrt(b), b)),
01794                     absolute_error_tolerance=1e-14,  relative_error_tolerance=1e-14,
01795                     extrapolate=1, debug=0, derivs=2)
01796             n3=fn.total_func_evals
01797 
01798             print ("%20.15f %10.2f %6d %6d %6d %6d "+4*"%20.15f ") % (lv, b, n0, n1, n2, n3, sum(v0), sum(v1), sum(v2), sum(v3) )
01799 
01800     if 0:
01801         print "\nAccumulatedHistogram tests"
01802         xg=(_numeric.array(range(21), _numeric_float)-10.0)*0.25
01803         yy=_numeric.exp(-xg[:-1]*xg[:-1])
01804         yy[3]=yy[8]=yy[9]=0
01805         ah=AccumulatedHistogram(xg[::-1], yy[::-1], normalize=True)
01806         print ah([-2, -1, 0, 1, 2])
01807         ah=AccumulatedHistogram(xg[::-1], yy[::-1], normalize=True, drop_zeros=False)
01808         print ah([-2, -1, 0, 1, 2])
01809         ahi=AccumulatedHistogram(xg, yy, normalize=True, inverse_function=True)
01810         print ahi([0, 0.01,0.5, 0.7, 0.95, 1])
01811     
01812     
01813     if 0:
01814         xv=_numeric.array([1.5**(0.1*i) for i in range(100)])
01815         yv=_numeric.array([x**(-4)+0.25*x**(-3) for x in xv])
01816         f=LogLogInterpolatingFunction(xv,yv, 
01817             lowerSlope=-4*xv[0]**(-5)+(0.25)*(-3)*xv[0]**(-4),
01818             upperSlope=-4*xv[-1]**(-5)+(0.25)*(-3)*xv[-1]**(-4))
01819         f0=C2PowerLaw(1., -4)+C2PowerLaw(0.25, -3)
01820         print f(_numeric.array([1.,2.,3.,4., 5., 10., 20.]))
01821         print f0(_numeric.array([1.,2.,3.,4., 5., 10., 20.]))
01822         partials=f.partial_integrals(_numeric.array([1.,2.,3.,4., 5., 10., 20., 21.]), debug=False)
01823         print partials, sum(partials)
01824         partials=f.log_log_partial_integrals(_numeric.array([1.,2.,3.,4., 5., 10., 20., 21.]), debug=False)
01825         print partials, sum(partials)
01826         pp=f0.partial_integrals(_numeric.array(range(11), _numeric_float)*0.1 + 20)
01827         print pp, sum(pp)
01828     
01829     if 0:
01830         print "\nTesting LinLogInverseIntegratedDensity for 1/e^2"
01831         energies=[float(2**(0.5*n)) for n in range(41)]
01832         spect=[10000.0/(e*e) for e in energies]
01833         
01834         e0=energies[-1]
01835         e1=energies[0]
01836         
01837         pf=LinLogInverseIntegratedDensity(energies[::-1], spect[::-1])
01838         
01839         print energies[0], energies[-1]
01840         
01841         for i in range(41):
01842             r=(0.025*i)**2
01843             mma=(e0*e1)/(r*e0 + e1 - r*e1)
01844             print "%12.6f %12.4e %12.4e %12.4f " % (r, pf(r), mma, 100*(pf(r)-mma)/mma)
01845         
01846         print "\nTesting LinLogInverseIntegratedDensity for 1/e using a C2Function instead of a table"
01847         energies=[float(2.0**n) for n in range(21)]
01848         
01849         e0=energies[-1]
01850         e1=energies[0]
01851                 
01852         pf=LinLogInverseIntegratedDensity(energies[::-1], C2PowerLaw(a=1000.0, b=-1))
01853         
01854         print energies[0], energies[-1]
01855         
01856         for i in range(41):
01857             r=(0.025*i)**2
01858             mma=e0*(e1/e0)**r
01859             
01860             print "%12.6f %12.4e %12.4e %12.4e " % (r, pf(r), mma, 100*(pf(r)-mma)/mma)
01861 
01862     if 0:
01863         fn=C2sin *2.0 #make a new function
01864         print "\nInitial sampling grid =", 
01865         grid=(0., 3., 6., 9., 12.)
01866         for i in range(len(grid)): print grid[i],
01867         print
01868         
01869         fn.SetSamplingGrid(grid)
01870         
01871         print "Starting tests of samples from grid"
01872         
01873         for xmin,xmax in ( (-10,-1), (20,30), (-3, 15), (-3, 2), (-3, 7), (5.0, 5.5), (1., 7.), (5.99, 10), (2, 9.01), (5., 20.) ):
01874             v=fn.GetSamplingGrid(xmin,xmax)
01875             print "%10.3f %10.3f: " %(xmin, xmax),  
01876             for i in range(len(v)): print v[i],
01877             print
01878             
01879         sn=fn.NormalizedFunction(0., math.pi)
01880         
01881         print "integral of non-normalized and normalized function ", fn.integral(0., math.pi), sn.integral(0., math.pi)
01882         
01883         gn=fn.SquareNormalizedFunction(0., 4.0*math.pi)
01884         
01885         fn2=fn*fn
01886         fn2.SetSamplingGrid((0., math.pi, 2.0*math.pi, 3.0*math.pi, 4.0*math.pi))
01887         gn2=gn*gn
01888         gn2.SetSamplingGrid((0., math.pi, 2.0*math.pi, 3.0*math.pi, 4.0*math.pi))
01889         
01890         print "integral of square of non-normalized and square-normalized function ", fn2.integral(0., 4.0*math.pi), gn2.integral(0., 4.0*math.pi)
01891         
01892     
01893     if 1:
01894         import math
01895         print "\nL'Hopital's rule test"
01896         for fn, fn0 in (
01897             (C2LHopitalRatio(C2sin, C2Linear(y0=-math.pi) ) , lambda x: math.sin(x)/ (x-math.pi) ),
01898             (C2LHopitalRatio(C2sin, C2Linear(y0=-math.pi)/C2Linear()), lambda x: math.sin(x)/ (x-math.pi) * x),
01899             (C2LHopitalRatio(C2Linear()*C2sin, C2Linear(y0=-math.pi)), lambda x: math.sin(x)/ (x-math.pi) * x), 
01900         ):
01901             print
01902             print fn
01903             for x in (0.1, 1, math.pi-0.1, math.pi-0.001, math.pi-1e-6, math.pi+1e-14, math.pi+1e-12):
01904                 y, yp, ypp=fn.value_with_derivatives(x)
01905                 print 5*"%22.15f" % ( x, y, yp, ypp, fn0(x) )
01906     
01907     if 1:
01908         import math
01909         print "inverse exponential function test"
01910         myexp=_fC2exp() #make a private copy of exp so we can change its domain
01911         myexp.SetDomain(-10,10)
01912         a=C2InverseFunction(myexp)
01913         y, yp, ypp=a.value_with_derivatives(3)
01914         print y, yp, ypp
01915         print myexp(y), math.log(3), (a(3.01)-a(2.99))*50, (a(3.01)+a(2.99)-2.0*a(3))*10000
01916         print a.GetDomain()     
01917     

Generated on Wed Nov 21 10:18:31 2007 for analysis by  doxygen 1.5.4