1  """Some utilities used by various CosmoloPy modules. 
  2  """ 
  3  import warnings 
  4  import math 
  5  import pickle 
  6   
  7  import numpy 
  8  import scipy 
  9  import scipy.integrate 
 10  import scipy.interpolate 
 11   
 12  import cosmolopy.distance as cd 
 13  import cosmolopy.constants as cc 
 14  from saveable import Saveable 
 17      """Set up uniform time array and corresponding redshift array. 
 18      """ 
 19 -    def __init__(self, z_min, z_max, dt_yr=2e6, **cosmo): 
  20          self.z_min = z_min 
 21          self.z_max = z_max 
 22          self.dt_yr = dt_yr 
 23          self.cosmo = cosmo 
 24           
 25          self.agefunc, self.redshiftfunc, e_f, e_t  = \ 
 26                        cd.quick_age_function(zmax=1.1 * z_max,  
 27                                              zmin=z_min-0.05,  
 28                                              zstep=0.01, 
 29                                              logspacing=True, 
 30                                              return_inverse=True, 
 31                                              **cosmo) 
 32          self.tmax = self.agefunc(z_min) 
 33          self.tmin = self.agefunc(z_max) 
 34          self.dt = self.dt_yr * cc.yr_s 
 35          self.t = numpy.arange(self.tmin, self.tmax + 1.01 * self.dt, self.dt) 
 36          self.t_yr = self.t / cc.yr_s 
 37          self.z = self.redshiftfunc(self.t) 
 38          print " Using %i points in t, dt = %.3g yr." % (len(self.t_yr), 
 39                                                          self.dt_yr) 
  40   
 42          return self.agefunc(z)/cc.yr_s/1e9 
  44          """Unpickle.""" 
 45          self.__dict__.update(dict) 
 46          self.agefunc, self.redshiftfunc, e_f, e_t  = \ 
 47                        cd.quick_age_function(zmax=1.1 * self.z_max,  
 48                                              zmin=self.z_min-0.05,  
 49                                              zstep=0.01, 
 50                                              logspacing=True, 
 51                                              return_inverse=True, 
 52                                              **self.cosmo) 
   53           
 56      """Interpolate/Extrapolate 1d data. 
 57      """ 
 58   
 61          if x is None: 
 62              x = self.x 
 63          if y is None: 
 64              y=self.y 
 65          if slope is None: 
 66              slope = (y[0]-y[-1]) / (x[0] - x[-1]) 
 67          intercept = y[match_index] - slope * x[match_index] 
 68          return slope, intercept 
  69   
 75          """ 
 76   
 77          Parameters 
 78          ---------- 
 79   
 80          x, y: 
 81   
 82            sequences of data. Will be sorted by x value before use. 
 83   
 84          bound_behavior: 
 85   
 86            length-2 sequence specifying behavior below the lower and 
 87            above the upper boungs of the data, respectively. Each 
 88            element can be 'extrapolate', 'constant', or a numerical 
 89            value. 
 90   
 91          npoints: 
 92   
 93            Linear extrapolation uses the slope between x[0] and 
 94            x[npoints-1] or x[-npoints] and x[-1]. Note: this is not a 
 95            linear fit over that range. It Ignores points within the 
 96            interval 
 97   
 98          interpargs: 
 99   
100            Extra keywords passed to scipy.interpolate.interp1d. 
101   
102          """ 
103          order = numpy.argsort(numpy.nan_to_num(x)) 
104          self.x = x[order] 
105          self.y = y[order] 
106   
107          self.bounds_behavior = bounds_behavior 
108           
109          self._interpfunc = scipy.interpolate.interp1d(self.x, self.y, 
110                                                        **interpargs) 
111   
112          if self.bounds_behavior[1] == 'constant': 
113              self._exfuncHigh = lambda x1: self.y[-1] 
114          elif self.bounds_behavior[1] == 'extrapolate': 
115              n1 = npoints[1] 
116              highSlope, highIntercept = self.linear_coefficients(self.x[-n1:], 
117                                                                  self.y[-n1:], 
118                                                                  slope=slopes[1], 
119                                                                  match_index=-1) 
120              self._exfuncHigh = lambda x1: x1 * highSlope + highIntercept 
121              self.highSlope = highSlope 
122              self.highIntercept = highIntercept 
123          else: 
124              self._exfuncHigh = lambda x1: self.bounds_behavior[1] 
125   
126          if self.bounds_behavior[0] == 'constant': 
127              self._exfuncLow = lambda x1: self.y[0] 
128          elif self.bounds_behavior[0] == 'extrapolate': 
129              n0 = npoints[0] 
130              lowSlope, lowIntercept = self.linear_coefficients(self.x[:n0], 
131                                                                self.y[:n0], 
132                                                                slope=slopes[0], 
133                                                                match_index=0) 
134              self._exfuncLow = lambda x1: x1 * lowSlope + lowIntercept 
135              self.lowSlope = lowSlope 
136              self.lowIntercept = lowIntercept 
137          else: 
138              self._exfuncLow = lambda x1: self.bounds_behavior[0] 
 139   
