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
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044 import math
00045 import operator
00046 import types
00047
00048 try:
00049
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
00072 class C2Exception(Exception):
00073 pass
00074
00075
00076
00077 class RangeError(IndexError):
00078 pass
00079
00080
00081
00082 class C2NakedFunction(C2Exception):
00083 pass
00084
00085
00086
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
00094
00095
00096
00097
00098 def __init__(self, *args) :
00099 if not args:
00100 self.xMin, self.xMax=-1e308, 1e308
00101 elif len(args)==1:
00102 self.xMin, self.xMax=args[0].xMin, args[0].xMax
00103 else:
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
00112 def __str__(self):
00113 return '<%s %s, Domain=[%g, %g]>' % ((self.ClassName, self.name,)+ self.GetDomain())
00114
00115
00116
00117
00118 def SetName(self, name):
00119 "set the short name of the function for __str__"
00120 self.name=name; return self
00121
00122
00123
00124
00125
00126 def value_with_derivatives(self, x):
00127 raise C2NakedFunction
00128
00129
00130
00131
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
00139
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
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
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
00160
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
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
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
00179 c, b, ypp=self.value_with_derivatives(root)
00180 c -= value
00181
00182 while abs(delta) > xtol:
00183 a=ypp/2
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
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
00198 root=0.5*(lower_bracket+upper_bracket)
00199 delta=upper_bracket-lower_bracket
00200
00201 c, b, ypp=self.value_with_derivatives(root)
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
00211
00212
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
00224
00225
00226
00227
00228
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
00235
00236 def GetDomain(self):
00237 "returns xMin, xMax"
00238 return self.xMin, self.xMax
00239
00240
00241
00242
00243
00244
00245
00246
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
00255
00256
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
00266
00267
00268 def __add__(self, right):
00269 "a+b returns a new C2Function which represents the sum"
00270 return C2Sum(self, right)
00271
00272
00273
00274
00275 def __sub__(self, right):
00276 "a-b returns a new C2Function which represents the difference"
00277 return C2Diff(self, right)
00278
00279
00280
00281
00282 def __mul__(self, right):
00283 "a*b returns a new C2Function which represents the product"
00284 return C2Product(self, right)
00285
00286
00287
00288
00289 def __div__(self, right):
00290 "a/b returns a new C2Function which represents the ratio"
00291 return C2Ratio(self, right)
00292
00293
00294
00295
00296
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
00303
00304
00305
00306
00307
00308
00309
00310
00311
00312
00313
00314
00315
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:
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
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
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
00376
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
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
00402 if extrapolate:
00403 eps=eps*eps_scale
00404
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
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
00436
00437
00438
00439
00440
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
00448
00449
00450
00451
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)
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]
00464
00465 if len(grid) > 2 :
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]
00469
00470 if len(grid) > 2 :
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]
00474
00475 return grid
00476
00477
00478
00479
00480
00481
00482
00483
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
00491
00492
00493
00494
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
00501
00502
00503
00504
00505
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
00519 class C2ScaledFunction(C2Function):
00520 "a lightweight class to create a function scaled vertically by a scale factor"
00521 ClassName='Scaled'
00522
00523
00524
00525
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
00540 class C2Constant(C2Function):
00541 "a constant and its derivatives"
00542 ClassName='Constant'
00543
00544
00545
00546 def __init__(self, val):
00547 C2Function.__init__(self)
00548 self.val=val
00549 self.name='%g' % val
00550
00551
00552
00553
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
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
00573 C2sin=_fC2sin()
00574
00575
00576
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
00585 C2cos=_fC2cos()
00586
00587
00588
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
00596 C2log=_fC2log()
00597
00598
00599
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
00608 C2exp=_fC2exp()
00609
00610
00611
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
00620 C2sqrt=_fC2sqrt()
00621
00622
00623
00624 class C2ScaledRecip(C2Function):
00625 "1/x"
00626 name='1/x'
00627
00628
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
00639 C2recip=C2ScaledRecip()
00640
00641
00642
00643 class _fC2identity(C2Function):
00644 name='Identity'
00645 def value_with_derivatives(self, x):
00646 return x, 1.0, 0.0
00647
00648
00649 C2identity=_fC2identity()
00650
00651
00652
00653 class C2Linear(C2Function):
00654 "slope*(x-x0) + y0"
00655
00656
00657
00658
00659
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
00669
00670
00671
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
00686 class C2Quadratic(C2Function):
00687 "a*(x-x0)**2 + b*(x-x0) + c"
00688
00689
00690
00691
00692
00693
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
00701
00702
00703
00704
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
00722 class C2PowerLaw(C2Function):
00723 "a*x**b where a and b are constant"
00724
00725
00726
00727
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
00737
00738
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
00752 class C2Polynomial(C2Function):
00753 "poly(x)"
00754
00755
00756
00757
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]
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
00779 class C2ComposedFunction(C2Function):
00780 "create a composed function outer(inner(...)). The functions can either be unbound class names or class instances"
00781
00782
00783
00784
00785
00786
00787 def __init__(self, outer, inner):
00788 if type(inner) is types.ClassType: inner=inner()
00789 if type(outer) is types.ClassType: outer=outer()
00790 C2Function.__init__(self, inner)
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
00799 class C2BinaryFunction(C2Function):
00800 "create a binary function using a helper function which computes the derivatives"
00801
00802
00803
00804
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
00813
00814
00815
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
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
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
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
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):
00862 ab2=_myfuncs.power(y0, (y1-2.0))
00863 ab1=ab2*y0
00864 ab=ab1*y0
00865 ab1 *= yp0
00866 ab1 *= y1
00867 ab2 *= y1*(y1-1)*yp0*yp0+y0*y1*ypp0
00868 return native(ab, ab1, ab2)
00869 else:
00870 loga=_myfuncs.log(y0)
00871 ab2=_myfuncs.exp(loga*(y1-2.0))
00872 ab1=ab2*y0
00873 ab=ab1*y0
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
00881
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
00889
00890
00891
00892
00893
00894
00895
00896
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
00913 u[1:-1]=dydx[1:]
00914 u[1:-1]-=dydx[:-1]
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
00959 u[1:-1]=dydx[1:]
00960 u[1:-1]-=dydx[:-1]
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
00987
00988
00989
00990
00991
00992
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
01025
01026
01027
01028
01029
01030
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)
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
01044
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
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
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
01086
01087
01088
01089
01090
01091
01092
01093
01094
01095
01096
01097
01098
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
01122
01123
01124
01125
01126
01127
01128
01129 def __init__(self, x, y, lowerSlope=None, upperSlope=None, XConversions=None, YConversions=None, cubic_spline=True):
01130 C2Function.__init__(self)
01131 self.SetDomain(min(x), max(x))
01132 self.Xraw=_numeric.array(x)
01133 self.xInverted=False
01134
01135 if YConversions is not None: self.YConversions=YConversions
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
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]:
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
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:
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
01185 yprime=gp*yp0*fpi
01186 fpp=self.fYinPP(y)
01187 gpp=self.fXinPP(x)
01188
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
01195
01196
01197
01198
01199
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
01215
01216
01217
01218
01219
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
01235
01236 def SetLowerExtrapolation(self, bound):
01237 if not self.xInverted:
01238 self.SetLeftExtrapolation(bound)
01239 else:
01240 self.SetRightExtrapolation(bound)
01241
01242
01243
01244
01245 def SetUpperExtrapolation(self, bound):
01246 if self.xInverted:
01247 self.SetLeftExtrapolation(bound)
01248 else:
01249 self.SetRightExtrapolation(bound)
01250
01251
01252
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)
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
01263
01264
01265
01266
01267 def UnaryOperator(self, C2source):
01268 "return new InterpolatingFunction C2source(self)"
01269 y=[C2source(self.fYout(yy)) for yy in self.F]
01270
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
01281
01282
01283
01284
01285
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]
01291 y=[yy[0] for yy in yv]
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
01299
01300
01301 def __add__(self, right):
01302 return self.BinaryOperator(right, C2Sum)
01303
01304
01305
01306
01307 def __sub__(self, right):
01308 return self.BinaryOperator(right, C2Diff)
01309
01310
01311
01312
01313
01314
01315 def __mul__(self, right):
01316 return self.BinaryOperator(right, C2Product)
01317
01318
01319
01320
01321
01322
01323 def __div__(self, right):
01324 return self.BinaryOperator(right, C2Ratio)
01325
01326
01327 LogConversions=_mylog, _recip, _mrecip2, _myexp
01328
01329
01330 class LogLinInterpolatingFunction(InterpolatingFunction):
01331 "An InterpolatingFunction which stores log(x) vs. y"
01332 ClassName='LogLinInterpolatingFunction'
01333 XConversions=LogConversions
01334
01335
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
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
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
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:
01378
01379 nz=_numeric.not_equal(bh, 0)
01380 if not inverse_function: nz[0]=nz[-1]=1
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]:
01387 be=be[::-1]
01388 cum=cum[::-1]
01389 cum*=-1
01390
01391 if normalize:
01392 cum *= (1.0/max(cum[0], cum[-1]))
01393
01394 if inverse_function:
01395 be, cum = cum, be
01396
01397 InterpolatingFunction.__init__(self, be, cum, **args)
01398 self.y2 *=0
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):
01419 np=len(binheights)
01420 be=_numeric.array(bincenters)
01421 bh=_numeric.array(binheights)
01422
01423 reversed = be[1] < be[0]
01424
01425 if reversed:
01426 be=be[::-1]
01427 bh=bh[::-1]
01428
01429 temp=self.IntermediateInterpolator(be, bh)
01430 else:
01431 temp=binheights
01432
01433
01434
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
01444
01445 InterpolatingFunction.__init__(self, integral, bincenters,
01446 lowerSlope=1.0/(scale*temp(bincenters[0])), upperSlope=1.0/(scale*temp(bincenters[-1]))
01447 )
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
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
01478 return y, 1.0/yp, -ypp/yp**3
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)
01494 y2, yp2, ypp2=f2.value_with_derivatives(x2)
01495
01496
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
01504
01505
01506
01507
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)
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
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
01541
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
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
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]:
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) ):
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
01586 x1=x+dx1
01587 x0=x1-dx
01588 x2=x1+dx
01589
01590 y0, yp0, ypp0=C2Ratio.value_with_derivatives(self, x0)
01591 y2, yp2, ypp2=C2Ratio.value_with_derivatives(self, x2)
01592
01593 yy0, yyp0, yypp0=self.left.value_with_derivatives(x1)
01594 yy1, yyp1, yypp1=self.right.value_with_derivatives(x1)
01595
01596 y1=yyp0/yyp1
01597
01598
01599 yp0*=dx
01600 yp2*=dx
01601 ypp0*=dx*dx
01602 ypp2*=dx*dx
01603
01604
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
01614
01615
01616 self.cache=x0,x1,x2, coefs
01617
01618 else:
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
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])
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))
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)
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))
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
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()
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