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