141          extstr = "" 
142          if hasattr(self, 'lowSlope'): 
143              extstr += "y = %g x + %g for x <= %g" % (self.lowSlope, 
144                                                       self.lowIntercept, 
145                                                       self.x[0]) 
146          if hasattr(self, 'highSlope'): 
147              if hasattr(self, 'lowSlope'): 
148                  extstr += "\n" 
149              extstr += "y = %g x + %g for x >= %g" % (self.highSlope, 
150                                                       self.highIntercept, 
151                                                       self.x[-1]) 
152          return extstr 
 153   
155          if numpy.isscalar(x1) or x1.shape==(): 
156              if x1 <= self.x[0]: 
157                  return self._exfuncLow(x1) 
158              elif x1 >= self.x[-1]: 
159                  return self._exfuncHigh(x1) 
160              else: 
161                  return self._interpfunc(x1) 
162          lowmask = x1 <= self.x[0] 
163          highmask = x1 >= self.x[-1] 
164          inmask = numpy.logical_not(numpy.logical_or(lowmask,highmask)) 
165          if numpy.all(inmask): 
166              return self._interpfunc(x1) 
167           
168          y1 = numpy.empty(x1.shape) 
169          y1[inmask] = self._interpfunc(x1[inmask]) 
170          y1[lowmask] = self._exfuncLow(x1[lowmask]) 
171          y1[highmask] = self._exfuncHigh(x1[highmask]) 
172   
173          return y1 
  174   
