WLC and FJC output are now with 2 decimal precision. Added a comment on autopeak...
[hooke.git] / libhooke.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 '''
5 libhooke.py
6
7 General library of internal objects and utilities for Hooke.
8
9 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
10 With algorithms contributed by Francesco Musiani (University of Bologna, Italy)
11
12 This program is released under the GNU General Public License version 2.
13 '''
14
15
16
17 import libhookecurve as lhc
18
19 import scipy
20 import scipy.signal
21 import scipy.optimize
22 import scipy.stats
23 import numpy
24 import xml.dom.minidom
25 import os
26 import string
27 import csv
28 from matplotlib.ticker import ScalarFormatter
29
30
31 HOOKE_VERSION=['0.8.3_devel', 'Seinei', '2008-04-16']
32 WX_GOOD=['2.6','2.8'] 
33     
34 class PlaylistXML:
35         '''
36         This module allows for import/export of an XML playlist into/out of a list of HookeCurve objects
37         '''
38         
39         def __init__(self):
40             
41             self.playlist=None #the DOM object representing the playlist data structure
42             self.playpath=None #the path of the playlist XML file
43             self.plaything=None
44             self.hidden_attributes=['curve'] #This list contains hidden attributes that we don't want to go into the playlist.
45         
46         def export(self, list_of_hooke_curves, generics):
47             '''
48             Creates an initial playlist from a list of files.
49             A playlist is an XML document with the following syntaxis:
50             <playlist>
51             <element path="/my/file/path/"/ attribute="attribute">
52             <element path="...">
53             </playlist>
54             '''   
55         
56             #create the output playlist, a simple XML document
57             impl=xml.dom.minidom.getDOMImplementation()
58             #create the document DOM object and the root element
59             newdoc=impl.createDocument(None, "playlist",None)
60             top_element=newdoc.documentElement
61             
62             #save generics variables
63             playlist_generics=newdoc.createElement("generics")
64             top_element.appendChild(playlist_generics)
65             for key in generics.keys():
66                 newdoc.createAttribute(key)
67                 playlist_generics.setAttribute(key,str(generics[key]))
68             
69             #save curves and their attributes
70             for item in list_of_hooke_curves:
71                 #playlist_element=newdoc.createElement("curve")
72                 playlist_element=newdoc.createElement("element")
73                 top_element.appendChild(playlist_element)
74                 for key in item.__dict__:
75                     if not (key in self.hidden_attributes):
76                         newdoc.createAttribute(key)
77                         playlist_element.setAttribute(key,str(item.__dict__[key]))    
78             
79             self.playlist=newdoc
80             
81         def load(self,filename):
82             '''
83             loads a playlist file
84             '''
85             myplay=file(filename)
86             self.playpath=filename
87             
88             #the following 3 lines are needed to strip newlines. otherwise, since newlines
89             #are XML elements too (why?), the parser would read them (and re-save them, multiplying
90             #newlines...)
91             #yes, I'm an XML n00b
92             the_file=myplay.read()
93             the_file_lines=the_file.split('\n')
94             the_file=''.join(the_file_lines)
95                        
96             self.playlist=xml.dom.minidom.parseString(the_file)  
97                            
98             #inner parsing functions
99             def handlePlaylist(playlist):
100                 list_of_files=playlist.getElementsByTagName("element")
101                 generics=playlist.getElementsByTagName("generics")
102                 return handleFiles(list_of_files), handleGenerics(generics)
103             
104             def handleGenerics(generics):
105                 generics_dict={}
106                 if len(generics)==0:
107                     return generics_dict
108                 
109                 for attribute in generics[0].attributes.keys():
110                     generics_dict[attribute]=generics[0].getAttribute(attribute)
111                 return generics_dict
112         
113             def handleFiles(list_of_files):
114                 new_playlist=[]
115                 for myfile in list_of_files:
116                     #rebuild a data structure from the xml attributes
117                     the_curve=lhc.HookeCurve(myfile.getAttribute('path'))
118                     for attribute in myfile.attributes.keys(): #extract attributes for the single curve
119                         the_curve.__dict__[attribute]=myfile.getAttribute(attribute)
120                     new_playlist.append(the_curve)
121                 
122                 return new_playlist #this is the true thing returned at the end of this function...(FIXME: clarity)
123                     
124             return handlePlaylist(self.playlist)
125             
126
127         def save(self,output_filename):
128             '''
129             saves the playlist in a XML file.
130             '''
131             try:
132                 outfile=file(output_filename,'w')
133             except IOError:
134                 print 'libhooke.py : Cannot save playlist. Wrong path or filename'
135                 return
136             
137             self.playlist.writexml(outfile,indent='\n')
138             outfile.close()
139
140
141 class HookeConfig:
142     '''
143     Handling of Hooke configuration file
144     
145     Mostly based on the simple-yet-useful examples of the Python Library Reference
146     about xml.dom.minidom
147     
148     FIXME: starting to look a mess, should require refactoring
149     '''
150     
151     def __init__(self):
152         self.config={}
153         self.config['plugins']=[]
154         self.config['drivers']=[]
155         self.config['plotmanips']=[]
156                 
157     def load_config(self, filename):
158         myconfig=file(filename)
159                     
160         #the following 3 lines are needed to strip newlines. otherwise, since newlines
161         #are XML elements too, the parser would read them (and re-save them, multiplying
162         #newlines...)
163         #yes, I'm an XML n00b
164         the_file=myconfig.read()
165         the_file_lines=the_file.split('\n')
166         the_file=''.join(the_file_lines)
167                        
168         self.config_tree=xml.dom.minidom.parseString(the_file)  
169         
170         def getText(nodelist):
171             #take the text from a nodelist
172             #from Python Library Reference 13.7.2
173             rc = ''
174             for node in nodelist:
175                 if node.nodeType == node.TEXT_NODE:
176                     rc += node.data
177             return rc
178         
179         def handleConfig(config):
180             display_elements=config.getElementsByTagName("display")
181             plugins_elements=config.getElementsByTagName("plugins")
182             drivers_elements=config.getElementsByTagName("drivers")
183             workdir_elements=config.getElementsByTagName("workdir")
184             defaultlist_elements=config.getElementsByTagName("defaultlist")
185             plotmanip_elements=config.getElementsByTagName("plotmanips")
186             handleDisplay(display_elements)
187             handlePlugins(plugins_elements)
188             handleDrivers(drivers_elements)
189             handleWorkdir(workdir_elements)
190             handleDefaultlist(defaultlist_elements)
191             handlePlotmanip(plotmanip_elements)
192             
193         def handleDisplay(display_elements):
194             for element in display_elements:
195                 for attribute in element.attributes.keys():
196                     self.config[attribute]=element.getAttribute(attribute)
197                     
198         def handlePlugins(plugins):
199             for plugin in plugins[0].childNodes:
200                 try:
201                     self.config['plugins'].append(str(plugin.tagName))
202                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
203                     pass
204         #FIXME: code duplication
205         def handleDrivers(drivers):
206             for driver in drivers[0].childNodes:
207                 try:
208                     self.config['drivers'].append(str(driver.tagName))
209                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
210                     pass
211         
212         def handlePlotmanip(plotmanips):
213             for plotmanip in plotmanips[0].childNodes:
214                 try:
215                     self.config['plotmanips'].append(str(plotmanip.tagName))
216                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
217                     pass
218         
219         def handleWorkdir(workdir):
220             '''
221             default working directory
222             '''
223             wdir=getText(workdir[0].childNodes)
224             self.config['workdir']=wdir.strip()
225             
226         def handleDefaultlist(defaultlist):
227             '''
228             default playlist
229             '''
230             dflist=getText(defaultlist[0].childNodes)
231             self.config['defaultlist']=dflist.strip()
232             
233         handleConfig(self.config_tree)
234         #making items in the dictionary more machine-readable
235         for item in self.config.keys():
236             try:
237                 self.config[item]=float(self.config[item])
238             except TypeError: #we are dealing with a list, probably. keep it this way.
239                 try:
240                     self.config[item]=eval(self.config[item])
241                 except: #not a list, not a tuple, probably a string?
242                     pass
243             except ValueError: #if we can't get it to a number, it must be None or a string
244                 if string.lower(self.config[item])=='none':
245                     self.config[item]=None
246                 else:
247                     pass
248                                                 
249         return self.config
250         
251         
252     def save_config(self, config_filename):
253         print 'Not Implemented.'
254         pass    
255
256
257 class EngrFormatter(ScalarFormatter):
258     """A variation of the standard ScalarFormatter, using only multiples of 
259 three
260 in the mantissa. A fixed number of decimals can be displayed with the optional 
261 parameter `ndec` . If `ndec` is None (default), the number of decimals is 
262 defined
263 from the current ticks.
264     """
265     def __init__(self, ndec=None, useOffset=True, useMathText=False):
266         ScalarFormatter.__init__(self, useOffset, useMathText)
267         if ndec is None or ndec < 0:
268             self.format = None
269         elif ndec == 0:
270             self.format = "%d"
271         else:
272             self.format = "%%1.%if" % ndec
273     #........................
274
275     def _set_orderOfMagnitude(self, mrange):
276             """Sets the order of magnitude."""        
277             locs = numpy.absolute(self.locs)
278             if self.offset: 
279                 oom = numpy.floor(numpy.log10(mrange))
280             else:
281                 if locs[0] > locs[-1]: 
282                     val = locs[0]
283                 else: 
284                     val = locs[-1]
285                 if val == 0: 
286                     oom = 0
287                 else: 
288                     oom = numpy.floor(numpy.log10(val))
289             if oom <= -3:
290                 self.orderOfMagnitude = 3*(oom//3)
291             elif oom <= -1:
292                 self.orderOfMagnitude = -3
293             elif oom >= 4:
294                 self.orderOfMagnitude = 3*(oom//3)
295             else:
296                 self.orderOfMagnitude = 0
297
298
299     #........................
300     def _set_format(self):
301         """Sets the format string to format all ticklabels."""
302         # set the format string to format all the ticklabels
303         locs = (numpy.array(self.locs)-self.offset) /  10**self.orderOfMagnitude+1e-15
304         sigfigs = [len(str('%1.3f'% loc).split('.')[1].rstrip('0')) \
305                    for loc in locs]
306         sigfigs.sort()
307         if self.format is None:
308             self.format = '%1.' + str(sigfigs[-1]) + 'f'
309         if self._usetex or self._useMathText: self.format = '$%s$'%self.format
310
311
312
313 class ClickedPoint:
314     '''
315     this class defines what a clicked point on the curve plot is
316     '''
317     def __init__(self):
318         
319         self.is_marker=None #boolean ; decides if it is a marker
320         self.is_line_edge=None #boolean ; decides if it is the edge of a line (unused)
321         self.absolute_coords=(None,None) #(float,float) ; the absolute coordinates of the clicked point on the graph
322         self.graph_coords=(None,None) #(float,float) ; the coordinates of the plot that are nearest in X to the clicked point
323         self.index=None #integer ; the index of the clicked point with respect to the vector selected
324         self.dest=None #0 or 1 ; 0=top plot 1=bottom plot
325                 
326         
327     def find_graph_coords_old(self, xvector, yvector):
328         '''
329         Given a clicked point on the plot, finds the nearest point in the dataset (in X) that
330         corresponds to the clicked point.
331         OLD & DEPRECATED - to be removed
332         '''
333                    
334         #FIXME: a general algorithm using min() is needed!
335         #print '---DEPRECATED FIND_GRAPH_COORDS_OLD---'
336         best_index=0
337         best_dist=10**9 #should be more than enough given the scale
338                 
339         for index in scipy.arange(1,len(xvector),1):
340             dist=((self.absolute_coords[0]-xvector[index])**2)+(100*((self.absolute_coords[1]-yvector[index])))**2
341                         #TODO, generalize? y coordinate is multiplied by 100 due to scale differences in the plot
342             if dist<best_dist:
343                 best_index=index
344                 best_dist=dist
345                         
346         self.index=best_index
347         self.graph_coords=(xvector[best_index],yvector[best_index])
348         return
349             
350     def find_graph_coords(self,xvector,yvector):
351         '''
352         Given a clicked point on the plot, finds the nearest point in the dataset that
353         corresponds to the clicked point.
354         '''
355         dists=[]
356         for index in scipy.arange(1,len(xvector),1):
357             dists.append(((self.absolute_coords[0]-xvector[index])**2)+((self.absolute_coords[1]-yvector[index])**2))
358                         
359         self.index=dists.index(min(dists))
360         self.graph_coords=(xvector[self.index],yvector[self.index])
361 #-----------------------------------------
362 #CSV-HELPING FUNCTIONS        
363         
364 def transposed2(lists, defval=0):
365     '''
366     transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
367     elements
368     (by Zoran Isailovski on the Python Cookbook online)
369     '''
370     if not lists: return []
371     return map(lambda *row: [elem or defval for elem in row], *lists)
372         
373 def csv_write_dictionary(f, data, sorting='COLUMNS'):
374     '''
375     Writes a CSV file from a dictionary, with keys as first column or row
376     Keys are in "random" order.
377     
378     Keys should be strings
379     Values should be lists or other iterables
380     '''
381     keys=data.keys()
382     values=data.values()
383     t_values=transposed2(values)
384     writer=csv.writer(f)
385
386     if sorting=='COLUMNS':
387         writer.writerow(keys)
388         for item in t_values:
389             writer.writerow(item)
390         
391     if sorting=='ROWS':
392         print 'Not implemented!' #FIXME: implement it.
393
394
395 #-----------------------------------------        
396                     
397 def debug():
398     '''
399     debug stuff from latest rewrite of hooke_playlist.py
400     should be removed sooner or later (or substituted with new debug code!)
401     '''
402     confo=HookeConfig()
403     print confo.load_config('hooke.conf')
404
405 if __name__ == '__main__':
406     debug()