Add support for CASAVA 1.7
[htsworkflow.git] / htsworkflow / pipelines / bustard.py
1 """
2 Extract configuration from Illumina Bustard Directory.
3
4 This includes the version number, run date, bustard executable parameters, and 
5 phasing estimates. 
6 """
7 from copy import copy
8 from datetime import date
9 from glob import glob
10 import logging
11 import os
12 import re
13 import sys
14 import time
15
16 from htsworkflow.pipelines.runfolder import \
17    ElementTree, \
18    VERSION_RE, \
19    EUROPEAN_STRPTIME
20
21 # make epydoc happy
22 __docformat__ = "restructuredtext en"
23
24 LANE_LIST = range(1,9)
25
26 class Phasing(object):
27     PHASING = 'Phasing'
28     PREPHASING = 'Prephasing'
29
30     def __init__(self, fromfile=None, xml=None):
31         self.lane = None
32         self.phasing = None
33         self.prephasing = None
34
35         if fromfile is not None:
36             self._initialize_from_file(fromfile)
37         elif xml is not None:
38             self.set_elements(xml)
39
40     def _initialize_from_file(self, pathname):
41         path, name = os.path.split(pathname)
42         basename, ext = os.path.splitext(name)
43         # the last character of the param base filename should be the
44         # lane number
45         tree = ElementTree.parse(pathname).getroot()
46         self.set_elements(tree)
47         self.lane = int(basename[-1])
48
49     def get_elements(self):
50         root = ElementTree.Element(Phasing.PHASING, {'lane': str(self.lane)})
51         root.tail = os.linesep
52         phasing = ElementTree.SubElement(root, Phasing.PHASING)
53         phasing.text = str(self.phasing)
54         phasing.tail = os.linesep
55         prephasing = ElementTree.SubElement(root, Phasing.PREPHASING)
56         prephasing.text = str(self.prephasing)
57         prephasing.tail = os.linesep
58         return root
59
60     def set_elements(self, tree):
61         if tree.tag not in ('Phasing', 'Parameters'):
62             raise ValueError('exptected Phasing or Parameters')
63         lane = tree.attrib.get('lane', None)
64         if lane is not None:
65             self.lane = int(lane)
66         for element in list(tree):
67             if element.tag == Phasing.PHASING:
68                 self.phasing = float(element.text)
69             elif element.tag == Phasing.PREPHASING:
70                 self.prephasing = float(element.text)
71
72 class CrosstalkMatrix(object):
73     CROSSTALK = "MatrixElements"
74     BASE = 'Base'
75     ELEMENT = 'Element'
76
77     def __init__(self, fromfile=None, xml=None):
78         self.base = {}
79
80         if fromfile is not None:
81             self._initialize_from_file(fromfile)
82         elif xml is not None:
83             self.set_elements(xml)
84
85     def _initialize_from_file(self, pathname):
86         data = open(pathname).readlines()
87         auto_header = '# Auto-generated frequency response matrix'
88         if data[0].strip() == auto_header and len(data) == 9:
89             # skip over lines 1,2,3,4 which contain the 4 bases
90             self.base['A'] = [ float(v) for v in data[5].split() ]
91             self.base['C'] = [ float(v) for v in data[6].split() ]
92             self.base['G'] = [ float(v) for v in data[7].split() ]
93             self.base['T'] = [ float(v) for v in data[8].split() ]
94         elif len(data) == 16:
95             self.base['A'] = [ float(v) for v in data[:4] ]
96             self.base['C'] = [ float(v) for v in data[4:8] ]
97             self.base['G'] = [ float(v) for v in data[8:12] ]
98             self.base['T'] = [ float(v) for v in data[12:16] ]            
99         else:
100             raise RuntimeError("matrix file %s is unusual" % (pathname,))
101     def get_elements(self):
102         root = ElementTree.Element(CrosstalkMatrix.CROSSTALK)
103         root.tail = os.linesep
104         base_order = ['A','C','G','T']
105         for b in base_order:
106             base_element = ElementTree.SubElement(root, CrosstalkMatrix.BASE)
107             base_element.text = b
108             base_element.tail = os.linesep
109         for b in base_order:
110             for value in self.base[b]:
111                 crosstalk_value = ElementTree.SubElement(root, CrosstalkMatrix.ELEMENT)
112                 crosstalk_value.text = unicode(value)
113                 crosstalk_value.tail = os.linesep
114
115         return root
116
117     def set_elements(self, tree):
118         if tree.tag != CrosstalkMatrix.CROSSTALK:
119             raise ValueError('Invalid run-xml exptected '+CrosstalkMatrix.CROSSTALK)
120         base_order = []
121         current_base = None
122         current_index = 0
123         for element in tree.getchildren():
124             # read in the order of the bases
125             if element.tag == 'Base':
126                 base_order.append(element.text)
127             elif element.tag == 'Element':
128                 # we're done reading bases, now its just the 4x4 matrix
129                 # written out as a list of elements
130                 # if this is the first element, make a copy of the list
131                 # to play with and initialize an empty list for the current base
132                 if current_base is None:
133                     current_base = copy(base_order)
134                     self.base[current_base[0]] = []
135                 # we found (probably) 4 bases go to the next base
136                 if current_index == len(base_order):
137                     current_base.pop(0)
138                     current_index = 0
139                     self.base[current_base[0]] = []
140                 value = float(element.text)
141                 self.base[current_base[0]].append(value)
142
143                 current_index += 1
144             else:
145                 raise RuntimeError("Unrecognized tag in run xml: %s" %(element.tag,))
146
147 def crosstalk_matrix_from_bustard_config(bustard_path, bustard_config_tree):
148     """
149     Analyze the bustard config file and try to find the crosstalk matrix.
150     """
151     bustard_run = bustard_config_tree[0]
152     if bustard_run.tag != 'Run':
153         raise RuntimeError('Expected Run tag, got %s' % (bustard_run.tag,))
154
155     call_parameters = bustard_run.find('BaseCallParameters')
156     if call_parameters is None:
157         raise RuntimeError('Missing BaseCallParameters section')
158
159     matrix = call_parameters.find('Matrix')
160     if matrix is None:
161         raise RuntimeError('Expected to find Matrix in Bustard BaseCallParameters')
162
163     matrix_auto_flag = int(matrix.find('AutoFlag').text)
164     matrix_auto_lane = int(matrix.find('AutoLane').text)
165
166     crosstalk = None
167     if matrix_auto_flag:
168         # we estimated the matrix from something in this run.
169         # though we don't really care which lane it was
170         if matrix_auto_lane == 0:
171             matrix_path = os.path.join(bustard_path, 
172                                        'Matrix', 's_02_matrix.txt')
173         else:
174             matrix_path = os.path.join(bustard_path, 'Matrix',
175                            's_%d_1_matrix.txt' % (matrix_auto_lane,))
176         crosstalk = CrosstalkMatrix(matrix_path)
177     else:
178         matrix_elements = call_parameters.find('MatrixElements')
179         # the matrix was provided
180         if matrix_elements is not None:
181             crosstalk = CrosstalkMatrix(xml=matrix_elements)
182         else:
183             # we have no crosstalk matrix?
184             pass
185
186     return crosstalk
187
188 class Bustard(object):
189     XML_VERSION = 2
190
191     # Xml Tags
192     BUSTARD = 'Bustard'
193     SOFTWARE_VERSION = 'version'
194     DATE = 'run_time'
195     USER = 'user'
196     PARAMETERS = 'Parameters'
197     BUSTARD_CONFIG = 'BaseCallAnalysis'
198
199     def __init__(self, xml=None):
200         self.version = None
201         self.date = None
202         self.user = None
203         self.phasing = {}
204         self.crosstalk = None
205         self.pathname = None
206         self.bustard_config = None
207
208         if xml is not None:
209             self.set_elements(xml)
210
211     def _get_time(self):
212         if self.date is None:
213             return None
214         return time.mktime(self.date.timetuple())
215     time = property(_get_time, doc='return run time as seconds since epoch')
216
217     def dump(self):
218         #print ElementTree.tostring(self.get_elements())
219         ElementTree.dump(self.get_elements())
220
221     def get_elements(self):
222         root = ElementTree.Element('Bustard', 
223                                    {'version': str(Bustard.XML_VERSION)})
224         version = ElementTree.SubElement(root, Bustard.SOFTWARE_VERSION)
225         version.text = self.version
226         if self.time is not None:
227             run_date = ElementTree.SubElement(root, Bustard.DATE)
228             run_date.text = str(self.time)
229         if self.user is not None:
230             user = ElementTree.SubElement(root, Bustard.USER)
231             user.text = self.user
232         params = ElementTree.SubElement(root, Bustard.PARAMETERS)
233
234         # add phasing parameters
235         for lane in LANE_LIST:
236             if self.phasing.has_key(lane):
237                 params.append(self.phasing[lane].get_elements())
238
239         # add crosstalk matrix if it exists
240         if self.crosstalk is not None:
241             root.append(self.crosstalk.get_elements())
242        
243         # add bustard config if it exists
244         if self.bustard_config is not None:
245             root.append(self.bustard_config)
246         return root
247
248     def set_elements(self, tree):
249         if tree.tag != Bustard.BUSTARD:
250             raise ValueError('Expected "Bustard" SubElements')
251         xml_version = int(tree.attrib.get('version', 0))
252         if xml_version > Bustard.XML_VERSION:
253             logging.warn('Bustard XML tree is a higher version than this class')
254         for element in list(tree):
255             if element.tag == Bustard.SOFTWARE_VERSION:
256                 self.version = element.text
257             elif element.tag == Bustard.DATE:
258                 self.date = date.fromtimestamp(float(element.text))
259             elif element.tag == Bustard.USER:
260                 self.user = element.text
261             elif element.tag == Bustard.PARAMETERS:
262                 for param in element:
263                     p = Phasing(xml=param)
264                     self.phasing[p.lane] = p
265             elif element.tag == CrosstalkMatrix.CROSSTALK:
266                 self.crosstalk = CrosstalkMatrix(xml=element)
267             elif element.tag == Bustard.BUSTARD_CONFIG:
268                 self.bustard_config = element
269             else:
270                 raise ValueError("Unrecognized tag: %s" % (element.tag,))
271         
272 def bustard(pathname):
273     """
274     Construct a Bustard object by analyzing an Illumina Bustard directory.
275
276     :Parameters:
277       - `pathname`: A bustard directory
278
279     :Return:
280       Fully initialized Bustard object.
281     """
282     b = Bustard()
283     pathname = os.path.abspath(pathname)
284     path, name = os.path.split(pathname)
285     groups = name.split("_")
286     if groups[0].lower().startswith('bustard'):
287         version = re.search(VERSION_RE, groups[0])
288         b.version = version.group(1)
289         t = time.strptime(groups[1], EUROPEAN_STRPTIME)
290         b.date = date(*t[0:3])
291         b.user = groups[2]
292     elif groups[0] == 'BaseCalls':
293         # stub values
294         b.version = None
295         b.date = None
296         b.user = None
297
298     b.pathname = pathname
299     bustard_config_filename = os.path.join(pathname, 'config.xml')
300     paramfiles = glob(os.path.join(pathname, "params?.xml"))
301     for paramfile in paramfiles:
302         phasing = Phasing(paramfile)
303         assert (phasing.lane >= 1 and phasing.lane <= 8)
304         b.phasing[phasing.lane] = phasing
305     # I only found these in Bustard1.9.5/1.9.6 directories
306     if b.version in ('1.9.5', '1.9.6'):
307         # at least for our runfolders for 1.9.5 and 1.9.6 matrix[1-8].txt are always the same
308         crosstalk_file = os.path.join(pathname, "matrix1.txt")
309         b.crosstalk = CrosstalkMatrix(crosstalk_file)
310     # for version 1.3.2 of the pipeline the bustard version number went down
311     # to match the rest of the pipeline. However there's now a nifty
312     # new (useful) bustard config file.
313     elif os.path.exists(bustard_config_filename):
314         bustard_config_root = ElementTree.parse(bustard_config_filename)
315         b.bustard_config = bustard_config_root.getroot()
316         b.crosstalk = crosstalk_matrix_from_bustard_config(b.pathname, b.bustard_config)
317         software = bustard_config_root.find('*/Software')
318         b.version = software.attrib['Version']
319         #b.version = software.attrib['Name'] + "-" + software.attrib['Version']
320
321     return b
322
323 def fromxml(tree):
324     """
325     Reconstruct a htsworkflow.pipelines.Bustard object from an xml block
326     """
327     b = Bustard()
328     b.set_elements(tree)
329     return b
330
331 def make_cmdline_parser():
332     from optparse import OptionParser
333     parser = OptionParser('%prog: bustard_directory')
334     return parser
335
336 def main(cmdline):
337     parser = make_cmdline_parser()
338     opts, args = parser.parse_args(cmdline)
339
340     for bustard_dir in args:
341         print u'analyzing bustard directory: ' + unicode(bustard_dir)
342         bustard_object = bustard(bustard_dir)
343         bustard_object.dump() 
344
345         bustard_object2 = Bustard(xml=bustard_object.get_elements())
346         print ('-------------------------------------')
347         bustard_object2.dump()
348         print ('=====================================')
349         b1_tree = bustard_object.get_elements()
350         b1 = ElementTree.tostring(b1_tree).split(os.linesep)
351         b2_tree = bustard_object2.get_elements()
352         b2 = ElementTree.tostring(b2_tree).split(os.linesep)
353         for line1, line2 in zip(b1, b2):
354             if b1 != b2:
355                 print "b1: ", b1
356                 print "b2: ", b2
357
358 if __name__ == "__main__":
359     main(sys.argv[1:])