176      """A piecewise powerlaw function. 
177   
178      You can specify the intervals and power indices, and this class 
179      will figure out the coefficients needed to make the function 
180      continuous and normalized to unit integral. 
181   
182      Notes 
183      ----- 
184   
185      Intervals are defined by an array l 
186   
187      Powerlaw indicies by and array p 
188   
189      a_n are the coefficients. 
190       
191      f(x) = a_n x^{p_n} for l_{n-1} <= x < l_n 
192   
193      Recursion relation for continuity: 
194   
195      a_n = a_{n-1} l_n^{p_{n-1} - p_n} 
196   
197      Integral of a piece: 
198   
199      I_n = a_n p_n (l_{n+1}^{p_n - 1} - l_n^{p_n - 1}) 
200   
201      Total integral: 
202   
203      I_tot = Sum_0^N I_n 
204   
205      """ 
206   
207 -    def __init__(self, limits, powers, 
208                   coefficients=None, 
209                   externalval=0.0, 
210                   norm=True): 
 211          """Defined a piecewise powerlaw. 
212   
213          If coefficients is None then the coefficients are determined 
214          by requiring the function to be continuous and normalized to 
215          an integral of one. 
216   
217          The function is composed of N powerlaws, where N = len(powers). 
218   
219          len(limits) must be one greated than len(powers) 
220   
221          Parameters 
222          ---------- 
223   
224          limits: array (length n+1) 
225              boundaries of the specified powerlaws. Must be one greater in 
226              length than coefficents and powers. Specify -numpy.infty for 
227              the first limit or numpy.infty for the last limit for 
228              unbounded powerlaws. 
229   
230          coefficients: optional array (length n) 
231              values of the coefficient a_i 
232   
233          powers: array (length n) 
234              values of the powerlaw indices p_i 
235   
236          externalval: scalar 
237              Value to return outside the defined domain. None 
238              correspons to 'NaN'. 
239   
240          norm: boolean 
241              Whether to normalize the integral of the function over the 
242              defined domain to unity. 
243   
244          The resulting function takes a single, one-dimensional array of 
245          values on which to operate. 
246   
247          """ 
248   
249          limits = numpy.atleast_1d(limits) 
250          powers = numpy.atleast_1d(powers) 
251   
252          if not len(limits) == len(powers)+1: 
253              raise ValueError("limits must be one longer than powers.") 
254   
255          if coefficients is None: 
256              coefficients = numpy.ones(len(powers)) 
257   
258               
259              for n in range(1,len(powers)): 
260                  coefficients[n] = (coefficients[n-1] * 
261                                     limits[n]**(powers[n-1] - powers[n])) 
262          else: 
263              coefficients = numpy.atleast_1d(coefficients) 
264              if not len(coefficients) == len(powers): 
265                  raise ValueError("coefficients and powers must be"+ 
266                                   " the same length.") 
267   
268           
269          integrals = ((coefficients / (powers + 1.)) * 
270                       (limits[1:]**(powers + 1.) - 
271                        limits[:-1]**(powers + 1.))) 
272          if norm: 
273               
274              integralTot = numpy.sum(integrals) 
275               
276              coefficients = coefficients / integralTot 
277              integrals = integrals /  integralTot 
278   
279          for array in [limits, coefficients, powers]: 
280              if array.ndim > 1: 
281                  raise ValueError("arguments must be a 1D arrays or scalars.") 
282          self._integrals = integrals 
283          self._limits = limits.reshape((-1,1)) 
284          self._coefficients = coefficients.reshape((-1,1)) 
285          self._powers = powers.reshape((-1,1)) 
286          self._externalval = externalval 
 287       
289          """Evaluate the powerlaw at values x. 
290          """ 
291          x = numpy.atleast_1d(x) 
292          if x.ndim > 1: 
293              raise ValueError("argument must be a 1D array or scalar.") 
294          y = numpy.sum((self._coefficients * x**self._powers) * 
295                        (x >= self._limits[0:-1]) * (x < self._limits[1:]), 
296                        axis=0) 
297          y[x < self._limits[0]] = self._externalval 
298          y[x >= self._limits[-1]] = self._externalval 
299          return y 
 300   
