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