Piecewise linear continuous functions.

Gribouillis Gribouillis is offline Offline Oct 19th, 2008, 10:11 am |
0
Joining together ordered sample points (xi, yi) when ths xi's are different yields a piecewise linear continuous (P1) function. This snippet defines a handy class to manipulate these functions. It allows
computing the value and the slope of the function at each point, arithmetic operations, absolute value, truncation and linear combinations of such functions, maximum, minimum and integral between bounds. If wxPython is installed on your system, the class also allows plotting the function in a wx plot widget.
Quick reply to this message  
Python Syntax
  1. #!/usr/bin/env python
  2.  
  3. # Gribouillis at www.daniweb.com
  4.  
  5. # module P1Function.py
  6. # type "help(P1Function)" in a python shell for class documentation
  7. """A module implementing piecewise linear continuous functions.
  8. """
  9. from bisect import bisect_left ,bisect_right
  10. from collections import defaultdict
  11.  
  12. class P1Function (object ):
  13. """P1Function(seq) -> new piecewise linear continuous function object
  14. 'seq' is an iterable over pairs of numbers (x, y).
  15. Raises ValueError if one of the numbers is not convertible to float
  16. or when 2 points have the same x but not the same y.
  17. """
  18. _bisect =(bisect_left ,bisect_right )
  19. _eps =1.0e-9
  20. def __init__ (self ,seq ):
  21. pairs =sorted (set (seq ))
  22. pairs =[(float (x ),float (y ))for x ,y in pairs ]
  23. if len (pairs )<2 :
  24. if pairs :
  25. pairs .append ((1.0 +2 *abs (pairs [0 ][0 ]),pairs [0 ][1 ]))
  26. else :
  27. pairs =[(x ,0.0 )for x in self .float_range (0.0 ,1.0 ,2 )]
  28. tmp =[pairs [0 ]]
  29. for i in xrange (1 ,len (pairs )):
  30. x ,y =tmp [-1 ]
  31. if pairs [i ][0 ]==x :
  32. if pairs [i ][1 ]!=y :
  33. raise ValueError ("Sample points aligned vertically")
  34. else :
  35. tmp .append (pairs [i ])
  36. pairs =[tmp [0 ]]
  37. for i in xrange (1 ,len (tmp )-1 ):
  38. a ,c ,b =pairs [-1 ][0 ],tmp [i ][0 ],tmp [i +1 ][0 ]
  39. ya ,yc ,yb =pairs [-1 ][1 ],tmp [i ][1 ],tmp [i +1 ][1 ]
  40. ycc =ya *((c -a )/(b -a ))+yb *((b -c )/(b -a ))
  41. if abs (yc -ycc )>=self ._eps *max (abs (yc ),abs (ycc )):
  42. pairs .append (tmp [i ])
  43. pairs .append (tmp [-1 ])
  44. self ._x ,self ._y =zip (*pairs )
  45. self ._x =[float (t )for t in self ._x ]
  46. def __iter__ (self ):
  47. """iter(func) -> iterator over pairs of numbers (x, y).
  48. This allows copying by passing a P1Function to the class constructor
  49. """
  50. return iter (zip (self ._x ,self ._y ))
  51. def __len__ (self ):
  52. """len(func) -> number of pairs (x, y) contained in the P1Function.
  53. This number may be different from the number of pairs passed to the
  54. constructor.
  55. """
  56. return len (self ._x )
  57. def pair (self ,i ):
  58. "func.pair(i) -> a sample point (xi, yi) (0 <= i < len(func))"
  59. return self ._x [i ],self ._y [i ]
  60. def x (self ,i ):
  61. "func.x(i) -> the coordinate xi of the sample point at index i"
  62. return self ._x [i ]
  63. def y (self ,i ):
  64. "func.y(i) -> the coordinate yi of the sample point at index i"
  65. return self ._y [i ]
  66. def index (self ,x ,left =False ):
  67. """func.index(x) -> an index i such that
  68. i = 0 if x < func.x(1)
  69. i = n-2 if func.x(n-2) <= x
  70. func.x(i) <= x < func.x(i+1) otherwise.
  71. Here, x is a float and n = len(func) is the number of pairs in func.
  72. When left is True, <= and < are exchanged in these definitions.
  73. """
  74.  
  75.  
  76.  
  77. return min (len (self ._x )-2 ,
  78. max (0 ,self ._bisect [not left ](self ._x ,x )-1 ))
  79. def index_bounds (self ,a =None ,b =None ):
  80. """self->index_bounds(a, b) -> pair of indexes (i, j)
  81. such that a <= self.x(k) <= b for all k in [i, j[.
  82. Returns a pair (i, i) when there is no such k.
  83. a and b default to None, meaning -infinity and +infinity"""
  84. a =a if a is not None else self ._x [0 ]
  85. b =b if b is not None else self ._x [-1 ]
  86. i =self ._bisect [0 ](self ._x ,a )# bisect left
  87. j =self ._bisect [1 ](self ._x ,b )# bisect_right
  88. return (i ,j )if i <j else (i ,i )
  89. def __call__ (self ,x ):
  90. """func(x) -> the value of the piecewise linear func at point x.
  91. x can be any float number, the function is extended by straight lines
  92. outside it's initial x-bounds"""
  93. i =self .index (x )
  94. return ((self ._x [i +1 ]-x )*self ._y [i ]+
  95. (x -self ._x [i ])*self ._y [i +1 ])/(self ._x [i +1 ]-self ._x [i ])
  96. @staticmethod
  97. def float_range (start ,stop ,n ):
  98. """P1Function.float_range(start, stop, n) -> list of n float
  99. A range function which returns n equally spaced floating numbers
  100. from start to stop"""
  101. L =[0.0 ]*n
  102. nm1 =n -1
  103. nm1inv =1.0 /nm1
  104. start ,stop =start *nm1inv ,stop *nm1inv
  105. for i in range (n ):
  106. L [i ]=(start *(nm1 -i )+stop *i )
  107. return L
  108. def slope (self ,x ,left =False ):
  109. """func.slope(x) -> the slope of the function at x
  110. For sample points, this is the slope at the right of the point, unless
  111. left is True.
  112. """
  113. i =self .index (x ,left )
  114. return (self ._y [i +1 ]-self ._y [i ])/(self ._x [i +1 ]-self ._x [i ])
  115. def max (self ,a =None ,b =None ):
  116. """func.max(a,b) -> the maximum value of func between a and b
  117. a and b default to None, meaning -infinity and +infinity. In this case,
  118. the maximum can be None, meaning +infinity."""
  119. return self ._extreme (a ,b ,1 )
  120. def min (self ,a =None ,b =None ):
  121. """func.min(a,b) -> the minimum value of func between a and b
  122. a and b default to None, meaning -infinity and +infinity. In this case,
  123. the minimum can be None, meaning -infinity."""
  124. return self ._extreme (a ,b ,-1 )
  125. def _extreme (self ,a ,b ,eps ):
  126. """func._extreme(a, b, eps) -> the max or min of func between a and b
  127. depending on eps being 1 or -1"""
  128. opt =(min ,None ,max )[1 +eps ]
  129. if a is None :
  130. if (self ._y [1 ]-self ._y [0 ])*eps <0 :
  131. return None
  132. a =self ._x [0 ]
  133. if b is None :
  134. if (self ._y [-1 ]-self ._y [-2 ])*eps >0 :
  135. return None
  136. b =self ._x [-1 ]
  137. if a >b :
  138. a ,b =b ,a
  139. i ,j =self .index_bounds (a ,b )
  140. m =opt (self (a ),self (b ))
  141. return opt (m ,opt (self ._y [i :j ])if i <j else m )
  142. def integral (self ,a =None ,b =None ):
  143. """func.integral(a, b) -> the value of the integral of func on [a,b].
  144. a and b default to None, meaning -infinity and +infinity.
  145. May return None when a or b is None, meaning undefined integral."""
  146. if a is None :
  147. if self ._y [0 ]!=0.0 or self ._y [1 ]!=0.0 :
  148. return None
  149. a =self ._x [1 ]
  150. if b is None :
  151. if self ._y [-1 ]!=0.0 or self ._y [-2 ]!=0.0 :
  152. return None
  153. b =self ._x [-2 ]
  154. swap =False
  155. if a >b :
  156. if a ==b :
  157. return 0.0
  158. else :
  159. swap =True
  160. a ,b =b ,a
  161. i ,j =self .index_bounds (a ,b )
  162. j -=1
  163. if i >j :
  164. return ((a -b )if swap else (b -a ))*self (0.5 *(b +a ))
  165. x ,y =self ._x ,self ._y
  166. res =(x [i ]-a )*self (0.5 *(x [i ]+a ))+(b -x [j ])*self (0.5 *(b +x [j ]))
  167. res +=sum (
  168. (x [k +1 ]-x [k ])*(y [k +1 ]+y [k ])*0.5 for k in xrange (i ,j ))
  169. return -res if swap else res
  170. @staticmethod
  171. def combine (pairs ):
  172. """P1Function.combine(pairs) -> a linear combination of P1Functions
  173. pairs is a sequence (ai, fi) where ai is a number and fi a P1Function.
  174. the sum of the ai * fi is returned as a P1Function"""
  175. d =defaultdict (float )
  176. for c ,f in pairs :
  177. d [f ]+=c
  178. for f ,c in d .items ():
  179. if c ==0.0 :
  180. del d [f ]
  181. xs =sorted (set (t for f in d for t in f ._x ))
  182. data =[(x ,sum (d [f ]*f (x )for f in d ))for x in xs ]
  183. return P1Function (data )
  184. def rescaled (self ,center =(0.0 ,0.0 ),zoom =(1.0 ,1.0 )):
  185. """func.rescaled(center, zoom) -> a rescaled P1Function.
  186. center is a pair (cx, cy), zoom is a pair (zx, zy).
  187. A P1Function g is returned, such that
  188. g(zx * (x - cx)) = zy * (func(x) - cy)
  189. This allows changing units in x or y (eg celsius to farenheit)"""
  190.  
  191. cx ,cy =center
  192. zx ,zy =zoom
  193. x ,y =self ._x ,self ._y
  194. return P1Function (
  195. (zx *(x [i ]-cx ),zy *(y [i ]-cy ))for i in xrange (len (x )))
  196. def truncated (self ,a =None ,b =None ):
  197. """func.truncated(a, b) -> a new P1Function truncated to [a,b].
  198. Truncation means forget the points (xi, yi) when xi is not in [a,b].
  199. a and b default to None, meaning -infinity and +infinity"""
  200. L =[(self ._x [i ],self ._y [i ])for i in self .index_bounds (a ,b )]
  201. L .extend ((x ,self (x ))for x in (a ,b )if x is not None )
  202. return P1Function (L )
  203. def __abs__ (self ):
  204. """abs(func) -> the absolute value of func (as a P1Function)"""
  205. L =[]
  206. x ,y =self ._x ,self ._y
  207. if (y [0 ]>0.0 and y [1 ]>y [0 ])or (
  208. y [0 ]<0.0 and y [1 ]<y [0 ]):
  209. u =y [0 ]*(x [0 ]-x [1 ])/(y [0 ]-y [1 ])
  210. L .append ((x [0 ]-1.5 *u ,0.5 *abs (y [0 ])))
  211. L .append ((x [0 ]-u ,0.0 ))
  212. for i in range (len (x )-1 ):
  213. L .append ((x [i ],abs (y [i ])))
  214. if (y [i ]<0.0 and y [i +1 ]>0.0 )or (
  215. y [i ]>0.0 and y [i +1 ]<0.0 ):
  216. u =y [i ]*(x [i ]-x [i +1 ])/(y [i ]-y [i +1 ])
  217. L .append ((x [i ]-u ,0.0 ))
  218. L .append ((x [-1 ],abs (y [-1 ])))
  219. if (y [-1 ]>0 and y [-2 ]>y [-1 ])or (
  220. y [-1 ]<0 and y [-2 ]<y [-1 ]):
  221. u =y [-1 ]*(x [-2 ]-x [-1 ])/(y [-1 ]-y [-2 ])
  222. L .append ((x [-1 ]-u ,0.0 ))
  223. L .append ((x [-1 ]-1.5 *u ,0.5 *abs (y [-1 ])))
  224. return P1Function (L )
  225. def __add__ (self ,other ):
  226. """f1 + f2 -> the sum of 2 P1Functions as a P1Function"""
  227. return self .combine ([(1.0 ,self ),(1.0 ,other )])
  228. def __sub__ (self ,other ):
  229. """f1 - f2 -> the difference of 2 P1Functions as a P1Function"""
  230. return self .combine ([(1.0 ,self ),(-1.0 ,other )])
  231. def __neg__ (self ):
  232. """-func -> the opposite of func as a P1Function"""
  233. return self .combine ([(-1.0 ,self )])
  234. def __mul__ (self ,factor ):
  235. """a * func -> a P1Function, func muliplied by the number a"""
  236. return self .combine ([float (factor ),self ])
  237. def plot (self ,**kwd ):
  238. """func.plot(**options) -> subprocess.Popen object
  239. Starts a new process which plots func using wxPython.
  240. options (with default values) are:
  241. title="P1Function", size=(400,300), line_colour='blue',
  242. line_width=1, marker='plus', caption='',
  243. x_axis='x axis', y_axis='y axis'
  244. Possible marker values:
  245. 'circle' 'cross' 'square' 'dot' 'plus' 'triangle'"""
  246. import subprocess
  247. options =dict (self ._plot_defaults )
  248. options .update (dict ((k .upper (),v )for k ,v in kwd .items ()))
  249. options ["POINTS"]=list (self )
  250. program =self ._plot_template %options
  251. child =subprocess .Popen ('python -c "exec(eval(raw_input()))"',
  252. shell =True ,stdin =subprocess .PIPE )
  253. child .stdin .write (repr (program ))
  254. child .stdin .write ("\n")
  255. child .stdin .close ()
  256. return child
  257.  
  258. _plot_defaults =dict (
  259. TITLE ="P1 Function",
  260. SIZE =(400 ,300 ),
  261. LINE_COLOUR ='blue',
  262. LINE_WIDTH =1 ,
  263. MARKER ='plus',
  264. CAPTION ='',
  265. X_AXIS ='x axis',
  266. Y_AXIS ='y axis'
  267. )
  268.  
  269. _plot_template ="""
  270. # thanks to vegaseat at www.daniweb.com
  271. import wx
  272. import wx.lib.plot as plot
  273. data = %(POINTS)s
  274.  
  275. class MyFrame(object):
  276. def __init__(self):
  277. self.frame = wx.Frame( None,
  278. title='%(TITLE)s', id=-1, size=%(SIZE)s)
  279. self.panel = wx.Panel(self.frame)
  280. plotter = plot.PlotCanvas(self.panel)
  281. plotter.SetSize(%(SIZE)s)
  282. plotter.SetEnableZoom(True)
  283. line = plot.PolyLine(data,
  284. colour='%(LINE_COLOUR)s', width=%(LINE_WIDTH)s)
  285. marker = plot.PolyMarker(data, marker='%(MARKER)s')
  286. gc = plot.PlotGraphics([line, marker],
  287. '%(CAPTION)s', '%(X_AXIS)s', '%(Y_AXIS)s')
  288. plotter.Draw(gc)
  289. self.frame.Show(True)
  290. app = wx.PySimpleApp()
  291. f = MyFrame()
  292. app.MainLoop()
  293. """

Message:


Similar Threads
Thread Tools Search this Thread



About Us | Contact Us | Advertise | DaniWeb | Acceptable Use Policy | RSS Feed

©2003 - 2009 DaniWeb® LLC