301 -    def integrate(self, low, high, weight_power=None): 
 302          """Integrate the function from low to high. 
303   
304          Optionally weight the integral by x^weight_power. 
305   
306          """ 
307          limits = self._limits.flatten() 
308          coefficients = self._coefficients.flatten() 
309          powers = self._powers.flatten() 
310          if weight_power is not None: 
311              powers += weight_power 
312               
313              integrals = ((coefficients / (powers + 1.)) * 
314                           (limits[1:]**(powers + 1.) - 
315                            limits[:-1]**(powers + 1.))) 
316          else: 
317              integrals = self._integrals 
318           
319          pairs = numpy.broadcast(low, high) 
320          integral = numpy.empty(pairs.shape) 
321          for (i, (x0, x1)) in enumerate(pairs): 
322               
323              x0, x1 = list(numpy.sort([x0,x1])) 
324   
325               
326              mask = numpy.logical_and(x0 < limits[:-1], 
327                                       x1 >= limits[1:]).flatten() 
328              indices = numpy.where(mask) 
329              if not numpy.any(mask): 
330                  integral.flat[i] = 0 
331   
332                   
333                  if x0 > limits[-1] or x1 < limits[0]: 
334                      integral.flat[i] = 0 
335                      continue 
336   
337                   
338                  containedmask = numpy.logical_and(x0 >= limits[:-1], 
339                                                    x1 < limits[1:]) 
340                   
341                  if numpy.any(containedmask): 
342                       
343                      index = numpy.where(containedmask)[0][0] 
344                      integral.flat[i] = ((coefficients[index] / 
345                                           (powers[index] + 1.)) * 
346                                          (x1**(powers[index] + 1.) - 
347                                           x0**(powers[index] + 1.))) 
348                      continue 
349                  elif x1 >= limits[0] and x1 < limits[1]: 
350                       
351                      highi = 0 
352                      lowi = -1 
353                  elif x0 < limits[-1] and x0 >= limits[-2]: 
354                       
355                      lowi = len(limits) - 2 
356                      highi = len(limits) 
357                  else: 
358                       
359                      lowi = numpy.max(numpy.where(x0 >= limits[:-1])) 
360                      highi = numpy.min(numpy.where(x1 < limits[1:])) 
361                  insideintegral = 0 
362              else: 
363                   
364                  insideintegral = numpy.sum(integrals[indices]) 
365   
366                  lowi = numpy.min(indices) - 1 
367                  highi = numpy.max(indices) + 1 
368   
369               
370              if x0 < limits[0] or lowi < 0: 
371                  lowintegral = 0. 
372              else: 
373                  lowintegral = ((coefficients[lowi] / (powers[lowi] + 1.)) * 
374                                 (limits[lowi + 1]**(powers[lowi] + 1.) - 
375                                  x0**(powers[lowi] + 1.))) 
376              if x1 > limits[-1] or highi > len(coefficients) - 1: 
377                  highintegral = 0. 
378              else: 
379                  highintegral = ((coefficients[highi] / (powers[highi] + 1.)) * 
380                                  (x1**(powers[highi] + 1.) - 
381                                   limits[highi]**(powers[highi] + 1.))) 
382              integral.flat[i] = highintegral + insideintegral + lowintegral 
383          return integral 
  384   
385 -def ccumulate(function, x, max=None, **kwargs): 
 386      """Integrate a function from x to max, where x can be an array. 
387   
388      Parameters 
389      ---------- 
390   
391      function: callable 
392   
393      x: array-like 
394   
395      max: float 
396          defaults to max(x) 
397   
398      Notes 
399      ----- 
400   
401      This can be used to find the complementary cumulative distribution 
402      function (CCDF) given the probability distribution function (PDF). 
403   
404      Unlike integrate_piecewise, the x values don't have to be in 
405      order, though a warning will be issued if any are greater than 
406      max, if max is specified. 
407      """ 
408   
409      x = numpy.atleast_1d(x) 
410       
411      order = numpy.argsort(numpy.nan_to_num(x)) 
412      bad_mask = numpy.isnan(x[order]) 
413      bad_order = order[bad_mask] 
414      good_order = order[numpy.logical_not(bad_mask)] 
415      x0 = x[good_order] 
416   
417      if len(x0) == 1: 
418          integral = [0] 
419      else: 
420          integral = integrate_piecewise(function, x0, **kwargs) 
421   
422       
423      postintegral = 0. 
424      if max is not None: 
425          sign = 0. 
426          if max >= x0[-1]: 
427              sign = 1. 
428              x1 = [x0[-1], max] 
429          elif(max < x0[-1]): 
430              warnings.warn("max %s is less than maximum x %s" % (max, x0[-1])) 
431              sign = -1. 
432              x1 = [max, x0[-1]] 
433          postintegral += sign * integrate_piecewise(function, x1, **kwargs)[-1] 
434   
435       
436      cintegral = postintegral + integral[-1] - integral 
437   
438       
439      ordintegral = numpy.empty(len(x)) 
440      ordintegral[good_order] = cintegral 
441      ordintegral[bad_order] = numpy.nan 
442      return ordintegral 
 443   
446      """Integrate function and return the integral at a sequence of points. 
447   
448      Useful when you want to efficiently calculate a cumulative integral. 
449   
450      Also useful for piecewise-defined functions where discontinuities 
451      or critical points cause quadrature routines to complain or become 
452      inaccurate. 
453   
454      Integration methods available are: quad, romberg.  
455   
456      Parameters 
457      ---------- 
458      function : callable 
459          User defined function. Should take a single vector argument 
460          and return q vector of the same shape. 
461   
462      x : array_like 
463          Array of points at which to evaluate the integral.  
464   
465      method : str, optional 
466          Name of the method to use to integrate each segment. 'quad' or 
467          'romberg'. 
468   
469      return_pieces : bool, optional 
470          Return the individual segments rather than the sum of all 
471          preceding segments. 
472      
473      Returns 
474      ------- 
475      integral : ndarray 
476          The value of the integral at each x. The first value is always zero. 
477      """ 
478       
479      x = numpy.asarray(x) 
480      if numpy.any(x[1:] - x[:-1] < 0): 
481          raise ValueError, "Array x must increase monotonically." 
482      if numpy.any(numpy.isnan(x)): 
483          raise ValueError, "Array x must not include NaN values."  
484      integral_list = [0.0] 
485      if method is None: 
486          method = 'quad' 
487      if method=='quad': 
488          args = {'limit':200} 
489          args.update(kwargs) 
490          for i in xrange(1, len(x)): 
491              a, b = x[i-1], x[i] 
492              integral, error = scipy.integrate.quad(function, a, b, 
493                                                     **args) 
494              integral_list.append(integral) 
495      elif method=='romberg': 
496          args = {'divmax':100, 'vec_func':True} 
497          args.update(kwargs) 
498          for i in xrange(1, len(x)): 
499              a, b = x[i-1], x[i] 
500              integral = scipy.integrate.romberg(function, a, b, 
501                                                 **args) 
502              integral_list.append(integral) 
503      else: 
504          raise ValueError, "Method '%s' unknown." % method 
505   
506      integrals = numpy.asarray(integral_list) 
507      if return_pieces: 
508          return integrals 
509   
510      integral = numpy.cumsum(numpy.nan_to_num(integrals)) 
511      return integral 
 512   
513  @numpy.vectorize 
514 -def _vecquad(function, low, high, **kwargs): 
 515      integral, error = scipy.integrate.quad(function,  
516                                             low, 
517                                             high, 
518                                             **kwargs) 
519      return integral, error 
 520   
521 -def vecquad(function, low, high, **kwargs): 
 522      """Integrate a function from low to high (vectorized). 
523       
524      Vectorized convenience function. 
525       
526      """ 
527      return _vecquad(function,low,high, **kwargs) 
 528   
529  @numpy.vectorize 
530 -def _logquad(function, low, high, **kwargs): 
 531       
532      def func_dlnx (lnx): 
533          x = numpy.exp(lnx) 
534          return function(x) * x 
 535      integral, error = scipy.integrate.quad(func_dlnx,  
536                                             math.log(low), 
537                                             math.log(high), 
538                                             **kwargs) 
539      return integral, error 
540   
541 -def logquad(function, low, high, **kwargs): 
 542      """Integrate a function from low to high using a log transform (vectorized). 
543   
544      The log transform is applied to the variable over which the 
545      integration is being performed. 
546       
547      """ 
548      return _logquad(function, low, high, **kwargs) 
 549   
551      """A decorator that normalizes a function. 
552   
553      Only works for functions of a single variable. 
554   
555      The new function is normalized over the interval from min to max, 
556      i.e. the integral of the new function from low to high is one. 
557      """ 
558   
559 -    def __init__(self, min, max, quiet=False, **kwargs): 
 560              self.min = min 
561              self.max = max 
562              self.quiet = quiet 
563              self.kwargs = kwargs 
 564   
566          integral = logquad(function, self.min, self.max, **self.kwargs)[0] 
567          newfunction = lambda x: function(x)/integral 
568   
569          if not self.quiet: 
570              print "Normalization factor for %s is %.3g" % (function.__name__, 
571                                                             1./integral) 
572           
573           
574   
575          newfunction.__name__ = function.__name__ 
576          newfunction.__dict__.update(function.__dict__) 
577          newfunction.__doc__ = function.__doc__ 
578          newfunction.min = self.min 
579          newfunction.max = self.max 
580          return newfunction 
  581