From 4e5d8893e55e9d6e3134d40bd261f0ad7173b62c Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 5 Dec 2011 12:59:34 +0000 Subject: [PATCH] git-svn-id: https://www.grenoble.prabi.fr/svn/LECASofts/ecoPCR/trunk@390 60f365c0-8329-0410-b2a4-ec073aeeaa1d --- obitools/SVGdraw.py | 1054 +++++++++++++++++ obitools/__init__.py | 711 ++++++++++++ obitools/__init__.pyc | Bin 0 -> 32071 bytes obitools/align/__init__.py | 13 + obitools/align/_assemble.so | Bin 0 -> 78032 bytes obitools/align/_dynamic.so | Bin 0 -> 127400 bytes obitools/align/_freeendgap.so | Bin 0 -> 71244 bytes obitools/align/_freeendgapfm.so | Bin 0 -> 53936 bytes obitools/align/_lcs.so | Bin 0 -> 147472 bytes obitools/align/_nws.so | Bin 0 -> 72308 bytes obitools/align/_profilenws.so | Bin 0 -> 131288 bytes obitools/align/_qsassemble.so | Bin 0 -> 91624 bytes obitools/align/_qsrassemble.so | Bin 0 -> 92072 bytes obitools/align/_rassemble.so | Bin 0 -> 78084 bytes obitools/align/_upperbond.so | Bin 0 -> 105440 bytes obitools/align/homopolymere.py | 56 + obitools/align/ssearch.py | 46 + obitools/alignment/__init__.py | 175 +++ obitools/alignment/ace.py | 47 + obitools/barcodecoverage/__init__.py | 7 + obitools/barcodecoverage/calcBc.py | 62 + obitools/barcodecoverage/calculateBc.py | 72 ++ obitools/barcodecoverage/drawBcTree.py | 108 ++ obitools/barcodecoverage/findErrors.py | 56 + obitools/barcodecoverage/readFiles.py | 69 ++ obitools/barcodecoverage/writeBcTree.py | 42 + obitools/blast/__init__.py | 207 ++++ obitools/carto/__init__.py | 376 ++++++ obitools/decorator.py | 0 obitools/distances/__init__.py | 29 + obitools/distances/observed.py | 77 ++ obitools/distances/phylip.py | 35 + obitools/distances/r.py | 25 + obitools/dnahash/__init__.py | 100 ++ obitools/ecobarcode/__init__.py | 0 obitools/ecobarcode/databases.py | 32 + obitools/ecobarcode/ecotag.py | 50 + obitools/ecobarcode/options.py | 64 + obitools/ecobarcode/rawdata.py | 38 + obitools/ecobarcode/taxonomy.py | 120 ++ obitools/ecopcr/__init__.py | 69 ++ obitools/ecopcr/annotation.py | 104 ++ obitools/ecopcr/options.py | 129 +++ obitools/ecopcr/sequence.py | 133 +++ obitools/ecopcr/taxonomy.py | 630 ++++++++++ obitools/ecotag/__init__.py | 2 + obitools/ecotag/parser.py | 150 +++ obitools/eutils/__init__.py | 54 + obitools/fast.py | 56 + obitools/fasta/__init__.py | 384 ++++++ obitools/fasta/_fasta.so | Bin 0 -> 75428 bytes obitools/fastq/__init__.py | 190 +++ obitools/fastq/_fastq.so | Bin 0 -> 159528 bytes obitools/fnaqual/__init__.py | 2 + obitools/fnaqual/fasta.py | 8 + obitools/fnaqual/quality.py | 137 +++ obitools/format/__init__.py | 28 + obitools/format/_format.so | Bin 0 -> 52432 bytes obitools/format/genericparser/__init__.py | 217 ++++ obitools/format/ontology/__init__.py | 0 obitools/format/ontology/go_obo.py | 274 +++++ obitools/format/options.py | 284 +++++ obitools/format/sequence/__init__.py | 24 + obitools/format/sequence/embl.py | 2 + obitools/format/sequence/fasta.py | 4 + obitools/format/sequence/fastq.py | 13 + obitools/format/sequence/fnaqual.py | 8 + obitools/format/sequence/genbank.py | 4 + obitools/format/sequence/tagmatcher.py | 5 + obitools/goa/__init__.py | 0 obitools/goa/parser.py | 33 + obitools/graph/__init__.py | 962 ++++++++++++++++ obitools/graph/__init__.pyc | Bin 0 -> 29446 bytes obitools/graph/algorithms/__init__.py | 0 obitools/graph/algorithms/__init__.pyc | Bin 0 -> 201 bytes obitools/graph/algorithms/clique.py | 134 +++ obitools/graph/algorithms/compact.py | 8 + obitools/graph/algorithms/component.py | 82 ++ obitools/graph/algorithms/component.pyc | Bin 0 -> 950 bytes obitools/graph/dag.py | 80 ++ obitools/graph/layout/__init__.py | 0 obitools/graph/layout/radialtree.py | 0 obitools/graph/rootedtree.py | 117 ++ obitools/graph/tree.py | 37 + obitools/gzip.py | 504 ++++++++ obitools/gzip.pyc | Bin 0 -> 17277 bytes obitools/location/__init__.py | 538 +++++++++ obitools/location/__init__.pyc | Bin 0 -> 30440 bytes obitools/location/feature.py | 177 +++ obitools/metabarcoding/__init__.py | 265 +++++ obitools/metabarcoding/options.py | 34 + obitools/obischemas/__init__.py | 28 + obitools/obischemas/kb/__init__.py | 55 + obitools/obischemas/kb/extern.py | 78 ++ obitools/obischemas/options.py | 31 + obitools/obo/__init__.py | 0 obitools/obo/go/__init__.py | 0 obitools/obo/go/parser.py | 53 + obitools/obo/parser.py | 707 ++++++++++++ obitools/options/__init__.py | 137 +++ obitools/options/bioseqcutter.py | 85 ++ obitools/options/bioseqedittag.py | 237 ++++ obitools/options/bioseqfilter.py | 179 +++ obitools/options/taxonomyfilter.py | 6 + obitools/parallel/__init__.py | 99 ++ obitools/parallel/jobqueue.py | 183 +++ obitools/phylogeny/__init__.py | 119 ++ obitools/phylogeny/newick.py | 123 ++ obitools/profile/__init__.py | 0 obitools/profile/_profile.so | Bin 0 -> 129896 bytes obitools/sample.py | 76 ++ obitools/seqdb/__init__.py | 88 ++ obitools/seqdb/blastdb/__init__.py | 0 obitools/seqdb/dnaparser.py | 16 + obitools/seqdb/embl/__init__.py | 13 + obitools/seqdb/embl/parser.py | 50 + obitools/seqdb/genbank/__init__.py | 84 ++ obitools/seqdb/genbank/ncbi.py | 79 ++ obitools/seqdb/genbank/parser.py | 53 + obitools/sequenceencoder/__init__.py | 73 ++ obitools/sequenceencoder/__init__.pyc | Bin 0 -> 4297 bytes obitools/solexa/__init__.py | 45 + obitools/statistics/__init__.py | 0 obitools/statistics/hypergeometric.py | 166 +++ obitools/statistics/noncentralhypergeo.py | 208 ++++ obitools/svg.py | 120 ++ obitools/table/__init__.py | 633 ++++++++++ obitools/table/csv.py | 52 + obitools/tagmatcher/__init__.py | 35 + obitools/tagmatcher/options.py | 14 + obitools/tagmatcher/parser.py | 89 ++ obitools/thermo/__init__.py | 597 ++++++++++ obitools/tools/__init__.py | 0 obitools/tools/_solexapairend.so | Bin 0 -> 130384 bytes obitools/tools/solexapairend.py | 51 + obitools/tree/__init__.py | 116 ++ obitools/tree/dot.py | 18 + obitools/tree/layout.py | 103 ++ obitools/tree/newick.py | 117 ++ obitools/tree/svg.py | 70 ++ obitools/tree/unrooted.py | 33 + obitools/unit/__init__.py | 8 + obitools/unit/obitools/__init__.py | 89 ++ obitools/utils/__init__.py | 324 ++++++ obitools/utils/__init__.pyc | Bin 0 -> 12369 bytes obitools/utils/bioseq.py | 232 ++++ obitools/utils/crc64.py | 53 + obitools/utils/iterator.py | 8 + obitools/utils/iterator.pyc | Bin 0 -> 565 bytes obitools/word/__init__.py | 72 ++ obitools/word/_binary.so | Bin 0 -> 150000 bytes obitools/word/options.py | 116 ++ obitools/word/predicate.py | 41 + obitools/zipfile.py | 1282 +++++++++++++++++++++ obitools/zipfile.pyc | Bin 0 -> 38336 bytes 155 files changed, 16897 insertions(+) create mode 100644 obitools/SVGdraw.py create mode 100644 obitools/__init__.py create mode 100644 obitools/__init__.pyc create mode 100644 obitools/align/__init__.py create mode 100755 obitools/align/_assemble.so create mode 100755 obitools/align/_dynamic.so create mode 100755 obitools/align/_freeendgap.so create mode 100755 obitools/align/_freeendgapfm.so create mode 100755 obitools/align/_lcs.so create mode 100755 obitools/align/_nws.so create mode 100755 obitools/align/_profilenws.so create mode 100755 obitools/align/_qsassemble.so create mode 100755 obitools/align/_qsrassemble.so create mode 100755 obitools/align/_rassemble.so create mode 100755 obitools/align/_upperbond.so create mode 100644 obitools/align/homopolymere.py create mode 100644 obitools/align/ssearch.py create mode 100644 obitools/alignment/__init__.py create mode 100644 obitools/alignment/ace.py create mode 100644 obitools/barcodecoverage/__init__.py create mode 100644 obitools/barcodecoverage/calcBc.py create mode 100644 obitools/barcodecoverage/calculateBc.py create mode 100644 obitools/barcodecoverage/drawBcTree.py create mode 100644 obitools/barcodecoverage/findErrors.py create mode 100644 obitools/barcodecoverage/readFiles.py create mode 100644 obitools/barcodecoverage/writeBcTree.py create mode 100644 obitools/blast/__init__.py create mode 100644 obitools/carto/__init__.py create mode 100644 obitools/decorator.py create mode 100644 obitools/distances/__init__.py create mode 100644 obitools/distances/observed.py create mode 100644 obitools/distances/phylip.py create mode 100644 obitools/distances/r.py create mode 100644 obitools/dnahash/__init__.py create mode 100644 obitools/ecobarcode/__init__.py create mode 100644 obitools/ecobarcode/databases.py create mode 100644 obitools/ecobarcode/ecotag.py create mode 100644 obitools/ecobarcode/options.py create mode 100644 obitools/ecobarcode/rawdata.py create mode 100644 obitools/ecobarcode/taxonomy.py create mode 100644 obitools/ecopcr/__init__.py create mode 100644 obitools/ecopcr/annotation.py create mode 100644 obitools/ecopcr/options.py create mode 100644 obitools/ecopcr/sequence.py create mode 100644 obitools/ecopcr/taxonomy.py create mode 100644 obitools/ecotag/__init__.py create mode 100644 obitools/ecotag/parser.py create mode 100644 obitools/eutils/__init__.py create mode 100644 obitools/fast.py create mode 100644 obitools/fasta/__init__.py create mode 100755 obitools/fasta/_fasta.so create mode 100644 obitools/fastq/__init__.py create mode 100755 obitools/fastq/_fastq.so create mode 100644 obitools/fnaqual/__init__.py create mode 100644 obitools/fnaqual/fasta.py create mode 100644 obitools/fnaqual/quality.py create mode 100644 obitools/format/__init__.py create mode 100755 obitools/format/_format.so create mode 100644 obitools/format/genericparser/__init__.py create mode 100644 obitools/format/ontology/__init__.py create mode 100644 obitools/format/ontology/go_obo.py create mode 100644 obitools/format/options.py create mode 100644 obitools/format/sequence/__init__.py create mode 100644 obitools/format/sequence/embl.py create mode 100644 obitools/format/sequence/fasta.py create mode 100644 obitools/format/sequence/fastq.py create mode 100644 obitools/format/sequence/fnaqual.py create mode 100644 obitools/format/sequence/genbank.py create mode 100644 obitools/format/sequence/tagmatcher.py create mode 100644 obitools/goa/__init__.py create mode 100644 obitools/goa/parser.py create mode 100644 obitools/graph/__init__.py create mode 100644 obitools/graph/__init__.pyc create mode 100644 obitools/graph/algorithms/__init__.py create mode 100644 obitools/graph/algorithms/__init__.pyc create mode 100644 obitools/graph/algorithms/clique.py create mode 100644 obitools/graph/algorithms/compact.py create mode 100644 obitools/graph/algorithms/component.py create mode 100644 obitools/graph/algorithms/component.pyc create mode 100644 obitools/graph/dag.py create mode 100644 obitools/graph/layout/__init__.py create mode 100644 obitools/graph/layout/radialtree.py create mode 100644 obitools/graph/rootedtree.py create mode 100644 obitools/graph/tree.py create mode 100644 obitools/gzip.py create mode 100644 obitools/gzip.pyc create mode 100644 obitools/location/__init__.py create mode 100644 obitools/location/__init__.pyc create mode 100644 obitools/location/feature.py create mode 100644 obitools/metabarcoding/__init__.py create mode 100644 obitools/metabarcoding/options.py create mode 100644 obitools/obischemas/__init__.py create mode 100644 obitools/obischemas/kb/__init__.py create mode 100644 obitools/obischemas/kb/extern.py create mode 100644 obitools/obischemas/options.py create mode 100644 obitools/obo/__init__.py create mode 100644 obitools/obo/go/__init__.py create mode 100644 obitools/obo/go/parser.py create mode 100644 obitools/obo/parser.py create mode 100644 obitools/options/__init__.py create mode 100644 obitools/options/bioseqcutter.py create mode 100644 obitools/options/bioseqedittag.py create mode 100644 obitools/options/bioseqfilter.py create mode 100644 obitools/options/taxonomyfilter.py create mode 100644 obitools/parallel/__init__.py create mode 100644 obitools/parallel/jobqueue.py create mode 100644 obitools/phylogeny/__init__.py create mode 100644 obitools/phylogeny/newick.py create mode 100644 obitools/profile/__init__.py create mode 100755 obitools/profile/_profile.so create mode 100644 obitools/sample.py create mode 100644 obitools/seqdb/__init__.py create mode 100644 obitools/seqdb/blastdb/__init__.py create mode 100644 obitools/seqdb/dnaparser.py create mode 100644 obitools/seqdb/embl/__init__.py create mode 100644 obitools/seqdb/embl/parser.py create mode 100644 obitools/seqdb/genbank/__init__.py create mode 100644 obitools/seqdb/genbank/ncbi.py create mode 100644 obitools/seqdb/genbank/parser.py create mode 100644 obitools/sequenceencoder/__init__.py create mode 100644 obitools/sequenceencoder/__init__.pyc create mode 100644 obitools/solexa/__init__.py create mode 100644 obitools/statistics/__init__.py create mode 100644 obitools/statistics/hypergeometric.py create mode 100644 obitools/statistics/noncentralhypergeo.py create mode 100644 obitools/svg.py create mode 100644 obitools/table/__init__.py create mode 100644 obitools/table/csv.py create mode 100644 obitools/tagmatcher/__init__.py create mode 100644 obitools/tagmatcher/options.py create mode 100644 obitools/tagmatcher/parser.py create mode 100644 obitools/thermo/__init__.py create mode 100644 obitools/tools/__init__.py create mode 100755 obitools/tools/_solexapairend.so create mode 100644 obitools/tools/solexapairend.py create mode 100644 obitools/tree/__init__.py create mode 100644 obitools/tree/dot.py create mode 100644 obitools/tree/layout.py create mode 100644 obitools/tree/newick.py create mode 100644 obitools/tree/svg.py create mode 100644 obitools/tree/unrooted.py create mode 100644 obitools/unit/__init__.py create mode 100644 obitools/unit/obitools/__init__.py create mode 100644 obitools/utils/__init__.py create mode 100644 obitools/utils/__init__.pyc create mode 100644 obitools/utils/bioseq.py create mode 100644 obitools/utils/crc64.py create mode 100644 obitools/utils/iterator.py create mode 100644 obitools/utils/iterator.pyc create mode 100644 obitools/word/__init__.py create mode 100755 obitools/word/_binary.so create mode 100644 obitools/word/options.py create mode 100644 obitools/word/predicate.py create mode 100644 obitools/zipfile.py create mode 100644 obitools/zipfile.pyc diff --git a/obitools/SVGdraw.py b/obitools/SVGdraw.py new file mode 100644 index 0000000..521f750 --- /dev/null +++ b/obitools/SVGdraw.py @@ -0,0 +1,1054 @@ +#!/usr/bin/env python +##Copyright (c) 2002, Fedor Baart & Hans de Wit (Stichting Farmaceutische Kengetallen) +##All rights reserved. +## +##Redistribution and use in source and binary forms, with or without modification, +##are permitted provided that the following conditions are met: +## +##Redistributions of source code must retain the above copyright notice, this +##list of conditions and the following disclaimer. +## +##Redistributions in binary form must reproduce the above copyright notice, +##this list of conditions and the following disclaimer in the documentation and/or +##other materials provided with the distribution. +## +##Neither the name of the Stichting Farmaceutische Kengetallen nor the names of +##its contributors may be used to endorse or promote products derived from this +##software without specific prior written permission. +## +##THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +##AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +##IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +##DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +##FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +##DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +##SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +##CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +##OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +##OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +##Thanks to Gerald Rosennfellner for his help and useful comments. + +__doc__="""Use SVGdraw to generate your SVGdrawings. + +SVGdraw uses an object model drawing and a method toXML to create SVG graphics +by using easy to use classes and methods usualy you start by creating a drawing eg + + d=drawing() + #then you create a SVG root element + s=svg() + #then you add some elements eg a circle and add it to the svg root element + c=circle() + #you can supply attributes by using named arguments. + c=circle(fill='red',stroke='blue') + #or by updating the attributes attribute: + c.attributes['stroke-width']=1 + s.addElement(c) + #then you add the svg root element to the drawing + d.setSVG(s) + #and finaly you xmlify the drawing + d.toXml() + + +this results in the svg source of the drawing, which consists of a circle +on a white background. Its as easy as that;) +This module was created using the SVG specification of www.w3c.org and the +O'Reilly (www.oreilly.com) python books as information sources. A svg viewer +is available from www.adobe.com""" + +__version__="1.0" + +# there are two possibilities to generate svg: +# via a dom implementation and directly using text strings +# the latter is way faster (and shorter in coding) +# the former is only used in debugging svg programs +# maybe it will be removed alltogether after a while +# with the following variable you indicate whether to use the dom implementation +# Note that PyXML is required for using the dom implementation. +# It is also possible to use the standard minidom. But I didn't try that one. +# Anyway the text based approach is about 60 times faster than using the full dom implementation. +use_dom_implementation=0 + + +import exceptions +if use_dom_implementation<>0: + try: + from xml.dom import implementation + from xml.dom.ext import PrettyPrint + except: + raise exceptions.ImportError, "PyXML is required for using the dom implementation" +#The implementation is used for the creating the XML document. +#The prettyprint module is used for converting the xml document object to a xml file + +import sys +assert sys.version_info[0]>=2 +if sys.version_info[1]<2: + True=1 + False=0 + file=open + +sys.setrecursionlimit=50 +#The recursion limit is set conservative so mistakes like s=svg() s.addElement(s) +#won't eat up too much processor time. + +#the following code is pasted form xml.sax.saxutils +#it makes it possible to run the code without the xml sax package installed +#To make it possible to have in your text elements, it is necessary to escape the texts +def _escape(data, entities={}): + """Escape &, <, and > in a string of data. + + You can escape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + data = data.replace("&", "&") + data = data.replace("<", "<") + data = data.replace(">", ">") + for chars, entity in entities.items(): + data = data.replace(chars, entity) + return data + +def _quoteattr(data, entities={}): + """Escape and quote an attribute value. + + Escape &, <, and > in a string of data, then quote it for use as + an attribute value. The \" character will be escaped as well, if + necessary. + + You can escape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + data = _escape(data, entities) + if '"' in data: + if "'" in data: + data = '"%s"' % data.replace('"', """) + else: + data = "'%s'" % data + else: + data = '"%s"' % data + return data + + + +def _xypointlist(a): + """formats a list of xy pairs""" + s='' + for e in a: #this could be done more elegant + s+=str(e)[1:-1] +' ' + return s + +def _viewboxlist(a): + """formats a tuple""" + s='' + for e in a: + s+=str(e)+' ' + return s + +def _pointlist(a): + """formats a list of numbers""" + return str(a)[1:-1] + +class pathdata: + """class used to create a pathdata object which can be used for a path. + although most methods are pretty straightforward it might be useful to look at the SVG specification.""" + #I didn't test the methods below. + def __init__(self,x=None,y=None): + self.path=[] + if x is not None and y is not None: + self.path.append('M '+str(x)+' '+str(y)) + def closepath(self): + """ends the path""" + self.path.append('z') + def move(self,x,y): + """move to absolute""" + self.path.append('M '+str(x)+' '+str(y)) + def relmove(self,x,y): + """move to relative""" + self.path.append('m '+str(x)+' '+str(y)) + def line(self,x,y): + """line to absolute""" + self.path.append('L '+str(x)+' '+str(y)) + def relline(self,x,y): + """line to relative""" + self.path.append('l '+str(x)+' '+str(y)) + def hline(self,x): + """horizontal line to absolute""" + self.path.append('H'+str(x)) + def relhline(self,x): + """horizontal line to relative""" + self.path.append('h'+str(x)) + def vline(self,y): + """verical line to absolute""" + self.path.append('V'+str(y)) + def relvline(self,y): + """vertical line to relative""" + self.path.append('v'+str(y)) + def bezier(self,x1,y1,x2,y2,x,y): + """bezier with xy1 and xy2 to xy absolut""" + self.path.append('C'+str(x1)+','+str(y1)+' '+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def relbezier(self,x1,y1,x2,y2,x,y): + """bezier with xy1 and xy2 to xy relative""" + self.path.append('c'+str(x1)+','+str(y1)+' '+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def smbezier(self,x2,y2,x,y): + """smooth bezier with xy2 to xy absolut""" + self.path.append('S'+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def relsmbezier(self,x2,y2,x,y): + """smooth bezier with xy2 to xy relative""" + self.path.append('s'+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def qbezier(self,x1,y1,x,y): + """quadratic bezier with xy1 to xy absolut""" + self.path.append('Q'+str(x1)+','+str(y1)+' '+str(x)+','+str(y)) + def relqbezier(self,x1,y1,x,y): + """quadratic bezier with xy1 to xy relative""" + self.path.append('q'+str(x1)+','+str(y1)+' '+str(x)+','+str(y)) + def smqbezier(self,x,y): + """smooth quadratic bezier to xy absolut""" + self.path.append('T'+str(x)+','+str(y)) + def relsmqbezier(self,x,y): + """smooth quadratic bezier to xy relative""" + self.path.append('t'+str(x)+','+str(y)) + def ellarc(self,rx,ry,xrot,laf,sf,x,y): + """elliptival arc with rx and ry rotating with xrot using large-arc-flag and sweep-flag to xy absolut""" + self.path.append('A'+str(rx)+','+str(ry)+' '+str(xrot)+' '+str(laf)+' '+str(sf)+' '+str(x)+' '+str(y)) + def relellarc(self,rx,ry,xrot,laf,sf,x,y): + """elliptival arc with rx and ry rotating with xrot using large-arc-flag and sweep-flag to xy relative""" + self.path.append('a'+str(rx)+','+str(ry)+' '+str(xrot)+' '+str(laf)+' '+str(sf)+' '+str(x)+' '+str(y)) + def __repr__(self): + return ' '.join(self.path) + + + + +class SVGelement: + """SVGelement(type,attributes,elements,text,namespace,**args) + Creates a arbitrary svg element and is intended to be subclassed not used on its own. + This element is the base of every svg element it defines a class which resembles + a xml-element. The main advantage of this kind of implementation is that you don't + have to create a toXML method for every different graph object. Every element + consists of a type, attribute, optional subelements, optional text and an optional + namespace. Note the elements==None, if elements = None:self.elements=[] construction. + This is done because if you default to elements=[] every object has a reference + to the same empty list.""" + def __init__(self,type='',attributes=None,elements=None,text='',namespace='',cdata=None,**args): + self.type=type + if attributes==None: + self.attributes={} + else: + self.attributes=attributes + if elements==None: + self.elements=[] + else: + self.elements=elements + self.text=text + self.namespace=namespace + self.cdata=cdata + for arg in args.keys(): + self.attributes[arg]=args[arg] + def addElement(self,SVGelement): + """adds an element to a SVGelement + + SVGelement.addElement(SVGelement) + """ + self.elements.append(SVGelement) + + #def toXml(self,level,f, preserveWhitespace=False): + def toXml(self,level,f, **kwargs): + preserve = kwargs.get("preserveWhitespace", False) + if preserve: + #print "PRESERVING" + NEWLINE = "" + TAB = "" + else: + #print "NOT PRESE" + NEWLINE = "\n" + TAB = "\t" + f.write(TAB*level) + f.write('<'+self.type) + for attkey in self.attributes.keys(): + f.write(' '+_escape(str(attkey))+'='+_quoteattr(str(self.attributes[attkey]))) + if self.namespace: + f.write(' xmlns="'+ _escape(str(self.namespace))+'" ') + if self.elements or self.text or self.cdata: + f.write('>') + if self.elements: + f.write(NEWLINE) + for element in self.elements: + element.toXml(level+1,f, preserveWhitespace=preserve) + if self.cdata: + f.write(NEWLINE+TAB*(level+1)+''+NEWLINE) + if self.text: + if type(self.text)==type(''): #If the text is only text + f.write(_escape(str(self.text))) + else: #If the text is a spannedtext class + f.write(str(self.text)) + if self.elements: + f.write(TAB*level+''+NEWLINE) + elif self.text: + f.write(''+NEWLINE) + elif self.cdata: + f.write(TAB*level+''+NEWLINE) + else: + f.write('/>'+NEWLINE) + +class tspan(SVGelement): + """ts=tspan(text='',**args) + + a tspan element can be used for applying formatting to a textsection + usage: + ts=tspan('this text is bold') + ts.attributes['font-weight']='bold' + st=spannedtext() + st.addtspan(ts) + t=text(3,5,st) + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'tspan',**args) + if self.text<>None: + self.text=text + def __repr__(self): + s="None: + raise ValueError, 'height is required' + if height<>None: + raise ValueError, 'width is required' + else: + raise ValueError, 'both height and width are required' + SVGelement.__init__(self,'rect',{'width':width,'height':height},**args) + if x<>None: + self.attributes['x']=x + if y<>None: + self.attributes['y']=y + if fill<>None: + self.attributes['fill']=fill + if stroke<>None: + self.attributes['stroke']=stroke + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + +class ellipse(SVGelement): + """e=ellipse(rx,ry,x,y,fill,stroke,stroke_width,**args) + + an ellipse is defined as a center and a x and y radius. + """ + def __init__(self,cx=None,cy=None,rx=None,ry=None,fill=None,stroke=None,stroke_width=None,**args): + if rx==None or ry== None: + if rx<>None: + raise ValueError, 'rx is required' + if ry<>None: + raise ValueError, 'ry is required' + else: + raise ValueError, 'both rx and ry are required' + SVGelement.__init__(self,'ellipse',{'rx':rx,'ry':ry},**args) + if cx<>None: + self.attributes['cx']=cx + if cy<>None: + self.attributes['cy']=cy + if fill<>None: + self.attributes['fill']=fill + if stroke<>None: + self.attributes['stroke']=stroke + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + + +class circle(SVGelement): + """c=circle(x,y,radius,fill,stroke,stroke_width,**args) + + The circle creates an element using a x, y and radius values eg + """ + def __init__(self,cx=None,cy=None,r=None,fill=None,stroke=None,stroke_width=None,**args): + if r==None: + raise ValueError, 'r is required' + SVGelement.__init__(self,'circle',{'r':r},**args) + if cx<>None: + self.attributes['cx']=cx + if cy<>None: + self.attributes['cy']=cy + if fill<>None: + self.attributes['fill']=fill + if stroke<>None: + self.attributes['stroke']=stroke + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + +class point(circle): + """p=point(x,y,color) + + A point is defined as a circle with a size 1 radius. It may be more efficient to use a + very small rectangle if you use many points because a circle is difficult to render. + """ + def __init__(self,x,y,fill='black',**args): + circle.__init__(self,x,y,1,fill,**args) + +class line(SVGelement): + """l=line(x1,y1,x2,y2,stroke,stroke_width,**args) + + A line is defined by a begin x,y pair and an end x,y pair + """ + def __init__(self,x1=None,y1=None,x2=None,y2=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'line',**args) + if x1<>None: + self.attributes['x1']=x1 + if y1<>None: + self.attributes['y1']=y1 + if x2<>None: + self.attributes['x2']=x2 + if y2<>None: + self.attributes['y2']=y2 + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + if stroke<>None: + self.attributes['stroke']=stroke + +class polyline(SVGelement): + """pl=polyline([[x1,y1],[x2,y2],...],fill,stroke,stroke_width,**args) + + a polyline is defined by a list of xy pairs + """ + def __init__(self,points,fill=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'polyline',{'points':_xypointlist(points)},**args) + if fill<>None: + self.attributes['fill']=fill + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + if stroke<>None: + self.attributes['stroke']=stroke + +class polygon(SVGelement): + """pl=polyline([[x1,y1],[x2,y2],...],fill,stroke,stroke_width,**args) + + a polygon is defined by a list of xy pairs + """ + def __init__(self,points,fill=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'polygon',{'points':_xypointlist(points)},**args) + if fill<>None: + self.attributes['fill']=fill + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + if stroke<>None: + self.attributes['stroke']=stroke + +class path(SVGelement): + """p=path(path,fill,stroke,stroke_width,**args) + + a path is defined by a path object and optional width, stroke and fillcolor + """ + def __init__(self,pathdata,fill=None,stroke=None,stroke_width=None,id=None,**args): + SVGelement.__init__(self,'path',{'d':str(pathdata)},**args) + if stroke<>None: + self.attributes['stroke']=stroke + if fill<>None: + self.attributes['fill']=fill + if stroke_width<>None: + self.attributes['stroke-width']=stroke_width + if id<>None: + self.attributes['id']=id + + +class text(SVGelement): + """t=text(x,y,text,font_size,font_family,**args) + + a text element can bge used for displaying text on the screen + """ + def __init__(self,x=None,y=None,text=None,font_size=None,font_family=None,text_anchor=None,**args): + SVGelement.__init__(self,'text',**args) + if x<>None: + self.attributes['x']=x + if y<>None: + self.attributes['y']=y + if font_size<>None: + self.attributes['font-size']=font_size + if font_family<>None: + self.attributes['font-family']=font_family + if text<>None: + self.text=text + if text_anchor<>None: + self.attributes['text-anchor']=text_anchor + + def toXml(self,level,f, **kwargs): + preserve = self.attributes.get("xml:space", None) + if preserve == "preserve": + #print "FOO PRE" + SVGelement.toXml(self,level, f, preserveWhitespace=True) + else: + #print "FOO NOT" + SVGelement.toXml(self, level, f, preserveWhitespace=False) + +class textpath(SVGelement): + """tp=textpath(text,link,**args) + + a textpath places a text on a path which is referenced by a link. + """ + def __init__(self,link,text=None,**args): + SVGelement.__init__(self,'textPath',{'xlink:href':link},**args) + if text<>None: + self.text=text + +class pattern(SVGelement): + """p=pattern(x,y,width,height,patternUnits,**args) + + A pattern is used to fill or stroke an object using a pre-defined + graphic object which can be replicated ("tiled") at fixed intervals + in x and y to cover the areas to be painted. + """ + def __init__(self,x=None,y=None,width=None,height=None,patternUnits=None,**args): + SVGelement.__init__(self,'pattern',**args) + if x<>None: + self.attributes['x']=x + if y<>None: + self.attributes['y']=y + if width<>None: + self.attributes['width']=width + if height<>None: + self.attributes['height']=height + if patternUnits<>None: + self.attributes['patternUnits']=patternUnits + +class title(SVGelement): + """t=title(text,**args) + + a title is a text element. The text is displayed in the title bar + add at least one to the root svg element + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'title',**args) + if text<>None: + self.text=text + +class description(SVGelement): + """d=description(text,**args) + + a description can be added to any element and is used for a tooltip + Add this element before adding other elements. + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'desc',**args) + if text<>None: + self.text=text + +class lineargradient(SVGelement): + """lg=lineargradient(x1,y1,x2,y2,id,**args) + + defines a lineargradient using two xy pairs. + stop elements van be added to define the gradient colors. + """ + def __init__(self,x1=None,y1=None,x2=None,y2=None,id=None,**args): + SVGelement.__init__(self,'linearGradient',**args) + if x1<>None: + self.attributes['x1']=x1 + if y1<>None: + self.attributes['y1']=y1 + if x2<>None: + self.attributes['x2']=x2 + if y2<>None: + self.attributes['y2']=y2 + if id<>None: + self.attributes['id']=id + +class radialgradient(SVGelement): + """rg=radialgradient(cx,cy,r,fx,fy,id,**args) + + defines a radial gradient using a outer circle which are defined by a cx,cy and r and by using a focalpoint. + stop elements van be added to define the gradient colors. + """ + def __init__(self,cx=None,cy=None,r=None,fx=None,fy=None,id=None,**args): + SVGelement.__init__(self,'radialGradient',**args) + if cx<>None: + self.attributes['cx']=cx + if cy<>None: + self.attributes['cy']=cy + if r<>None: + self.attributes['r']=r + if fx<>None: + self.attributes['fx']=fx + if fy<>None: + self.attributes['fy']=fy + if id<>None: + self.attributes['id']=id + +class stop(SVGelement): + """st=stop(offset,stop_color,**args) + + Puts a stop color at the specified radius + """ + def __init__(self,offset,stop_color=None,**args): + SVGelement.__init__(self,'stop',{'offset':offset},**args) + if stop_color<>None: + self.attributes['stop-color']=stop_color + +class style(SVGelement): + """st=style(type,cdata=None,**args) + + Add a CDATA element to this element for defing in line stylesheets etc.. + """ + def __init__(self,type,cdata=None,**args): + SVGelement.__init__(self,'style',{'type':type},cdata=cdata, **args) + + +class image(SVGelement): + """im=image(url,width,height,x,y,**args) + + adds an image to the drawing. Supported formats are .png, .jpg and .svg. + """ + def __init__(self,url,x=None,y=None,width=None,height=None,**args): + if width==None or height==None: + if width<>None: + raise ValueError, 'height is required' + if height<>None: + raise ValueError, 'width is required' + else: + raise ValueError, 'both height and width are required' + SVGelement.__init__(self,'image',{'xlink:href':url,'width':width,'height':height},**args) + if x<>None: + self.attributes['x']=x + if y<>None: + self.attributes['y']=y + +class cursor(SVGelement): + """c=cursor(url,**args) + + defines a custom cursor for a element or a drawing + """ + def __init__(self,url,**args): + SVGelement.__init__(self,'cursor',{'xlink:href':url},**args) + + +class marker(SVGelement): + """m=marker(id,viewbox,refX,refY,markerWidth,markerHeight,**args) + + defines a marker which can be used as an endpoint for a line or other pathtypes + add an element to it which should be used as a marker. + """ + def __init__(self,id=None,viewBox=None,refx=None,refy=None,markerWidth=None,markerHeight=None,**args): + SVGelement.__init__(self,'marker',**args) + if id<>None: + self.attributes['id']=id + if viewBox<>None: + self.attributes['viewBox']=_viewboxlist(viewBox) + if refx<>None: + self.attributes['refX']=refx + if refy<>None: + self.attributes['refY']=refy + if markerWidth<>None: + self.attributes['markerWidth']=markerWidth + if markerHeight<>None: + self.attributes['markerHeight']=markerHeight + +class group(SVGelement): + """g=group(id,**args) + + a group is defined by an id and is used to contain elements + g.addElement(SVGelement) + """ + def __init__(self,id=None,**args): + SVGelement.__init__(self,'g',**args) + if id<>None: + self.attributes['id']=id + +class symbol(SVGelement): + """sy=symbol(id,viewbox,**args) + + defines a symbol which can be used on different places in your graph using + the use element. A symbol is not rendered but you can use 'use' elements to + display it by referencing its id. + sy.addElement(SVGelement) + """ + + def __init__(self,id=None,viewBox=None,**args): + SVGelement.__init__(self,'symbol',**args) + if id<>None: + self.attributes['id']=id + if viewBox<>None: + self.attributes['viewBox']=_viewboxlist(viewBox) + +class defs(SVGelement): + """d=defs(**args) + + container for defining elements + """ + def __init__(self,**args): + SVGelement.__init__(self,'defs',**args) + +class switch(SVGelement): + """sw=switch(**args) + + Elements added to a switch element which are "switched" by the attributes + requiredFeatures, requiredExtensions and systemLanguage. + Refer to the SVG specification for details. + """ + def __init__(self,**args): + SVGelement.__init__(self,'switch',**args) + + +class use(SVGelement): + """u=use(link,x,y,width,height,**args) + + references a symbol by linking to its id and its position, height and width + """ + def __init__(self,link,x=None,y=None,width=None,height=None,**args): + SVGelement.__init__(self,'use',{'xlink:href':link},**args) + if x<>None: + self.attributes['x']=x + if y<>None: + self.attributes['y']=y + + if width<>None: + self.attributes['width']=width + if height<>None: + self.attributes['height']=height + + +class link(SVGelement): + """a=link(url,**args) + + a link is defined by a hyperlink. add elements which have to be linked + a.addElement(SVGelement) + """ + def __init__(self,link='',**args): + SVGelement.__init__(self,'a',{'xlink:href':link},**args) + +class view(SVGelement): + """v=view(id,**args) + + a view can be used to create a view with different attributes""" + def __init__(self,id=None,**args): + SVGelement.__init__(self,'view',**args) + if id<>None: + self.attributes['id']=id + +class script(SVGelement): + """sc=script(type,type,cdata,**args) + + adds a script element which contains CDATA to the SVG drawing + + """ + def __init__(self,type,cdata=None,**args): + SVGelement.__init__(self,'script',{'type':type},cdata=cdata,**args) + +class animate(SVGelement): + """an=animate(attribute,from,to,during,**args) + + animates an attribute. + """ + def __init__(self,attribute,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animate',{'attributeName':attribute},**args) + if fr<>None: + self.attributes['from']=fr + if to<>None: + self.attributes['to']=to + if dur<>None: + self.attributes['dur']=dur + +class animateMotion(SVGelement): + """an=animateMotion(pathdata,dur,**args) + + animates a SVGelement over the given path in dur seconds + """ + def __init__(self,pathdata,dur,**args): + SVGelement.__init__(self,'animateMotion',**args) + if pathdata<>None: + self.attributes['path']=str(pathdata) + if dur<>None: + self.attributes['dur']=dur + +class animateTransform(SVGelement): + """antr=animateTransform(type,from,to,dur,**args) + + transform an element from and to a value. + """ + def __init__(self,type=None,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animateTransform',{'attributeName':'transform'},**args) + #As far as I know the attributeName is always transform + if type<>None: + self.attributes['type']=type + if fr<>None: + self.attributes['from']=fr + if to<>None: + self.attributes['to']=to + if dur<>None: + self.attributes['dur']=dur +class animateColor(SVGelement): + """ac=animateColor(attribute,type,from,to,dur,**args) + + Animates the color of a element + """ + def __init__(self,attribute,type=None,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animateColor',{'attributeName':attribute},**args) + if type<>None: + self.attributes['type']=type + if fr<>None: + self.attributes['from']=fr + if to<>None: + self.attributes['to']=to + if dur<>None: + self.attributes['dur']=dur +class set(SVGelement): + """st=set(attribute,to,during,**args) + + sets an attribute to a value for a + """ + def __init__(self,attribute,to=None,dur=None,**args): + SVGelement.__init__(self,'set',{'attributeName':attribute},**args) + if to<>None: + self.attributes['to']=to + if dur<>None: + self.attributes['dur']=dur + + + +class svg(SVGelement): + """s=svg(viewbox,width,height,**args) + + a svg or element is the root of a drawing add all elements to a svg element. + You can have different svg elements in one svg file + s.addElement(SVGelement) + + eg + d=drawing() + s=svg((0,0,100,100),'100%','100%') + c=circle(50,50,20) + s.addElement(c) + d.setSVG(s) + d.toXml() + """ + def __init__(self,viewBox=None, width=None, height=None,**args): + SVGelement.__init__(self,'svg',**args) + if viewBox<>None: + self.attributes['viewBox']=_viewboxlist(viewBox) + if width<>None: + self.attributes['width']=width + if height<>None: + self.attributes['height']=height + self.namespace="http://www.w3.org/2000/svg" + +class drawing: + """d=drawing() + + this is the actual SVG document. It needs a svg element as a root. + Use the addSVG method to set the svg to the root. Use the toXml method to write the SVG + source to the screen or to a file + d=drawing() + d.addSVG(svg) + d.toXml(optionalfilename) + """ + + def __init__(self): + self.svg=None + def setSVG(self,svg): + self.svg=svg + #Voeg een element toe aan de grafiek toe. + if use_dom_implementation==0: + def toXml(self, filename='',compress=False): + import cStringIO + xml=cStringIO.StringIO() + xml.write('\n') + xml.write("""]>\n""") + self.svg.toXml(0,xml) + if not filename: + if compress: + import gzip + f=cStringIO.StringIO() + zf=gzip.GzipFile(fileobj=f,mode='wb') + zf.write(xml.getvalue()) + zf.close() + f.seek(0) + return f.read() + else: + return xml.getvalue() + else: + if filename[-4:]=='svgz': + import gzip + f=gzip.GzipFile(filename=filename,mode="wb", compresslevel=9) + f.write(xml.getvalue()) + f.close() + else: + f=file(filename,'w') + f.write(xml.getvalue()) + f.close() + + else: + def toXml(self,filename='',compress=False): + """drawing.toXml() ---->to the screen + drawing.toXml(filename)---->to the file + writes a svg drawing to the screen or to a file + compresses if filename ends with svgz or if compress is true + """ + doctype = implementation.createDocumentType('svg',"-//W3C//DTD SVG 1.0//EN""",'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ') + + global root + #root is defined global so it can be used by the appender. Its also possible to use it as an arugument but + #that is a bit messy. + root=implementation.createDocument(None,None,doctype) + #Create the xml document. + global appender + def appender(element,elementroot): + """This recursive function appends elements to an element and sets the attributes + and type. It stops when alle elements have been appended""" + if element.namespace: + e=root.createElementNS(element.namespace,element.type) + else: + e=root.createElement(element.type) + if element.text: + textnode=root.createTextNode(element.text) + e.appendChild(textnode) + for attribute in element.attributes.keys(): #in element.attributes is supported from python 2.2 + e.setAttribute(attribute,str(element.attributes[attribute])) + if element.elements: + for el in element.elements: + e=appender(el,e) + elementroot.appendChild(e) + return elementroot + root=appender(self.svg,root) + if not filename: + import cStringIO + xml=cStringIO.StringIO() + PrettyPrint(root,xml) + if compress: + import gzip + f=cStringIO.StringIO() + zf=gzip.GzipFile(fileobj=f,mode='wb') + zf.write(xml.getvalue()) + zf.close() + f.seek(0) + return f.read() + else: + return xml.getvalue() + else: + try: + if filename[-4:]=='svgz': + import gzip + import cStringIO + xml=cStringIO.StringIO() + PrettyPrint(root,xml) + f=gzip.GzipFile(filename=filename,mode='wb',compresslevel=9) + f.write(xml.getvalue()) + f.close() + else: + f=open(filename,'w') + PrettyPrint(root,f) + f.close() + except: + print "Cannot write SVG file: " + filename + def validate(self): + try: + import xml.parsers.xmlproc.xmlval + except: + raise exceptions.ImportError,'PyXml is required for validating SVG' + svg=self.toXml() + xv=xml.parsers.xmlproc.xmlval.XMLValidator() + try: + xv.feed(svg) + except: + raise "SVG is not well formed, see messages above" + else: + print "SVG well formed" +if __name__=='__main__': + + + d=drawing() + s=svg((0,0,100,100)) + r=rect(-100,-100,300,300,'cyan') + s.addElement(r) + + t=title('SVGdraw Demo') + s.addElement(t) + g=group('animations') + e=ellipse(0,0,5,2) + g.addElement(e) + c=circle(0,0,1,'red') + g.addElement(c) + pd=pathdata(0,-10) + for i in range(6): + pd.relsmbezier(10,5,0,10) + pd.relsmbezier(-10,5,0,10) + an=animateMotion(pd,10) + an.attributes['rotate']='auto-reverse' + an.attributes['repeatCount']="indefinite" + g.addElement(an) + s.addElement(g) + for i in range(20,120,20): + u=use('#animations',i,0) + s.addElement(u) + for i in range(0,120,20): + for j in range(5,105,10): + c=circle(i,j,1,'red','black',.5) + s.addElement(c) + d.setSVG(s) + + print d.toXml() + diff --git a/obitools/__init__.py b/obitools/__init__.py new file mode 100644 index 0000000..3063d78 --- /dev/null +++ b/obitools/__init__.py @@ -0,0 +1,711 @@ +''' +**obitools** main module +------------------------ + +.. codeauthor:: Eric Coissac + + + +obitools module provides base class for sequence manipulation. + +All biological sequences must be subclass of :py:class:`obitools.BioSequence`. +Some biological sequences are defined as transformation of other +biological sequences. For example Reversed complemented sequences +are a transformation of a :py:class:`obitools.NucSequence`. This particular +type of sequences are subclasses of the :py:class:`obitools.WrappedBioSequence`. + +.. inheritance-diagram:: BioSequence NucSequence AASequence WrappedBioSequence SubSequence DNAComplementSequence + :parts: 1 + + +''' + +from weakref import ref + +from obitools.utils.iterator import uniqueChain +from itertools import chain +import re + +_default_raw_parser = " %s *= *([^;]*);" + +try: + from functools import partial +except: + # + # Add for compatibility purpose with Python < 2.5 + # + def partial(func, *args, **keywords): + def newfunc(*fargs, **fkeywords): + newkeywords = keywords.copy() + newkeywords.update(fkeywords) + return func(*(args + fargs), **newkeywords) + newfunc.func = func + newfunc.args = args + newfunc.keywords = keywords + return newfunc + + +from obitools.sequenceencoder import DNAComplementEncoder +from obitools.location import Location + +class WrapperSetIterator(object): + def __init__(self,s): + self._i = set.__iter__(s) + def next(self): + return self._i.next()() + def __iter__(self): + return self + +class WrapperSet(set): + def __iter__(self): + return WrapperSetIterator(self) + + +class BioSequence(object): + ''' + BioSequence class is the base class for biological + sequence representation. + + It provides storage of : + + - the sequence itself, + - an identifier, + - a definition an manage + - a set of complementary information on a key / value principle. + + .. warning:: + + :py:class:`obitools.BioSequence` is an abstract class, this constructor + can only be called by a subclass constructor. + ''' + + def __init__(self,id,seq,definition=None,rawinfo=None,rawparser=_default_raw_parser,**info): + ''' + + :param id: sequence identifier + :type id: `str` + + :param seq: the sequence + :type seq: `str` + + :param definition: sequence definition (optional) + :type definition: `str` + + :param rawinfo: a text containing a set of key=value; patterns + :type definition: `str` + + :param rawparser: a text describing a regular patterns template + used to parse rawinfo + :type definition: `str` + + :param info: extra named parameters can be added to associate complementary + data to the sequence + + ''' + + assert type(self)!=BioSequence,"obitools.BioSequence is an abstract class" + + self._seq=str(seq).lower() + self._info = dict(info) + if rawinfo is not None: + self._rawinfo=' ' + rawinfo + else: + self._rawinfo=None + self._rawparser=rawparser + self.definition=definition + self.id=id + self._hasTaxid=None + + def get_seq(self): + return self.__seq + + + def set_seq(self, value): + if not isinstance(value, str): + value=str(value) + self.__seq = value + self.__len = len(value) + + + def clone(self): + seq = type(self)(self.id, + str(self), + definition=self.definition + ) + seq._info=dict(self.getTags()) + seq._rawinfo=self._rawinfo + seq._rawparser=self._rawparser + seq._hasTaxid=self._hasTaxid + return seq + + def getDefinition(self): + ''' + Sequence definition getter. + + :return: the sequence definition + :rtype: str + + ''' + return self._definition + + def setDefinition(self, value): + ''' + Sequence definition setter. + + :param value: the new sequence definition + :type value: C{str} + :return: C{None} + ''' + self._definition = value + + def getId(self): + ''' + Sequence identifier getter + + :return: the sequence identifier + :rtype: C{str} + ''' + return self._id + + def setId(self, value): + ''' + Sequence identifier setter. + + :param value: the new sequence identifier + :type value: C{str} + :return: C{None} + ''' + self._id = value + + def getStr(self): + ''' + Return the sequence as a string + + :return: the string representation of the sequence + :rtype: str + ''' + return self._seq + + def getSymbolAt(self,position): + ''' + Return the symbole at C{position} in the sequence + + :param position: the desired position. Position start from 0 + if position is < 0 then they are considered + to reference the end of the sequence. + :type position: `int` + + :return: a one letter string + :rtype: `str` + ''' + return str(self)[position] + + def getSubSeq(self,location): + ''' + return a subsequence as described by C{location}. + + The C{location} parametter can be a L{obitools.location.Location} instance, + an interger or a python C{slice} instance. If C{location} + is an iterger this method is equivalent to L{getSymbolAt}. + + :param location: the positions of the subsequence to return + :type location: C{Location} or C{int} or C{slice} + :return: the subsequence + :rtype: a single character as a C{str} is C{location} is an integer, + a L{obitools.SubSequence} instance otherwise. + + ''' + if isinstance(location,Location): + return location.extractSequence(self) + elif isinstance(location, int): + return self.getSymbolAt(location) + elif isinstance(location, slice): + return SubSequence(self,location) + + raise TypeError,'key must be a Location, an integer or a slice' + + def getKey(self,key): + if key not in self._info: + if self._rawinfo is None: + if key=='count': + return 1 + else: + raise KeyError,key + p = re.compile(self._rawparser % key) + m = p.search(self._rawinfo) + if m is not None: + v=m.group(1) + self._rawinfo=' ' + self._rawinfo[0:m.start(0)]+self._rawinfo[m.end(0):] + try: + v = eval(v) + except: + pass + self._info[key]=v + else: + if key=='count': + v=1 + else: + raise KeyError,key + else: + v=self._info[key] + return v + + def extractTaxon(self): + ''' + Extract Taxonomy information from the sequence header. + This method by default return None. It should be subclassed + if necessary as in L{obitools.seqdb.AnnotatedSequence}. + + :return: None + ''' + self._hasTaxid=self.hasKey('taxid') + return None + + def __str__(self): + return self.getStr() + + def __getitem__(self,key): + if isinstance(key, str): + if key=='taxid' and self._hasTaxid is None: + self.extractTaxon() + return self.getKey(key) + else: + return self.getSubSeq(key) + + def __setitem__(self,key,value): + self._info[key]=value + if key=='taxid': + self._hasTaxid=value is not None + + def __delitem__(self,key): + if isinstance(key, str): + if key in self: + del self._info[key] + else: + raise KeyError,key + + if key=='taxid': + self._hasTaxid=False + else: + raise TypeError,key + + def __iter__(self): + ''' + Iterate through the sequence symbols + ''' + return iter(str(self)) + + def __len__(self): + return self.__len + + def hasKey(self,key): + rep = key in self._info + + if not rep and self._rawinfo is not None: + p = re.compile(self._rawparser % key) + m = p.search(self._rawinfo) + if m is not None: + v=m.group(1) + self._rawinfo=' ' + self._rawinfo[0:m.start(0)]+self._rawinfo[m.end(0):] + try: + v = eval(v) + except: + pass + self._info[key]=v + rep=True + + return rep + + def __contains__(self,key): + ''' + methods allowing to use the C{in} operator on a C{BioSequence}. + + The C{in} operator test if the C{key} value is defined for this + sequence. + + :param key: the name of the checked value + :type key: str + :return: C{True} if the value is defined, {False} otherwise. + :rtype: C{bool} + ''' + if key=='taxid' and self._hasTaxid is None: + self.extractTaxon() + return self.hasKey(key) + + def rawiteritems(self): + return self._info.iteritems() + + def iteritems(self): + ''' + iterate other items dictionary storing the values + associated to the sequence. It works similarly to + the iteritems function of C{dict}. + + :return: an iterator over the items (key,value) + link to a sequence + :rtype: iterator over tuple + :see: L{items} + ''' + if self._rawinfo is not None: + p = re.compile(self._rawparser % "([a-zA-Z]\w*)") + for k,v in p.findall(self._rawinfo): + try: + self._info[k]=eval(v) + except: + self._info[k]=v + self._rawinfo=None + return self._info.iteritems() + + def items(self): + return [x for x in self.iteritems()] + + def iterkeys(self): + return (k for k,v in self.iteritems()) + + def keys(self): + return [x for x in self.iterkeys()] + + def getTags(self): + self.iteritems() + return self._info + + def getRoot(self): + return self + + def getWrappers(self): + if not hasattr(self, '_wrappers'): + self._wrappers=WrapperSet() + return self._wrappers + + def register(self,wrapper): + self.wrappers.add(ref(wrapper,self._unregister)) + + def _unregister(self,ref): + self.wrappers.remove(ref) + + wrappers = property(getWrappers,None,None,'') + + definition = property(getDefinition, setDefinition, None, "Sequence Definition") + + id = property(getId, setId, None, 'Sequence identifier') + + def _getTaxid(self): + return self['taxid'] + + def _setTaxid(self,taxid): + self['taxid']=taxid + + taxid = property(_getTaxid,_setTaxid,None,'NCBI Taxonomy identifier') + _seq = property(get_seq, set_seq, None, None) + +class NucSequence(BioSequence): + """ + :py:class:`NucSequence` specialize the :py:class:`BioSequence` class for storing DNA + sequences. + + The constructor is identical to the :py:class:`BioSequence` constructor. + """ + + def complement(self): + """ + :return: The reverse complemented sequence as an instance of :py:class:`DNAComplementSequence` + :rtype: :py:class:`DNAComplementSequence` + """ + return DNAComplementSequence(self) + + def isNucleotide(self): + return True + + +class AASequence(BioSequence): + """ + :py:class:`AASequence` specialize the :py:class:`BioSequence` class for storing protein + sequences. + + The constructor is identical to the :py:class:`BioSequence` constructor. + """ + + + def isNucleotide(self): + return False + + +class WrappedBioSequence(BioSequence): + """ + .. warning:: + + :py:class:`obitools.WrappedBioSequence` is an abstract class, this constructor + can only be called by a subclass constructor. + """ + + + def __init__(self,reference,id=None,definition=None,**info): + + assert type(self)!=WrappedBioSequence,"obitools.WrappedBioSequence is an abstract class" + + self._wrapped = reference + reference.register(self) + self._id=id + self.definition=definition + self._info=info + + def clone(self): + seq = type(self)(self.wrapped, + id=self._id, + definition=self._definition + ) + seq._info=dict(self._info) + + return seq + + def getWrapped(self): + return self._wrapped + + def getDefinition(self): + d = self._definition or self.wrapped.definition + return d + + def getId(self): + d = self._id or self.wrapped.id + return d + + def isNucleotide(self): + return self.wrapped.isNucleotide() + + + def iterkeys(self): + return uniqueChain(self._info.iterkeys(), + self.wrapped.iterkeys()) + + def rawiteritems(self): + return chain(self._info.iteritems(), + (x for x in self.wrapped.rawiteritems() + if x[0] not in self._info)) + + def iteritems(self): + for x in self.iterkeys(): + yield (x,self[x]) + + def getKey(self,key): + if key in self._info: + return self._info[key] + else: + return self.wrapped.getKey(key) + + def hasKey(self,key): + return key in self._info or self.wrapped.hasKey(key) + + def getSymbolAt(self,position): + return self.wrapped.getSymbolAt(self.posInWrapped(position)) + + def posInWrapped(self,position,reference=None): + if reference is None or reference is self.wrapped: + return self._posInWrapped(position) + else: + return self.wrapped.posInWrapped(self._posInWrapped(position),reference) + + + def getStr(self): + return str(self.wrapped) + + def getRoot(self): + return self.wrapped.getRoot() + + def complement(self): + """ + The :py:meth:`complement` method of the :py:class:`WrappedBioSequence` class + raises an exception :py:exc:`AttributeError` if the method is called and the cut + sequence does not corresponds to a nucleic acid sequence. + """ + + if self.wrapped.isNucleotide(): + return DNAComplementSequence(self) + raise AttributeError + + + def _posInWrapped(self,position): + return position + + + definition = property(getDefinition,BioSequence.setDefinition, None) + id = property(getId,BioSequence.setId, None) + + wrapped = property(getWrapped, None, None, "A pointer to the wrapped sequence") + + def _getWrappedRawInfo(self): + return self.wrapped._rawinfo + + _rawinfo = property(_getWrappedRawInfo) + + +class SubSequence(WrappedBioSequence): + """ + """ + + + @staticmethod + def _sign(x): + if x == 0: + return 0 + elif x < 0: + return -1 + return 1 + + def __init__(self,reference, + location=None, + start=None,stop=None, + id=None,definition=None, + **info): + WrappedBioSequence.__init__(self,reference,id=None,definition=None,**info) + + if isinstance(location, slice): + self._location = location + else: + step = 1 + if not isinstance(start, int): + start = 0; + if not isinstance(stop,int): + stop = len(reference) + self._location=slice(start,stop,step) + + self._indices=self._location.indices(len(self.wrapped)) + self._xrange=xrange(*self._indices) + + self._info['cut']='[%d,%d,%s]' % self._indices + + if hasattr(reference,'quality'): + self.quality = reference.quality[self._location] + + def getId(self): + d = self._id or ("%s_SUB" % self.wrapped.id) + return d + + + def clone(self): + seq = WrappedBioSequence.clone(self) + seq._location=self._location + seq._indices=seq._location.indices(len(seq.wrapped)) + seq._xrange=xrange(*seq._indices) + return seq + + + def __len__(self): + return len(self._xrange) + + def getStr(self): + return ''.join([x for x in self]) + + def __iter__(self): + return (self.wrapped.getSymbolAt(x) for x in self._xrange) + + def _posInWrapped(self,position): + return self._xrange[position] + + + id = property(getId,BioSequence.setId, None) + + + +class DNAComplementSequence(WrappedBioSequence): + """ + Class used to represent a reverse complemented DNA sequence. Usually instances + of this class are produced by using the :py:meth:`NucSequence.complement` method. + """ + + + _comp={'a': 't', 'c': 'g', 'g': 'c', 't': 'a', + 'r': 'y', 'y': 'r', 'k': 'm', 'm': 'k', + 's': 's', 'w': 'w', 'b': 'v', 'd': 'h', + 'h': 'd', 'v': 'b', 'n': 'n', 'u': 'a', + '-': '-'} + + + def __init__(self,reference, + id=None,definition=None,**info): + WrappedBioSequence.__init__(self,reference,id=None,definition=None,**info) + assert reference.isNucleotide() + self._info['complemented']=True + if hasattr(reference,'quality'): + self.quality = reference.quality[::-1] + + + def getId(self): + d = self._id or ("%s_CMP" % self.wrapped.id) + return d + + def __len__(self): + return len(self._wrapped) + + def getStr(self): + return ''.join([x for x in self]) + + def __iter__(self): + return (self.getSymbolAt(x) for x in xrange(len(self))) + + def _posInWrapped(self,position): + return -(position+1) + + def getSymbolAt(self,position): + return DNAComplementSequence._comp[self.wrapped.getSymbolAt(self.posInWrapped(position))] + + def complement(self): + """ + The :py:meth:`complement` method of the :py:class:`DNAComplementSequence` class actually + returns the wrapped sequenced. Effectively the reversed complemented sequence of a reversed + complemented sequence is the initial sequence. + """ + return self.wrapped + + id = property(getId,BioSequence.setId, None) + + +def _isNucSeq(text): + acgt = 0 + notnuc = 0 + ltot = len(text) * 0.8 + for c in text.lower(): + if c in 'acgt-': + acgt+=1 + if c not in DNAComplementEncoder._comp: + notnuc+=1 + return notnuc==0 and float(acgt) > ltot + + +def bioSeqGenerator(id,seq,definition=None,rawinfo=None,rawparser=_default_raw_parser,**info): + """ + Generate automagically the good class instance between : + + - :py:class:`NucSequence` + - :py:class:`AASequence` + + Build a new sequence instance. Sequences are instancied as :py:class:`NucSequence` if the + `seq` attribute contains more than 80% of *A*, *C*, *G*, *T* or *-* symbols + in upper or lower cases. Conversely, the new sequence instance is instancied as + :py:class:`AASequence`. + + + + :param id: sequence identifier + :type id: `str` + + :param seq: the sequence + :type seq: `str` + + :param definition: sequence definition (optional) + :type definition: `str` + + :param rawinfo: a text containing a set of key=value; patterns + :type definition: `str` + + :param rawparser: a text describing a regular patterns template + used to parse rawinfo + :type definition: `str` + + :param info: extra named parameters can be added to associate complementary + data to the sequence + """ + if _isNucSeq(seq): + return NucSequence(id,seq,definition,rawinfo,rawparser,**info) + else: + return AASequence(id,seq,definition,rawinfo,rawparser,**info) + diff --git a/obitools/__init__.pyc b/obitools/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cc211121b11edecd9906b392c67290f33ac3fa3 GIT binary patch literal 32071 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 literal 0 HcmV?d00001 diff --git a/obitools/align/__init__.py b/obitools/align/__init__.py new file mode 100644 index 0000000..54cca7d --- /dev/null +++ b/obitools/align/__init__.py @@ -0,0 +1,13 @@ + + +from _nws import NWS +from _upperbond import indexSequences +from _lcs import LCS,lenlcs +from _assemble import DirectAssemble, ReverseAssemble +from _qsassemble import QSolexaDirectAssemble,QSolexaReverseAssemble +from _rassemble import RightDirectAssemble as RightReverseAssemble +from _qsrassemble import QSolexaRightDirectAssemble,QSolexaRightReverseAssemble +from _freeendgap import FreeEndGap +from _freeendgapfm import FreeEndGapFullMatch +from _upperbond import isLCSReachable + diff --git a/obitools/align/_assemble.so b/obitools/align/_assemble.so new file mode 100755 index 0000000000000000000000000000000000000000..dbc21392951d34af27abccc787a93f532899cc38 GIT binary patch literal 78032 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 literal 0 HcmV?d00001 diff --git a/obitools/align/_freeendgapfm.so b/obitools/align/_freeendgapfm.so new file mode 100755 index 0000000000000000000000000000000000000000..f88c07b04afc5e0607152461b89c0ca14198e1ac GIT binary patch literal 53936 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 U;FppM%jvdxBsroyB-fPx1#-${qW}N^ literal 0 HcmV?d00001 diff --git a/obitools/align/_lcs.so b/obitools/align/_lcs.so new file mode 100755 index 0000000000000000000000000000000000000000..555a2a2a57b30b377543e37ebd00be6b506f6fc1 GIT binary patch literal 147472 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%0D?*&Drn%J*o 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

jZUlD597abIkym80ou#zWB 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 literal 0 HcmV?d00001 diff --git a/obitools/align/_nws.so b/obitools/align/_nws.so new file mode 100755 index 0000000000000000000000000000000000000000..af7e849783bad15c44c54563ded1ab401726fab9 GIT binary patch literal 72308 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 literal 0 HcmV?d00001 diff --git a/obitools/align/_qsassemble.so b/obitools/align/_qsassemble.so new file mode 100755 index 0000000000000000000000000000000000000000..3bc83e98feba258e553d4ecb3870ff20c40a437b GIT binary patch literal 91624 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

QofpqzcemyB;qP)`FDx<){yca6QvQc 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 literal 0 HcmV?d00001 diff --git a/obitools/align/_qsrassemble.so b/obitools/align/_qsrassemble.so new file mode 100755 index 0000000000000000000000000000000000000000..75b98aa5882fe92adb86f439b218e456fb483bbd GIT binary patch literal 92072 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 z�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 Vn=fd?%6vf^*5waclrKI!`oHeM#s~la literal 0 HcmV?d00001 diff --git a/obitools/align/_upperbond.so b/obitools/align/_upperbond.so new file mode 100755 index 0000000000000000000000000000000000000000..5f2b1fe0d4cade55e57551a287b0e21f5aa05b53 GIT binary patch literal 105440 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[^ ]+)", re.MULTILINE) + _matchLQuery = re.compile("^Query:.+\n.+?(\d+)(?= nt\n)", re.MULTILINE) + _matchProp = re.compile("^The best scores are:.*\n(.+?)>>>", re.DOTALL+re.MULTILINE) + def __init__(self,file): + if isinstance(file,str): + file = open(file,'rU') + self.data = file.read() + self.query= SsearchParser._matchQuery.search(self.data).group(1) + self.queryLength= int(SsearchParser._matchLQuery.search(self.data).group(1)) + props = SsearchParser._matchProp.search(self.data) + if props: + props=props.group(0).split('\n')[1:-2] + self.props=[] + for line in props: + subject,tab = line.split('\t') + tab=tab.split() + ssp = subject.split() + ac = ssp[0] + dbl= int(ssp[-5][:-1]) + ident = float(tab[0]) + matchlen = abs(int(tab[5]) - int(tab[4])) +1 + self.props.append({"ac" :ac, + "identity" :ident, + "subjectlength":dbl, + 'matchlength' : matchlen}) + +def run(seq,database,program='fasta35',opts=''): + ssearchin,ssearchout,ssearcherr = os.popen3("%s %s %s" % (program,opts,database)) + print >>ssearchin,formatFasta(seq) + ssearchin.close() + result = SsearchParser(ssearchout) + + return seq,result + +def ssearchIterator(sequenceIterator,database,program='ssearch35',opts=''): + for seq in sequenceIterator: + yield run(seq,database,program,opts) + + diff --git a/obitools/alignment/__init__.py b/obitools/alignment/__init__.py new file mode 100644 index 0000000..a89793a --- /dev/null +++ b/obitools/alignment/__init__.py @@ -0,0 +1,175 @@ +from obitools import BioSequence +from obitools import WrappedBioSequence +from copy import deepcopy + +class GappedPositionException(Exception): + pass + +class AlignedSequence(WrappedBioSequence): + + def __init__(self,reference, + id=None,definition=None,**info): + WrappedBioSequence.__init__(self,reference,id=None,definition=None,**info) + self._length=len(reference) + self._gaps=[[self._length,0]] + + def clone(self): + seq = WrappedBioSequence.clone(self) + seq._gaps=deepcopy(self._gaps) + seq._length=reduce(lambda x,y:x+y, (z[0]+z[1] for z in self._gaps),0) + return seq + + def setGaps(self, value): + ''' + Set gap vector to an AlignedSequence. + + Gap vector describes the gap positions on a sequence. + It is a gap of couple. The first couple member is the count + of sequence letter, the second one is the gap length. + @param value: a list of length 2 list describing gap positions + @type value: list of couple + ''' + assert isinstance(value, list),'Gap vector must be a list' + assert reduce(lambda x,y: x and y, + (isinstance(z, list) and len(z)==2 for z in value), + True),"Value must be a list of length 2 list" + + lseq = reduce(lambda x,y:x+y, (z[0] for z in value),0) + assert lseq==len(self.wrapped),"Gap vector incompatible with the sequence" + self._gaps = value + self._length=reduce(lambda x,y:x+y, (z[0]+z[1] for z in value),0) + + def getGaps(self): + return tuple(self._gaps) + gaps = property(getGaps, setGaps, None, "Gaps's Docstring") + + def _getIndice(self,pos): + i=0 + cpos=0 + for s,g in self._gaps: + cpos+=s + if cpos>pos: + return i,pos-cpos+s + cpos+=g + if cpos>pos: + return i,-pos+cpos-g-1 + i+=1 + raise IndexError + + def getId(self): + d = self._id or ("%s_ALN" % self.wrapped.id) + return d + + def __len__(self): + return self._length + + def getStr(self): + return ''.join([x for x in self]) + + def __iter__(self): + def isymb(): + cpos=0 + for s,g in self._gaps: + for x in xrange(s): + yield self.wrapped[cpos+x] + for x in xrange(g): + yield '-' + cpos+=s + return isymb() + + def _posInWrapped(self,position): + i,s=self._getIndice(position) + if s<0: + raise GappedPositionException + value=self._gaps + p=reduce(lambda x,y:x+y, (z[0] for z in value[:i]),0)+s + return p + + def getSymbolAt(self,position): + try: + return self.wrapped.getSymbolAt(self.posInWrapped(position)) + except GappedPositionException: + return '-' + + def insertGap(self,position,count=1): + if position==self._length: + idx=len(self._gaps)-1 + p=-1 + else: + idx,p = self._getIndice(position) + + if p >= 0: + self._gaps.insert(idx, [p,count]) + self._gaps[idx+1][0]-=p + else: + self._gaps[idx][1]+=count + self._length=reduce(lambda x,y:x+y, (z[0]+z[1] for z in self._gaps),0) + + + id = property(getId,BioSequence.setId, None, "Sequence Identifier") + + +class Alignment(list): + + def _assertData(self,data): + assert isinstance(data, BioSequence),'You must only add bioseq to an alignement' + if hasattr(self, '_alignlen'): + assert self._alignlen==len(data),'All aligned sequences must have the same length' + else: + self._alignlen=len(data) + return data + + def clone(self): + ali = Alignment(x.clone() for x in self) + return ali + + def append(self,data): + data = self._assertData(data) + list.append(self,data) + + def __setitem__(self,index,data): + + data = self._assertData(data) + list.__setitem__(self,index,data) + + def getSite(self,key): + if isinstance(key,int): + return [x[key] for x in self] + + def insertGap(self,position,count=1): + for s in self: + s.insertGap(position,count) + + def isFullGapSite(self,key): + return reduce(lambda x,y: x and y,(z=='-' for z in self.getSite(key)),True) + + def isGappedSite(self,key): + return '-' in self.getSite(key) + + def __str__(self): + l = len(self[0]) + rep="" + idmax = max(len(x.id) for x in self)+2 + template= "%%-%ds %%-60s" % idmax + for p in xrange(0,l,60): + for s in self: + rep+= (template % (s.id,s[p:p+60])).strip() + '\n' + rep+="\n" + return rep + +def alignmentReader(file,sequenceIterator): + seqs = sequenceIterator(file) + alignement = Alignment() + for seq in seqs: + alignement.append(seq) + return alignement + + + + + +def columnIterator(alignment): + lali = len(alignment[0]) + for p in xrange(lali): + c = [x[p] for x in alignment] + yield c \ No newline at end of file diff --git a/obitools/alignment/ace.py b/obitools/alignment/ace.py new file mode 100644 index 0000000..59cc8f6 --- /dev/null +++ b/obitools/alignment/ace.py @@ -0,0 +1,47 @@ +from obitools.format.genericparser import GenericParser +from obitools.utils import universalOpen +from obitools.fasta import parseFastaDescription +from obitools import NucSequence + + +import sys + +_contigIterator=GenericParser('^CO ') + +_contigIterator.addParseAction('AF', '\nAF +(\S+) +([UC]) +(-?[0-9]+)') +_contigIterator.addParseAction('RD', '\nRD +(\S+) +([0-9]+) +([0-9]+) +([0-9]+) *\n([A-Za-z\n*]+?)\n\n') +_contigIterator.addParseAction('DS', '\nDS +(.+)') +_contigIterator.addParseAction('CO', '^CO (\S+)') + +def contigIterator(file): + file = universalOpen(file) + for entry in _contigIterator(file): + contig=[] + for rd,ds,af in map(None,entry['RD'],entry['DS'],entry['AF']): + id = rd[0] + shift = int(af[2]) + if shift < 0: + print >> sys.stderr,"Sequence %s in contig %s has a negative paddng value %d : skipped" % (id,entry['CO'][0],shift) + #continue + + definition,info = parseFastaDescription(ds) + info['shift']=shift + seq = rd[4].replace('\n','').replace('*','-').strip() + contig.append(NucSequence(id,seq,definition,**info)) + + maxlen = max(len(x)+x['shift'] for x in contig) + minshift=min(x['shift'] for x in contig) + rep = [] + + for s in contig: + info = s.getTags() + info['shift']-=minshift-1 + head = '-' * (info['shift']-1) + + tail = (maxlen + minshift - len(s) - info['shift'] - 1) + info['tail']=tail + newseq = NucSequence(s.id,head + str(s)+ '-' * tail,s.definition,**info) + rep.append(newseq) + + yield entry['CO'][0],rep + \ No newline at end of file diff --git a/obitools/barcodecoverage/__init__.py b/obitools/barcodecoverage/__init__.py new file mode 100644 index 0000000..09e542e --- /dev/null +++ b/obitools/barcodecoverage/__init__.py @@ -0,0 +1,7 @@ +''' + +@author: merciece +Creates the tree representing the coverage of 2 primers from an ecoPCR output file and an ecoPCR database. + + +''' \ No newline at end of file diff --git a/obitools/barcodecoverage/calcBc.py b/obitools/barcodecoverage/calcBc.py new file mode 100644 index 0000000..13b0401 --- /dev/null +++ b/obitools/barcodecoverage/calcBc.py @@ -0,0 +1,62 @@ +#!/usr/local/bin/python +''' +Created on 24 nov. 2011 + +@author: merciece +''' + + +def main(amplifiedSeqs, seqsFromDB, keptRanks, errors, tax) : + ''' + error threshold is set to 3 + ''' + + listtaxabygroupinDB = {} + + for seq in seqsFromDB : + taxid = seq['taxid'] + p = [a for a in tax.parentalTreeIterator(taxid)] + for a in p : + if a != p[0] : + if a[1] in keptRanks : + group = a[0] + if group in listtaxabygroupinDB and taxid not in listtaxabygroupinDB[group] : + listtaxabygroupinDB[group].add(taxid) + elif group not in listtaxabygroupinDB : + listtaxabygroupinDB[group]=set([taxid]) + + taxabygroup = dict((x,len(listtaxabygroupinDB[x])) for x in listtaxabygroupinDB) + + listamplifiedtaxabygroup = {} + + for seq in amplifiedSeqs : + if errors[seq.id][2] <= 3 : + taxid = seq['taxid'] + p = [a for a in tax.parentalTreeIterator(taxid)] + for a in p : + if a != p[0] : + if a[1] in keptRanks : + group = a[0] + if group in listamplifiedtaxabygroup and taxid not in listamplifiedtaxabygroup[group] : + listamplifiedtaxabygroup[group].add(taxid) + elif group not in listamplifiedtaxabygroup : + listamplifiedtaxabygroup[group]=set([taxid]) + + amplifiedtaxabygroup = dict((x,len(listamplifiedtaxabygroup[x])) for x in listamplifiedtaxabygroup) + + BcValues = {} + + groups = [g for g in taxabygroup.keys()] + + for g in groups : + if g in amplifiedtaxabygroup : + BcValues[g] = float(amplifiedtaxabygroup[g])/taxabygroup[g]*100 + BcValues[g] = round(BcValues[g], 2) + else : + BcValues[g] = 0.0 + + return BcValues + + + + diff --git a/obitools/barcodecoverage/calculateBc.py b/obitools/barcodecoverage/calculateBc.py new file mode 100644 index 0000000..c5edb8a --- /dev/null +++ b/obitools/barcodecoverage/calculateBc.py @@ -0,0 +1,72 @@ +#!/usr/local/bin/python +''' +Created on 24 nov. 2011 + +@author: merciece +''' + +import sys + + +def main(amplifiedSeqs, seqsFromDB, keptRanks, tax) : + + BcValues = {} + + #speciesid = tax.findRankByName('species') + #subspeciesid = tax.findRankByName('subspecies') + + listtaxonbygroup = {} + + for seq in seqsFromDB : + taxid = seq['taxid'] + p = [a for a in tax.parentalTreeIterator(taxid)] + for a in p : + if a != p[0] : + if a[1] in keptRanks : + group = a + if group in listtaxonbygroup: + listtaxonbygroup[group].add(taxid) + else: + listtaxonbygroup[group]=set([taxid]) + + #stats = dict((x,len(listtaxonbygroup[x])) for x in listtaxonbygroup) + + print>>sys.stderr, listtaxonbygroup + + listtaxonbygroup = {} + + for seq in amplifiedSeqs : + taxid = seq['taxid'] + p = [a for a in tax.parentalTreeIterator(taxid)] + for a in p : + if a != p[0] : + if a[1] in keptRanks : + group = a + if group in listtaxonbygroup: + listtaxonbygroup[group].add(taxid) + else: + listtaxonbygroup[group]=set([taxid]) + + print>>sys.stderr, listtaxonbygroup + + return BcValues + +# dbstats= dict((x,len(listtaxonbygroup[x])) for x in listtaxonbygroup) +# +# ranks = [r for r in keptRanks] +# ranks.sort() +# +# print '%-20s\t%10s\t%10s\t%7s' % ('rank','ecopcr','db','percent') +# +# print>>sys.stderr, stats +# print>>sys.stderr, dbstats +# print>>sys.stderr, ranks +# +# for r in ranks: +# if r in dbstats and dbstats[r]: +# print '%-20s\t%10d\t%10d\t%8.2f' % (r,dbstats[r],stats[r],float(dbstats[r])/stats[r]*100) + + + + + diff --git a/obitools/barcodecoverage/drawBcTree.py b/obitools/barcodecoverage/drawBcTree.py new file mode 100644 index 0000000..9b1e215 --- /dev/null +++ b/obitools/barcodecoverage/drawBcTree.py @@ -0,0 +1,108 @@ +#!/usr/local/bin/python +''' +Created on 25 nov. 2011 + +@author: merciece +''' + +from obitools.graph.rootedtree import nexusFormat + + +figtree="""\ +begin figtree; + set appearance.backgroundColorAttribute="User Selection"; + set appearance.backgroundColour=#-1; + set appearance.branchColorAttribute="bc"; + set appearance.branchLineWidth=2.0; + set appearance.foregroundColour=#-16777216; + set appearance.selectionColour=#-2144520576; + set branchLabels.colorAttribute="User Selection"; + set branchLabels.displayAttribute="errors"; + set branchLabels.fontName="sansserif"; + set branchLabels.fontSize=10; + set branchLabels.fontStyle=0; + set branchLabels.isShown=true; + set branchLabels.significantDigits=4; + set layout.expansion=2000; + set layout.layoutType="RECTILINEAR"; + set layout.zoom=0; + set nodeBars.barWidth=4.0; + set nodeLabels.colorAttribute="User Selection"; + set nodeLabels.displayAttribute="label"; + set nodeLabels.fontName="sansserif"; + set nodeLabels.fontSize=10; + set nodeLabels.fontStyle=0; + set nodeLabels.isShown=true; + set nodeLabels.significantDigits=4; + set polarLayout.alignTipLabels=false; + set polarLayout.angularRange=0; + set polarLayout.rootAngle=0; + set polarLayout.rootLength=100; + set polarLayout.showRoot=true; + set radialLayout.spread=0.0; + set rectilinearLayout.alignTipLabels=false; + set rectilinearLayout.curvature=0; + set rectilinearLayout.rootLength=100; + set scale.offsetAge=0.0; + set scale.rootAge=1.0; + set scale.scaleFactor=1.0; + set scale.scaleRoot=false; + set scaleAxis.automaticScale=true; + set scaleAxis.fontSize=8.0; + set scaleAxis.isShown=false; + set scaleAxis.lineWidth=2.0; + set scaleAxis.majorTicks=1.0; + set scaleAxis.origin=0.0; + set scaleAxis.reverseAxis=false; + set scaleAxis.showGrid=true; + set scaleAxis.significantDigits=4; + set scaleBar.automaticScale=true; + set scaleBar.fontSize=10.0; + set scaleBar.isShown=true; + set scaleBar.lineWidth=1.0; + set scaleBar.scaleRange=0.0; + set scaleBar.significantDigits=4; + set tipLabels.colorAttribute="User Selection"; + set tipLabels.displayAttribute="Names"; + set tipLabels.fontName="sansserif"; + set tipLabels.fontSize=10; + set tipLabels.fontStyle=0; + set tipLabels.isShown=true; + set tipLabels.significantDigits=4; + set trees.order=false; + set trees.orderType="increasing"; + set trees.rooting=false; + set trees.rootingType="User Selection"; + set trees.transform=false; + set trees.transformType="cladogram"; +end; +""" + + +def cartoonRankGenerator(rank): + def cartoon(node): + return 'rank' in node and node['rank']==rank + + return cartoon + + +def collapseBcGenerator(Bclimit): + def collapse(node): + return 'bc' in node and node['bc']<=Bclimit + return collapse + + +def label(node): + if 'bc' in node: + return "(%+3.1f) %s" % (node['bc'],node['name']) + else: + return " %s" % node['name'] + + +def main(coverageTree) : + print nexusFormat(coverageTree, + label=label, + blocks=figtree, + cartoon=cartoonRankGenerator('family')) + #collapse=collapseBcGenerator(70)) + diff --git a/obitools/barcodecoverage/findErrors.py b/obitools/barcodecoverage/findErrors.py new file mode 100644 index 0000000..dae20a0 --- /dev/null +++ b/obitools/barcodecoverage/findErrors.py @@ -0,0 +1,56 @@ +#!/usr/local/bin/python +''' +Created on 24 nov. 2011 + +@author: merciece +''' + + +def main(seqs, keptRanks, tax): + errorsBySeq = getErrorsOnLeaves(seqs) + errorsByTaxon = propagateErrors(errorsBySeq, keptRanks, tax) + return errorsBySeq, errorsByTaxon + + +def getErrorsOnLeaves(seqs) : + errors = {} + for s in seqs : + taxid = s['taxid'] + forErrs = s['forward_error'] + revErrs = s['reverse_error'] + total = forErrs + revErrs + seqNb = 1 + errors[s.id] = [forErrs,revErrs,total,seqNb,taxid] + return errors + + +def propagateErrors(errorsOnLeaves, keptRanks, tax) : + allErrors = {} + for seq in errorsOnLeaves : + taxid = errorsOnLeaves[seq][4] + p = [a for a in tax.parentalTreeIterator(taxid)] + for a in p : + if a[1] in keptRanks : + group = a[0] + if group in allErrors : + allErrors[group][0] += errorsOnLeaves[seq][0] + allErrors[group][1] += errorsOnLeaves[seq][1] + allErrors[group][2] += errorsOnLeaves[seq][2] + allErrors[group][3] += 1 + else : + allErrors[group] = errorsOnLeaves[seq] + + for group in allErrors : + allErrors[group][0] /= float(allErrors[group][3]) + allErrors[group][1] /= float(allErrors[group][3]) + allErrors[group][2] /= float(allErrors[group][3]) + + allErrors[group][0] = round(allErrors[group][0], 2) + allErrors[group][1] = round(allErrors[group][1], 2) + allErrors[group][2] = round(allErrors[group][2], 2) + + return allErrors + + + + diff --git a/obitools/barcodecoverage/readFiles.py b/obitools/barcodecoverage/readFiles.py new file mode 100644 index 0000000..b03e72a --- /dev/null +++ b/obitools/barcodecoverage/readFiles.py @@ -0,0 +1,69 @@ +#!/usr/local/bin/python +''' +Created on 23 nov. 2011 + +@author: merciece +''' + +from obitools.ecopcr import sequence +from obitools.ecopcr import taxonomy + + +def main(entries,options): + filteredDataFromDB = ecoPCRDatabaseReader(options) + filteredData = ecoPCRFileReader(entries,filteredDataFromDB) + return filteredDataFromDB,filteredData + + +def ecoPCRDatabaseReader(options): + + tax = taxonomy.EcoTaxonomyDB(options.taxonomy) + seqs = sequence.EcoPCRDBSequenceIterator(options.taxonomy,taxonomy=tax) + + norankid = tax.findRankByName('no rank') + speciesid = tax.findRankByName('species') + genusid = tax.findRankByName('genus') + familyid = tax.findRankByName('family') + + minrankseq = set([speciesid,genusid,familyid]) + + usedrankid = {} + + ingroup = {} + outgroup= {} + + for s in seqs : + if 'taxid' in s : + taxid = s['taxid'] + allrank = set() + for p in tax.parentalTreeIterator(taxid): + if p[1]!=norankid: + allrank.add(p[1]) + if len(minrankseq & allrank) == 3: + for r in allrank: + usedrankid[r]=usedrankid.get(r,0) + 1 + + if tax.isAncestor(options.ingroup,taxid): + ingroup[s.id] = s + else: + outgroup[s.id] = s + + keptranks = set(r for r in usedrankid + if float(usedrankid[r])/float(len(ingroup)) > options.rankthresold) + + return { 'ingroup' : ingroup, + 'outgroup': outgroup, + 'ranks' : keptranks, + 'taxonomy': tax + } + + +def ecoPCRFileReader(entries,filteredDataFromDB) : + filteredData = [] + for s in entries : + if 'taxid' in s : + seqId = s.id + if seqId in filteredDataFromDB['ingroup'] : + filteredData.append(s) + return filteredData + diff --git a/obitools/barcodecoverage/writeBcTree.py b/obitools/barcodecoverage/writeBcTree.py new file mode 100644 index 0000000..7c8243e --- /dev/null +++ b/obitools/barcodecoverage/writeBcTree.py @@ -0,0 +1,42 @@ +#!/usr/local/bin/python +''' +Created on 25 nov. 2011 + +@author: merciece +''' + +from obitools.graph.rootedtree import RootedTree + + +def main(BcValues,errors,tax) : + + tree = RootedTree() + tset = set(BcValues) + + for taxon in BcValues: + if taxon in errors : + forErr = errors[taxon][0] + revErr = errors[taxon][1] + totErr = errors[taxon][2] + else : + forErr = -1.0 + revErr = -1.0 + totErr = -1.0 + + tree.addNode(taxon, rank=tax.getRank(taxon), + name=tax.getScientificName(taxon), + bc = BcValues[taxon], + errors = str(forErr)+' '+str(revErr), + totError = totErr + ) + + for taxon in BcValues: + piter = tax.parentalTreeIterator(taxon) + taxon = piter.next() + for parent in piter: + if taxon[0] in tset and parent[0] in BcValues: + tset.remove(taxon[0]) + tree.addEdge(parent[0], taxon[0]) + taxon=parent + + return tree diff --git a/obitools/blast/__init__.py b/obitools/blast/__init__.py new file mode 100644 index 0000000..11b5274 --- /dev/null +++ b/obitools/blast/__init__.py @@ -0,0 +1,207 @@ +from os import popen2 +from itertools import imap,count + +from obitools.table import iTableIterator,TableRow,Table,SelectionIterator +from obitools.utils import ColumnFile +from obitools.location import SimpleLocation +from obitools.fasta import formatFasta +import sys + +class Blast(object): + ''' + Run blast + ''' + + def __init__(self,mode,db,program='blastall',**options): + self._mode = mode + self._db = db + self._program = program + self._options = options + + def getMode(self): + return self._mode + + + def getDb(self): + return self._db + + + def getProgram(self): + return self._program + + def _blastcmd(self): + tmp = """%(program)s \\ + -p %(mode)s \\ + -d %(db)s \\ + -m 8 \\ + %(options)s \\ + """ + options = ' '.join(['-%s %s' % (x[0],str(x[1])) + for x in self._options.iteritems()]) + data = { + 'program' : self.program, + 'db' : self.db, + 'mode' : self.mode, + 'options' : options + } + + return tmp % data + + def __call__(self,sequence): + ''' + Run blast with one sequence object + @param sequence: + @type sequence: + ''' + cmd = self._blastcmd() + + (blast_in,blast_out) = popen2(cmd) + + print >>blast_in,formatFasta(sequence) + blast_in.close() + + blast = BlastResultIterator(blast_out) + + return blast + + mode = property(getMode, None, None, "Mode's Docstring") + + db = property(getDb, None, None, "Db's Docstring") + + program = property(getProgram, None, None, "Program's Docstring") + + +class NetBlast(Blast): + ''' + Run blast on ncbi servers + ''' + + def __init__(self,mode,db,**options): + ''' + + @param mode: + @param db: + ''' + Blast.__init__(self, mode, db, 'blastcl3',**options) + + +class BlastResultIterator(iTableIterator): + + def __init__(self,blastoutput,query=None): + ''' + + @param blastoutput: + @type blastoutput: + ''' + self._blast = ColumnFile(blastoutput, + strip=True, + skip="#", + sep="\t", + types=self.types + ) + self._query = query + self._hindex = dict((k,i) for i,k in imap(None,count(),self._getHeaders())) + + def _getHeaders(self): + return ('Query id','Subject id', + '% identity','alignment length', + 'mismatches', 'gap openings', + 'q. start', 'q. end', + 's. start', 's. end', + 'e-value', 'bit score') + + def _getTypes(self): + return (str,str, + float,int, + int,int, + int,int, + int,int, + float,float) + + def _getRowFactory(self): + return BlastMatch + + def _getSubrowFactory(self): + return TableRow + + def _getQuery(self): + return self._query + + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + query = property(_getQuery,None,None) + + def next(self): + ''' + + ''' + value = self._blast.next() + return self.rowFactory(self,value) + + + +class BlastResult(Table): + ''' + Results of a blast run + ''' + +class BlastMatch(TableRow): + ''' + Blast high scoring pair between two sequences + ''' + + def getQueryLocation(self): + l = SimpleLocation(self[6], self[7]) + return l + + def getSubjectLocation(self): + l = SimpleLocation(self[8], self[9]) + return l + + def getSubjectSequence(self,database): + return database[self[1]] + + def queryCov(self,query=None): + ''' + Compute coverage of match on query sequence. + + @param query: the query sequence. Default is None. + In this case the query sequence associated + to this blast result is used. + @type query: L{obitools.BioSequence} + + @return: coverage fraction + @rtype: float + ''' + if query is None: + query = self.table.query + assert query is not None + return float(self[7]-self[6]+1)/float(len(query)) + + def __getitem__(self,key): + if key=='query coverage' and self.table.query is not None: + return self.queryCov() + else: + return TableRow.__getitem__(self,key) + +class BlastCovMinFilter(SelectionIterator): + + def __init__(self,blastiterator,covmin,query=None,**conditions): + if query is None: + query = blastiterator.table.query + assert query is not None + SelectionIterator.__init__(self,blastiterator,**conditions) + self._query = query + self._covmin=covmin + + def _covMinPredicat(self,row): + return row.queryCov(self._query)>=self._covmin + + def _checkCondition(self,row): + return self._covMinPredicat(row) and SelectionIterator._checkCondition(self, row) + + + \ No newline at end of file diff --git a/obitools/carto/__init__.py b/obitools/carto/__init__.py new file mode 100644 index 0000000..b7ac176 --- /dev/null +++ b/obitools/carto/__init__.py @@ -0,0 +1,376 @@ +# -*- coding: latin1 -*- + + + +from obitools import SVGdraw +import math + +class Map(object): + """ + Map represente une instance d'une carte genetique physique. + Une telle carte est definie par la longueur de la sequence + qui lui est associe. + + A une carte est associe un certain nombre de niveaux (Level) + eux meme decoupe en sous-niveau (SubLevel) + Les sous niveaux contiennent eux des features + """ + def __init__(self,name,seqlength,scale=1): + """ + Constructeur d'une nouvelle carte + + *Param*: + + name + nom de la carte + + seqlength + longueur de la sequence associee a la carte + + scale + echelle de la carte indicant combien de pixel + correspondent a une unite de la carte + """ + self.name = name + self.seqlength = seqlength + self.scale = scale + self.levels = {} + self.basicHSize = 10 + + def __str__(self): + return '<%s>' % self.name + + def __getitem__(self,level): + """ + retourne le niveau *level* de la carte et + le cree s'il n'existe pas + """ + if not isinstance(level,int): + raise TypeError('level must be an non Zero integer value') + elif level==0: + raise AssertionError('Level cannot be set to 0') + try: + return self.levels[level] + except KeyError: + self.levels[level] = Level(level,self) + return self.levels[level] + + def getBasicHSize(self): + """ + retourne la hauteur de base d'un element de cartographie + exprimee en pixel + """ + return self.basicHSize + + def getScale(self): + """ + Retourne l'echelle de la carte en nombre de pixels par + unite physique de la carte + """ + return self.scale + + + + def getNegativeBase(self): + return reduce(lambda x,y:x-y,[self.levels[z].getHeight() + for z in self.levels + if z < 0],self.getHeight()) + + def getPositiveBase(self): + return self.getNegativeBase() - 3 * self.getBasicHSize() + + def getHeight(self): + return reduce(lambda x,y:x+y,[z.getHeight() for z in self.levels.values()],0) \ + + 4 * self.getBasicHSize() + + def toXML(self,file=None,begin=0,end=None): + dessin = SVGdraw.drawing() + if end==None: + end = self.seqlength + hauteur= self.getHeight() + largeur=(end-begin+1)*self.scale + svg = SVGdraw.svg((begin*self.scale,0,largeur,hauteur), + '%fpx' % (self.seqlength * self.scale), + '%dpx' % hauteur) + + centre = self.getPositiveBase() + (1 + 1/4) * self.getBasicHSize() + svg.addElement(SVGdraw.rect(0,centre,self.seqlength * self.scale,self.getBasicHSize()/2)) + for e in self.levels.values(): + svg.addElement(e.getElement()) + dessin.setSVG(svg) + return dessin.toXml(file) + +class Feature(object): + pass + +class Level(object): + + def __init__(self,level,map): + if not isinstance(map,Map): + raise AssertionError('map is not an instance of class Map') + if level in map.levels: + raise AssertionError('Level %d already define for map %s' % (level,map)) + else: + map.levels[level] = self + self.map = map + self.level = level + self.sublevels = {} + + def __getitem__(self,sublevel): + """ + retourne le niveau *sublevel* du niveau en + le creant s'il n'existe pas + """ + if not isinstance(sublevel,int): + raise TypeError('sublevel must be a positive integer value') + elif sublevel<0: + raise AssertionError('Level cannot be negative') + try: + return self.sublevels[sublevel] + except KeyError: + self.sublevels[sublevel] = SubLevel(sublevel,self) + return self.sublevels[sublevel] + + def getBase(self): + if self.level < 0: + base = self.map.getNegativeBase() + base += reduce(lambda x,y:x+y,[self.map.levels[z].getHeight() + for z in self.map.levels + if z <0 and z >= self.level],0) + return base + else: + base = self.map.getPositiveBase() + base -= reduce(lambda x,y:x+y,[self.map.levels[z].getHeight() + for z in self.map.levels + if z >0 and z < self.level],0) + return base + + def getElement(self): + objet = SVGdraw.group('level%d' % self.level) + for e in self.sublevels.values(): + objet.addElement(e.getElement()) + return objet + + + + def getHeight(self): + return reduce(lambda x,y:x+y,[z.getHeight() for z in self.sublevels.values()],0) \ + + 2 * self.map.getBasicHSize() + +class SubLevel(object): + + def __init__(self,sublevel,level): + if not isinstance(level,Level): + raise AssertionError('level is not an instance of class Level') + if level in level.sublevels: + raise AssertionError('Sublevel %d already define for level %s' % (sublevel,level)) + else: + level.sublevels[sublevel] = self + self.level = level + self.sublevel = sublevel + self.features = {} + + def getHeight(self): + return max([x.getHeight() for x in self.features.values()]+[0]) + 4 * self.level.map.getBasicHSize() + + def getBase(self): + base = self.level.getBase() + if self.level.level < 0: + base -= self.level.getHeight() - 2 * self.level.map.getBasicHSize() + base += reduce(lambda x,y:x+y,[self.level.sublevels[z].getHeight() + for z in self.level.sublevels + if z <= self.sublevel],0) + base -= 2* self.level.map.getBasicHSize() + else: + base -= reduce(lambda x,y:x+y,[self.level.sublevels[z].getHeight() + for z in self.level.sublevels + if z < self.sublevel],0) + base -= self.level.map.getBasicHSize() + return base + + def getElement(self): + base = self.getBase() + objet = SVGdraw.group('sublevel%d' % self.sublevel) + for e in self.features.values(): + objet.addElement(e.getElement(base)) + return objet + + def add(self,feature): + if not isinstance(feature,Feature): + raise TypeError('feature must be an instance oof Feature') + if feature.name in self.features: + raise AssertionError('A feature with the same name (%s) have already be insert in this sublevel' + % feature.name) + self.features[feature.name]=feature + feature.sublevel=self + +class SimpleFeature(Feature): + + def __init__(self,name,begin,end,visiblename=False,color=0): + self.begin = begin + self.end = end + self.name = name + self.color = color + self.sublevel = None + self.visiblename=visiblename + + def getHeight(self): + if not self.sublevel: + raise AssertionError('Not affected Simple feature') + if self.visiblename: + return self.sublevel.level.map.getBasicHSize() * 2 + else: + return self.sublevel.level.map.getBasicHSize() + + def getElement(self,base): + scale = self.sublevel.level.map.getScale() + y = base - self.sublevel.level.map.getBasicHSize() + x = self.begin * scale + width = (self.end - self.begin + 1) * scale + heigh = self.sublevel.level.map.getBasicHSize() + + objet = SVGdraw.rect(x,y,width,heigh,stroke=self.color) + objet.addElement(SVGdraw.description(self.name)) + + return objet + +class BoxFeature(SimpleFeature): + + def getHeight(self): + if not self.sublevel: + raise AssertionError('Not affected Box feature') + if self.visiblename: + return self.sublevel.level.map.getBasicHSize() * 4 + else: + return self.sublevel.level.map.getBasicHSize() * 3 + + def getElement(self,base): + scale = self.sublevel.level.map.getScale() + y = base - self.sublevel.level.map.getBasicHSize() * 2 + x = self.begin * scale + width = (self.end - self.begin + 1) * scale + height = self.sublevel.level.map.getBasicHSize() * 3 + + objet = SVGdraw.rect(x,y,width,height,stroke=self.color,fill="none") + objet.addElement(SVGdraw.description(self.name)) + + return objet + +class MultiPartFeature(Feature): + + def __init__(self,name,*args,**kargs): + self.limits = args + self.name = name + try: + self.color = kargs['color'] + except KeyError: + self.color = "black" + + try: + self.visiblename=kargs['visiblename'] + except KeyError: + self.visiblename=None + + try: + self.flatlink=kargs['flatlink'] + except KeyError: + self.flatlink=False + + try: + self.roundlink=kargs['roundlink'] + except KeyError: + self.roundlink=False + + self.sublevel = None + + + def getHeight(self): + if not self.sublevel: + raise AssertionError('Not affected Simple feature') + if self.visiblename: + return self.sublevel.level.map.getBasicHSize() * 3 + else: + return self.sublevel.level.map.getBasicHSize() * 2 + + def getElement(self,base): + scale = self.sublevel.level.map.getScale() + + y = base - self.sublevel.level.map.getBasicHSize() + height = self.sublevel.level.map.getBasicHSize() + objet = SVGdraw.group(self.name) + for (debut,fin) in self.limits: + x = debut * scale + width = (fin - debut + 1) * scale + part = SVGdraw.rect(x,y,width,height,fill=self.color) + objet.addElement(part) + + debut = self.limits[0][1] + for (fin,next) in self.limits[1:]: + debut*=scale + fin*=scale + path = SVGdraw.pathdata(debut,y + height / 2) + delta = height / 2 + if self.roundlink: + path.qbezier((debut+fin)/2, y - delta,fin,y + height / 2) + else: + if self.flatlink: + delta = - height / 2 + path.line((debut+fin)/2, y - delta) + path.line(fin,y + height / 2) + path = SVGdraw.path(path,fill="none",stroke=self.color) + objet.addElement(path) + debut = next + + objet.addElement(SVGdraw.description(self.name)) + + return objet + +class TagFeature(Feature): + + def __init__(self,name,begin,length,ratio,visiblename=False,color=0): + self.begin = begin + self.length = length + self.ratio = ratio + self.name = name + self.color = color + self.sublevel = None + self.visiblename=visiblename + + def getHeight(self): + if not self.sublevel: + raise AssertionError('Not affected Tag feature') + + return self.sublevel.level.map.getBasicHSize()*11 + + def getElement(self,base): + scale = self.sublevel.level.map.getScale() + height = math.floor(max(1,self.sublevel.level.map.getBasicHSize()* 10 * self.ratio)) + y = base + self.sublevel.level.map.getBasicHSize() - height + x = self.begin * scale + width = self.length * scale + objet = SVGdraw.rect(x,y,width,height,stroke=self.color) + objet.addElement(SVGdraw.description(self.name)) + + return objet + +if __name__ == '__main__': + carte = Map('essai',20000,scale=0.5) + carte[-1][0].add(SimpleFeature('toto',100,300)) + carte[1][0].add(SimpleFeature('toto',100,300)) + carte[1][1].add(SimpleFeature('toto',200,1000)) + + carte[1][0].add(MultiPartFeature('bout',(1400,1450),(1470,1550),(1650,1800),color='red',flatlink=True)) + carte[1][0].add(MultiPartFeature('titi',(400,450),(470,550),(650,800),color='red',flatlink=True)) + carte[-1][1].add(MultiPartFeature('titi',(400,450),(470,550),(650,800),color='green')) + carte[-1][2].add(MultiPartFeature('titi',(400,450),(470,550),(650,800),color='purple',roundlink=True)) + + carte[-1][1].add(BoxFeature('tutu',390,810,color='purple')) + carte[1][0].add(BoxFeature('tutu',390,810,color='red')) + carte[2][0].add(TagFeature('t1',1400,20,0.8)) + carte[2][0].add(TagFeature('t2',1600,20,0.2)) + carte.basicHSize=6 + print carte.toXML('truc.svg',begin=0,end=1000) + print carte.toXML('truc2.svg',begin=460,end=2000) + + + diff --git a/obitools/decorator.py b/obitools/decorator.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/distances/__init__.py b/obitools/distances/__init__.py new file mode 100644 index 0000000..1542fa9 --- /dev/null +++ b/obitools/distances/__init__.py @@ -0,0 +1,29 @@ +class DistanceMatrix(object): + + def __init__(self,alignment): + ''' + DistanceMatrix constructor. + + @param alignment: aligment used to compute distance matrix + @type alignment: obitools.align.Alignment + ''' + self.aligment = alignment + self.matrix = [[None] * (x+1) for x in xrange(len(alignment))] + + def evaluateDist(self,x,y): + raise NotImplementedError + + def __getitem__(self,key): + assert isinstance(key,(tuple,list)) and len(key)==2, \ + 'key must be a tuple or a list of two integers' + x,y = key + if y < x: + z=x + x=y + y=z + rep = self.matrix[y][x] + if rep is None: + rep = self.evaluateDist(x,y) + self.matrix[y][x] = rep + + return rep \ No newline at end of file diff --git a/obitools/distances/observed.py b/obitools/distances/observed.py new file mode 100644 index 0000000..8828d92 --- /dev/null +++ b/obitools/distances/observed.py @@ -0,0 +1,77 @@ +''' +Module dedicated to compute observed divergeances from +an alignment. No distance correction is applied at all +''' + +from itertools import imap + +from obitools.distances import DistanceMatrix + +class PairewiseGapRemoval(DistanceMatrix): + ''' + Observed divergeance matrix from an alignment. + Gap are removed from the alignemt on a pairewise + sequence base + ''' + + def evaluateDist(self,x,y): + ''' + Compute the observed divergeance from two sequences + of an aligment. + + @attention: For performance purpose this method should + be directly used. use instead the __getitem__ + method from DistanceMatrix. + + @see: L{__getitem__} + + @param x: number of the fisrt sequence in the aligment + @type x: int + @param y: umber of the second sequence in the aligment + @type y: int + + + ''' + + seq1 = self.aligment[x] + seq2 = self.aligment[y] + + diff,tot = reduce(lambda x,y: (x[0]+y,x[1]+1), + (z[0]!=z[1] for z in imap(None,seq1,seq2) + if '-' not in z),(0,0)) + return float(diff)/tot + + +class Pairewise(DistanceMatrix): + ''' + Observed divergeance matrix from an alignment. + Gap are kept from the alignemt + ''' + + def evaluateDist(self,x,y): + ''' + Compute the observed divergeance from two sequences + of an aligment. + + @attention: For performance purpose this method should + be directly used. use instead the __getitem__ + method from DistanceMatrix. + + @see: L{__getitem__} + + @param x: number of the fisrt sequence in the aligment + @type x: int + @param y: umber of the second sequence in the aligment + @type y: int + + + ''' + + seq1 = self.aligment[x] + seq2 = self.aligment[y] + + diff,tot = reduce(lambda x,y: (x[0]+y,x[1]+1), + (z[0]!=z[1] for z in imap(None,seq1,seq2)), + (0,0)) + return float(diff)/tot + \ No newline at end of file diff --git a/obitools/distances/phylip.py b/obitools/distances/phylip.py new file mode 100644 index 0000000..e2043fa --- /dev/null +++ b/obitools/distances/phylip.py @@ -0,0 +1,35 @@ +import sys + +from itertools import imap,count + +def writePhylipMatrix(matrix): + names = [x.id for x in matrix.aligment] + pnames= [x[:10] for x in names] + unicity={} + redundent=[] + for n in pnames: + unicity[n]=unicity.get(n,0)+1 + redundent.append(unicity[n]) + + for i,n,r in imap(None,count(),pnames,redundent): + alternate = n + if r > 1: + while alternate in pnames: + lcut = 9 - len(str(r)) + alternate = n[:lcut]+ '_%d' % r + r+=1 + pnames[i]='%-10s' % alternate + + firstline = '%5d' % len(matrix.aligment) + rep = [firstline] + for i,n in imap(None,count(),pnames): + line = [n] + for j in xrange(i): + line.append('%5.4f' % matrix[(j,i)]) + rep.append(' '.join(line)) + return '\n'.join(rep) + + + + + \ No newline at end of file diff --git a/obitools/distances/r.py b/obitools/distances/r.py new file mode 100644 index 0000000..f674a4c --- /dev/null +++ b/obitools/distances/r.py @@ -0,0 +1,25 @@ +import sys + +from itertools import imap,count + +def writeRMatrix(matrix): + names = [x.id for x in matrix.aligment] + lmax = max(max(len(x) for x in names),5) + lali = len(matrix.aligment) + + nformat = '%%-%ds' % lmax + dformat = '%%%d.4f' % lmax + + pnames=[nformat % x for x in names] + + rep = [' '.join(pnames)] + + for i in xrange(lali): + line=[] + for j in xrange(lali): + line.append('%5.4f' % matrix[(j,i)]) + rep.append(' '.join(line)) + return '\n'.join(rep) + + + \ No newline at end of file diff --git a/obitools/dnahash/__init__.py b/obitools/dnahash/__init__.py new file mode 100644 index 0000000..ca02e35 --- /dev/null +++ b/obitools/dnahash/__init__.py @@ -0,0 +1,100 @@ +_A=[0] +_C=[1] +_G=[2] +_T=[3] +_R= _A + _G +_Y= _C + _T +_M= _C + _A +_K= _T + _G +_W= _T + _A +_S= _C + _G +_B= _C + _G + _T +_D= _A + _G + _T +_H= _A + _C + _T +_V= _A + _C + _G +_N= _A + _C + _G + _T + +_dnahash={'a':_A, + 'c':_C, + 'g':_G, + 't':_T, + 'r':_R, + 'y':_Y, + 'm':_M, + 'k':_K, + 'w':_W, + 's':_S, + 'b':_B, + 'd':_D, + 'h':_H, + 'v':_V, + 'n':_N, + } + +def hashCodeIterator(sequence,wsize,degeneratemax=0,offset=0): + errors = 0 + emask = [0] * wsize + epointer = 0 + size = 0 + position = offset + hashs = set([0]) + hashmask = 0 + for i in xrange(wsize): + hashmask <<= 2 + hashmask +=3 + + for l in sequence: + l = l.lower() + hl = _dnahash[l] + + if emask[epointer]: + errors-=1 + emask[epointer]=0 + + if len(hl) > 1: + errors +=1 + emask[epointer]=1 + + epointer+=1 + epointer%=wsize + + if errors > degeneratemax: + hl=set([hl[0]]) + + hashs=set((((hc<<2) | cl) & hashmask) + for hc in hashs + for cl in hl) + + if size < wsize: + size+=1 + + if size==wsize: + if errors <= degeneratemax: + yield (position,hashs,errors) + position+=1 + +def hashSequence(sequence,wsize,degeneratemax=0,offset=0,hashs=None): + if hashs is None: + hashs=[[] for x in xrange(4**wsize)] + + for pos,keys,errors in hashCodeIterator(sequence, wsize, degeneratemax, offset): + for k in keys: + hashs[k].append(pos) + + return hashs + +def hashSequences(sequences,wsize,maxpos,degeneratemax=0): + hashs=None + offsets=[] + offset=0 + for s in sequences: + offsets.append(offset) + hashSequence(s,wsize,degeneratemax=degeneratemax,offset=offset,hashs=hashs) + offset+=len(s) + + return hashs,offsets + + + + + \ No newline at end of file diff --git a/obitools/ecobarcode/__init__.py b/obitools/ecobarcode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/ecobarcode/databases.py b/obitools/ecobarcode/databases.py new file mode 100644 index 0000000..70d2319 --- /dev/null +++ b/obitools/ecobarcode/databases.py @@ -0,0 +1,32 @@ +''' +Created on 25 sept. 2010 + +@author: coissac +''' +from obitools import NucSequence + +def referenceDBIterator(options): + + cursor = options.ecobarcodedb.cursor() + + cursor.execute("select id from databases.database where name='%s'" % options.database) + options.dbid = cursor.fetchone()[0] + + cursor.execute(''' + select s.accession,r.id,r.taxid,r.sequence + from databases.database d, + databases.reference r, + databases.relatedsequences s + where r.database = d.id + and s.reference= r.id + and s.mainac + and d.name = '%s' + ''' % options.database + ) + + for ac,id,taxid,sequence in cursor: + s = NucSequence(ac,sequence) + s['taxid']=taxid + s['refdbid']=id + yield s + \ No newline at end of file diff --git a/obitools/ecobarcode/ecotag.py b/obitools/ecobarcode/ecotag.py new file mode 100644 index 0000000..2ebd3fb --- /dev/null +++ b/obitools/ecobarcode/ecotag.py @@ -0,0 +1,50 @@ +''' +Created on 25 sept. 2010 + +@author: coissac +''' + +def alreadyIdentified(seqid,options): + cursor = options.ecobarcodedb.cursor() + cursor.execute(''' + select count(*) + from ecotag.identification + where sequence=%s + and database=%s + ''',(int(seqid),int(options.dbid))) + + return int(cursor.fetchone()[0]) > 0; + +def storeIdentification(seqid, + idstatus,taxid, + matches, + options + ): + + cursor = options.ecobarcodedb.cursor() + + if not options.updatedb: + cursor.execute(''' + delete from ecotag.identification where sequence=%s and database=%s + ''',(int(seqid),int(options.dbid))) + + cursor.execute(''' + insert into ecotag.identification (sequence,database,idstatus,taxid) + values (%s,%s,%s,%s) + returning id + ''' , (int(seqid),int(options.dbid),idstatus,int(taxid))) + + idid = cursor.fetchone()[0] + + for seq,identity in matches.iteritems(): + cursor.execute(''' + insert into ecotag.evidence (identification,reference,identity) + values (%s, + %s, + %s) + ''',(idid,seq,identity)) + + + cursor.close() + + options.ecobarcodedb.commit() diff --git a/obitools/ecobarcode/options.py b/obitools/ecobarcode/options.py new file mode 100644 index 0000000..6086423 --- /dev/null +++ b/obitools/ecobarcode/options.py @@ -0,0 +1,64 @@ +''' +Created on 23 sept. 2010 + +@author: coissac +''' +import psycopg2 + +from obitools.ecobarcode.taxonomy import EcoTaxonomyDB + +def addEcoBarcodeDBOption(optionManager): + optionManager.add_option('--dbname', + action="store", dest="ecobarcodedb", + type='str', + default=None, + help="Specify the name of the ecobarcode database") + + optionManager.add_option('--server', + action="store", dest="dbserver", + type='str', + default="localhost", + help="Specify the adress of the ecobarcode database server") + + optionManager.add_option('--user', + action="store", dest="dbuser", + type='str', + default='postgres', + help="Specify the user of the ecobarcode database") + + optionManager.add_option('--port', + action="store", dest="dbport", + type='str', + default=5432, + help="Specify the port of the ecobarcode database") + + optionManager.add_option('--passwd', + action="store", dest="dbpasswd", + type='str', + default='', + help="Specify the passwd of the ecobarcode database") + + optionManager.add_option('--primer', + action="store", dest="primer", + type='str', + default=None, + help="Specify the primer used for amplification") + + +def ecobarcodeDatabaseConnection(options): + if options.ecobarcodedb is not None: + connection = psycopg2.connect(database=options.ecobarcodedb, + user=options.dbuser, + password=options.dbpasswd, + host=options.dbserver, + port=options.dbport) + options.dbname=options.ecobarcodedb + else: + connection=None + if connection is not None: + options.ecobarcodedb=connection + taxonomy = EcoTaxonomyDB(connection) + else: + taxonomy=None + return taxonomy + diff --git a/obitools/ecobarcode/rawdata.py b/obitools/ecobarcode/rawdata.py new file mode 100644 index 0000000..a5f58cf --- /dev/null +++ b/obitools/ecobarcode/rawdata.py @@ -0,0 +1,38 @@ +''' +Created on 25 sept. 2010 + +@author: coissac +''' + +from obitools import NucSequence +from obitools.utils import progressBar +from obitools.ecobarcode.ecotag import alreadyIdentified + +import sys + +def sequenceIterator(options): + cursor = options.ecobarcodedb.cursor() + + cursor.execute(''' + select s.id,sum(o.count),s.sequence + from rawdata.sequence s, + rawdata.occurrences o + where o.sequence= s.id + and s.primers = '%s' + group by s.id,s.sequence + ''' % options.primer + ) + + nbseq = cursor.rowcount + progressBar(1, nbseq, True, head=options.dbname) + for id,count,sequence in cursor: + progressBar(cursor.rownumber+1, nbseq, head=options.dbname) + if not options.updatedb or not alreadyIdentified(id,options): + s = NucSequence(id,sequence) + s['count']=count + print >>sys.stderr,' +', cursor.rownumber+1, + yield s + else: + print >>sys.stderr,' @', cursor.rownumber+1, + + print >>sys.stderr diff --git a/obitools/ecobarcode/taxonomy.py b/obitools/ecobarcode/taxonomy.py new file mode 100644 index 0000000..c7d0185 --- /dev/null +++ b/obitools/ecobarcode/taxonomy.py @@ -0,0 +1,120 @@ +''' +Created on 24 sept. 2010 + +@author: coissac +''' + +from obitools.ecopcr.taxonomy import TaxonomyDump +from obitools.ecopcr.taxonomy import Taxonomy +import sys + +class EcoTaxonomyDB(TaxonomyDump) : + + def __init__(self,dbconnect): + self._dbconnect=dbconnect + + print >> sys.stderr,"Reading ecobarcode taxonomy database..." + + self._readNodeTable() + print >> sys.stderr," ok" + + print >>sys.stderr,"Adding scientific name..." + + self._name=[] + for taxid,name,classname in self._nameIterator(): + self._name.append((name,classname,self._index[taxid])) + if classname == 'scientific name': + self._taxonomy[self._index[taxid]].append(name) + + print >>sys.stderr,"Adding taxid alias..." + for taxid,current in self._mergedNodeIterator(): + self._index[taxid]=self._index[current] + + print >>sys.stderr,"Adding deleted taxid..." + for taxid in self._deletedNodeIterator(): + self._index[taxid]=None + + + Taxonomy.__init__(self) + + ##### + # + # Iterator functions + # + ##### + + def _readNodeTable(self): + + cursor = self._dbconnect.cursor() + + cursor.execute(""" + select taxid,rank,parent + from ncbitaxonomy.nodes + """) + + print >>sys.stderr,"Reading taxonomy nodes..." + taxonomy=[list(n) for n in cursor] + + print >>sys.stderr,"List all taxonomy rank..." + ranks =list(set(x[1] for x in taxonomy)) + ranks.sort() + rankidx = dict(map(None,ranks,xrange(len(ranks)))) + + print >>sys.stderr,"Sorting taxons..." + taxonomy.sort(TaxonomyDump._taxonCmp) + + self._taxonomy=taxonomy + + print >>sys.stderr,"Indexing taxonomy..." + index = {} + for t in self._taxonomy: + index[t[0]]=self._bsearchTaxon(t[0]) + + print >>sys.stderr,"Indexing parent and rank..." + for t in self._taxonomy: + t[1]=rankidx[t[1]] + t[2]=index[t[2]] + + self._ranks=ranks + self._index=index + + cursor.close() + + def _nameIterator(self): + cursor = self._dbconnect.cursor() + + cursor.execute(""" + select taxid,name,nameclass + from ncbitaxonomy.names + """) + + for taxid,name,nameclass in cursor: + yield taxid,name,nameclass + + cursor.close() + + def _mergedNodeIterator(self): + cursor = self._dbconnect.cursor() + + cursor.execute(""" + select oldtaxid,newtaxid + from ncbitaxonomy.merged + """) + + for oldtaxid,newtaxid in cursor: + yield oldtaxid,newtaxid + + cursor.close() + + def _deletedNodeIterator(self): + cursor = self._dbconnect.cursor() + + cursor.execute(""" + select taxid + from ncbitaxonomy.delnodes + """) + + for taxid in cursor: + yield taxid[0] + + cursor.close() diff --git a/obitools/ecopcr/__init__.py b/obitools/ecopcr/__init__.py new file mode 100644 index 0000000..10a90e5 --- /dev/null +++ b/obitools/ecopcr/__init__.py @@ -0,0 +1,69 @@ +from obitools import utils +from obitools import NucSequence +from obitools.utils import universalOpen, universalTell, fileSize, progressBar +import struct +import sys + + +class EcoPCRFile(utils.ColumnFile): + def __init__(self,stream): + utils.ColumnFile.__init__(self, + stream, '|', True, + (str,int,int, + str,int,str, + int,str,int, + str,int,str, + str,str,int,float, + str,int,float, + int, + str,str), "#") + + + def next(self): + data = utils.ColumnFile.next(self) + seq = NucSequence(data[0],data[20],data[21]) + seq['seq_length_ori']=data[1] + seq['taxid']=data[2] + seq['rank']=data[3] + seq['species']=data[4] + seq['species_sn']=data[5] + seq['genus']=data[6] + seq['genus_sn']=data[7] + seq['family']=data[8] + seq['family_sn']=data[9] + seq['strand']=data[12] + seq['forward_primer']=data[13] + seq['forward_error']=data[14] + seq['forward_tm']=data[15] + seq['reverse_primer']=data[16] + seq['reverse_error']=data[17] + seq['reverse_tm']=data[18] + + return seq + + + +class EcoPCRDBFile(object): + + def _ecoRecordIterator(self,file): + file = universalOpen(file) + (recordCount,) = struct.unpack('> I',file.read(4)) + self._recover=False + + if recordCount: + for i in xrange(recordCount): + (recordSize,)=struct.unpack('>I',file.read(4)) + record = file.read(recordSize) + yield record + else: + print >> sys.stderr,"\n\n WARNING : EcoPCRDB readding set into recover data mode\n" + self._recover=True + ok=True + while(ok): + try: + (recordSize,)=struct.unpack('>I',file.read(4)) + record = file.read(recordSize) + yield record + except: + ok=False + \ No newline at end of file diff --git a/obitools/ecopcr/annotation.py b/obitools/ecopcr/annotation.py new file mode 100644 index 0000000..7c76fb2 --- /dev/null +++ b/obitools/ecopcr/annotation.py @@ -0,0 +1,104 @@ +import struct + +class EcoPCRDBAnnotationWriter(object): + ''' + Class used to write Annotation description in EcoPCRDB format. + + EcoPCRDBAnnotationWriter is oftenly called through the EcoPCRDBSequenceWriter class + + @see: L{ecopcr.sequence.EcoPCRDBSequenceWriter} + ''' + + def __init__(self,dbname,id,fileidx=1,type=('CDS'),definition=None): + ''' + class constructor + + @param dbname: name of ecoPCR database + @type dbname: C{str} + @param id: name of the qualifier used as feature id + @type id: C{str} + @param fileidx: + @type fileidx: C{int} + @param type: + @type type: C{list} or C{tuple} + @param definition: + @type definition: C{str} + ''' + self._type = type + self._definition = definition + self._id = id + self._filename="%s_%03d.adx" % (dbname,fileidx) + self._file = open(self._filename,'wb') + self._sequenceIdx=0 + + + ftname ="%s.fdx" % (dbname) + ft = open(ftname,'wb') + + self._fttypeidx=dict(map(None,type,xrange(len(type)))) + + ft.write(struct.pack('> I',len(type))) + + for t in type: + ft.write(self._ecoFtTypePacker(t)) + + ft.close() + + self._annotationCount=0 + self._file.write(struct.pack('> I',self._annotationCount)) + + + def _ecoFtTypePacker(self,type): + totalSize = len(type) + packed = struct.pack('> I %ds' % totalSize,totalSize,type) + + assert len(packed) == totalSize+4, "error in feature type packing" + + return packed + + def _ecoAnnotationPacker(self,feature,seqidx): + begin = feature.begin-1 + end = feature.end + type = self._fttypeidx[feature.ftType] + strand = feature.isDirect() + id = feature[self._id][0] + if self._definition in feature: + definition = feature[self._definition][0] + else: + definition = '' + + assert strand is not None,"Only strand defined features can be stored" + + deflength = len(definition) + + totalSize = 4 + 4 + 4 + 4 + 4 + 20 + 4 + deflength + + packed = struct.pack('> I I I I I 20s I %ds' % (deflength), + totalSize, + seqidx, + begin, + end, + type, + int(strand), + id, + deflength, + definition) + + assert len(packed) == totalSize+4, "error in annotation packing" + + return packed + + + def put(self,sequence,seqidx=None): + if seqidx is None: + seqidx = self._sequenceIdx + self._sequenceIdx+=1 + for feature in sequence.getFeatureTable(): + if feature.ftType in self._type: + self._annotationCount+=1 + self._file.write(self._ecoAnnotationPacker(feature,seqidx)) + + def __del__(self): + self._file.seek(0,0) + self._file.write(struct.pack('> I',self._annotationCount)) + self._file.close() diff --git a/obitools/ecopcr/options.py b/obitools/ecopcr/options.py new file mode 100644 index 0000000..03663cd --- /dev/null +++ b/obitools/ecopcr/options.py @@ -0,0 +1,129 @@ +''' +Created on 13 fevr. 2011 + +@author: coissac +''' + +from obitools.ecopcr.taxonomy import Taxonomy, EcoTaxonomyDB, TaxonomyDump, ecoTaxonomyWriter + +try: + from obitools.ecobarcode.options import addEcoBarcodeDBOption,ecobarcodeDatabaseConnection +except ImportError: + def addEcoBarcodeDBOption(optionmanager): + pass + def ecobarcodeDatabaseConnection(options): + return None + +def addTaxonomyDBOptions(optionManager): + addEcoBarcodeDBOption(optionManager) + optionManager.add_option('-d','--database', + action="store", dest="taxonomy", + metavar="", + type="string", + help="ecoPCR taxonomy Database " + "name") + optionManager.add_option('-t','--taxonomy-dump', + action="store", dest="taxdump", + metavar="", + type="string", + help="NCBI Taxonomy dump repository " + "name") + + +def addTaxonomyFilterOptions(optionManager): + addTaxonomyDBOptions(optionManager) + optionManager.add_option('--require-rank', + action="append", + dest='requiredRank', + metavar="", + type="string", + default=[], + help="select sequence with taxid tag containing " + "a parent of rank ") + + optionManager.add_option('-r','--required', + action="append", + dest='required', + metavar="", + type="int", + default=[], + help="required taxid") + + optionManager.add_option('-i','--ignore', + action="append", + dest='ignored', + metavar="", + type="int", + default=[], + help="ignored taxid") + +def loadTaxonomyDatabase(options): + if isinstance(options.taxonomy, Taxonomy): + return options.taxonomy + taxonomy = ecobarcodeDatabaseConnection(options) + if (taxonomy is not None or + options.taxonomy is not None or + options.taxdump is not None): + if options.taxdump is not None: + taxonomy = TaxonomyDump(options.taxdump) + if taxonomy is not None and isinstance(options.taxonomy, str): + ecoTaxonomyWriter(options.taxonomy,taxonomy) + options.ecodb=options.taxonomy + if isinstance(options.taxonomy, Taxonomy): + taxonomy = options.taxonomy + if taxonomy is None and isinstance(options.taxonomy, str): + taxonomy = EcoTaxonomyDB(options.taxonomy) + options.ecodb=options.taxonomy + options.taxonomy=taxonomy + return options.taxonomy + +def taxonomyFilterGenerator(options): + loadTaxonomyDatabase(options) + if options.taxonomy is not None: + taxonomy=options.taxonomy + def taxonomyFilter(seq): + def annotateAtRank(seq,rank): + if 'taxid' in seq and seq['taxid'] is not None: + rtaxid= taxonomy.getTaxonAtRank(seq['taxid'],rank) + return rtaxid + return None + good = True + if 'taxid' in seq: + taxid = seq['taxid'] +# print taxid, + if options.requiredRank: + taxonatrank = reduce(lambda x,y: x and y, + (annotateAtRank(seq,rank) is not None + for rank in options.requiredRank),True) + good = good and taxonatrank +# print >>sys.stderr, " Has rank : ",good, + if options.required: + good = good and reduce(lambda x,y: x or y, + (taxonomy.isAncestor(r,taxid) for r in options.required), + False) +# print " Required : ",good, + if options.ignored: + good = good and not reduce(lambda x,y: x or y, + (taxonomy.isAncestor(r,taxid) for r in options.ignored), + False) +# print " Ignored : ",good, +# print " Global : ",good + + return good + + + else: + def taxonomyFilter(seq): + return True + + return taxonomyFilter + +def taxonomyFilterIteratorGenerator(options): + taxonomyFilter = taxonomyFilterGenerator(options) + + def filterIterator(seqiterator): + for seq in seqiterator: + if taxonomyFilter(seq): + yield seq + + return filterIterator \ No newline at end of file diff --git a/obitools/ecopcr/sequence.py b/obitools/ecopcr/sequence.py new file mode 100644 index 0000000..1465e69 --- /dev/null +++ b/obitools/ecopcr/sequence.py @@ -0,0 +1,133 @@ +from obitools import NucSequence +from obitools.ecopcr import EcoPCRDBFile +from obitools.ecopcr.taxonomy import EcoTaxonomyDB, ecoTaxonomyWriter +from obitools.ecopcr.annotation import EcoPCRDBAnnotationWriter +from obitools.utils import universalOpen +from glob import glob +import struct +import gzip +import sys + + +class EcoPCRDBSequenceIterator(EcoPCRDBFile): + ''' + Build an iterator over the sequences include in a sequence database + formated for ecoPCR + ''' + + def __init__(self,path,taxonomy=None): + ''' + ecoPCR data iterator constructor + + @param path: path to the ecoPCR database including the database prefix name + @type path: C{str} + @param taxonomy: a taxonomy can be given to the reader to decode the taxonomic data + associated to the sequences. If no Taxonomy is furnish, it will be read + before the sequence database files using the same path. + @type taxonomy: L{obitools.ecopcr.taxonomy.Taxonomy} + ''' + self._path = path + + if taxonomy is not None: + self._taxonomy=taxonomy + else: + self._taxonomy=EcoTaxonomyDB(path) + + self._seqfilesFiles = glob('%s_???.sdx' % self._path) + self._seqfilesFiles.sort() + + def __ecoSequenceIterator(self,file): + for record in self._ecoRecordIterator(file): + lrecord = len(record) + lnames = lrecord - (4*4+20) + (taxid,seqid,deflength,seqlength,cptseqlength,string)=struct.unpack('> I 20s I I I %ds' % lnames, record) + seqid=seqid.strip('\x00') + de = string[:deflength] + seq = gzip.zlib.decompress(string[deflength:]) + bioseq = NucSequence(seqid,seq,de,taxidx=taxid,taxid=self._taxonomy._taxonomy[taxid][0]) + yield bioseq + + def __iter__(self): + for seqfile in self._seqfilesFiles: + for seq in self.__ecoSequenceIterator(seqfile): + yield seq + +class EcoPCRDBSequenceWriter(object): + + def __init__(self,dbname,fileidx=1,taxonomy=None,ftid=None,type=None,definition=None,append=False): + self._taxonomy=taxonomy + self._filename="%s_%03d.sdx" % (dbname,fileidx) + if append: + mode ='r+b' + f = universalOpen(self._filename) + (recordCount,) = struct.unpack('> I',f.read(4)) + self._sequenceCount=recordCount + del f + self._file = open(self._filename,mode) + self._file.seek(0,0) + self._file.write(struct.pack('> I',0)) + self._file.seek(0,2) + else: + self._sequenceCount=0 + mode = 'wb' + self._file = open(self._filename,mode) + self._file.write(struct.pack('> I',self._sequenceCount)) + + if self._taxonomy is not None: + print >> sys.stderr,"Writing the taxonomy file...", + ecoTaxonomyWriter(dbname,self._taxonomy) + print >> sys.stderr,"Ok" + + if type is not None: + assert ftid is not None,"You must specify an id attribute for features" + self._annotation = EcoPCRDBAnnotationWriter(dbname, ftid, fileidx, type, definition) + else: + self._annotation = None + + def _ecoSeqPacker(self,seq): + + compactseq = gzip.zlib.compress(str(seq).upper(),9) + cptseqlength = len(compactseq) + delength = len(seq.definition) + + totalSize = 4 + 20 + 4 + 4 + 4 + cptseqlength + delength + + if self._taxonomy is None or 'taxid' not in seq: + taxon=-1 + else: + taxon=self._taxonomy.findIndex(seq['taxid']) + + try: + packed = struct.pack('> I i 20s I I I %ds %ds' % (delength,cptseqlength), + totalSize, + taxon, + seq.id, + delength, + len(seq), + cptseqlength, + seq.definition, + compactseq) + except struct.error as e: + print >>sys.stderr,"\n\n============\n\nError on sequence : %s\n\n" % seq.id + raise e + + assert len(packed) == totalSize+4, "error in sequence packing" + + return packed + + + def put(self,sequence): + if self._taxonomy is not None: + if 'taxid' not in sequence and hasattr(sequence, 'extractTaxon'): + sequence.extractTaxon() + self._file.write(self._ecoSeqPacker(sequence)) + if self._annotation is not None: + self._annotation.put(sequence, self._sequenceCount) + self._sequenceCount+=1 + + def __del__(self): + self._file.seek(0,0) + self._file.write(struct.pack('> I',self._sequenceCount)) + self._file.close() + + diff --git a/obitools/ecopcr/taxonomy.py b/obitools/ecopcr/taxonomy.py new file mode 100644 index 0000000..bb2ec4e --- /dev/null +++ b/obitools/ecopcr/taxonomy.py @@ -0,0 +1,630 @@ +import struct +import sys + +from itertools import count,imap + +from obitools.ecopcr import EcoPCRDBFile +from obitools.utils import universalOpen +from obitools.utils import ColumnFile + +class Taxonomy(object): + def __init__(self): + ''' + The taxonomy database constructor + + @param path: path to the ecoPCR database including the database prefix name + @type path: C{str} + ''' + + self._ranks.append('obi') + + self._speciesidx = self._ranks.index('species') + self._genusidx = self._ranks.index('genus') + self._familyidx = self._ranks.index('family') + self._orderidx = self._ranks.index('order') + self._nameidx=dict((x[0],x[2]) for x in self._name) + self._nameidx.update(dict((x[0],x[2]) for x in self._preferedName)) + self._preferedidx=dict((x[2],x[1]) for x in self._preferedName) + + self._bigestTaxid = max(x[0] for x in self._taxonomy) + + + def findTaxonByIdx(self,idx): + if idx is None: + return None + return self._taxonomy[idx] + + def findIndex(self,taxid): + if taxid is None: + return None + return self._index[taxid] + + def findTaxonByTaxid(self,taxid): + return self.findTaxonByIdx(self.findIndex(taxid)) + + def findTaxonByName(self,name): + return self._taxonomy[self._nameidx[name]] + + def findRankByName(self,rank): + try: + return self._ranks.index(rank) + except ValueError: + return None + + def __contains__(self,taxid): + return self.findTaxonByTaxid(taxid) is not None + + + + + ##### + # + # PUBLIC METHODS + # + ##### + + + def subTreeIterator(self, taxid): + "return subtree for given taxonomic id " + idx = self.findTaxonByTaxid(taxid) + yield self._taxonomy[idx] + for t in self._taxonomy: + if t[2] == idx: + for subt in self.subTreeIterator(t[0]): + yield subt + + def parentalTreeIterator(self, taxid): + """ + return parental tree for given taxonomic id starting from + first ancester to the root. + """ + taxon=self.findTaxonByTaxid(taxid) + if taxon is not None: + while taxon[2]!= 0: + yield taxon + taxon = self._taxonomy[taxon[2]] + yield self._taxonomy[0] + else: + raise StopIteration + + def isAncestor(self,parent,taxid): + return parent in [x[0] for x in self.parentalTreeIterator(taxid)] + + def lastCommonTaxon(self,*taxids): + if not taxids: + return None + if len(taxids)==1: + return taxids[0] + + if len(taxids)==2: + t1 = [x[0] for x in self.parentalTreeIterator(taxids[0])] + t2 = [x[0] for x in self.parentalTreeIterator(taxids[1])] + t1.reverse() + t2.reverse() + + count = min(len(t1),len(t2)) + i=0 + while(i < count and t1[i]==t2[i]): + i+=1 + i-=1 + + return t1[i] + + ancetre = taxids[0] + for taxon in taxids[1:]: + ancetre = self.lastCommonTaxon(ancetre,taxon) + + return ancetre + + def betterCommonTaxon(self,error=1,*taxids): + lca = self.lastCommonTaxon(*taxids) + idx = self._index[lca] + sublca = [t[0] for t in self._taxonomy if t[2]==idx] + return sublca + + + def getPreferedName(self,taxid): + idx = self.findIndex(taxid) + return self._preferedidx.get(idx,self._taxonomy[idx][3]) + + + def getScientificName(self,taxid): + return self.findTaxonByTaxid(taxid)[3] + + def getRankId(self,taxid): + return self.findTaxonByTaxid(taxid)[1] + + def getRank(self,taxid): + return self._ranks[self.getRankId(taxid)] + + def getTaxonAtRank(self,taxid,rankid): + if isinstance(rankid, str): + rankid=self._ranks.index(rankid) + try: + return [x[0] for x in self.parentalTreeIterator(taxid) + if x[1]==rankid][0] + except IndexError: + return None + + def getSpecies(self,taxid): + return self.getTaxonAtRank(taxid, self._speciesidx) + + def getGenus(self,taxid): + return self.getTaxonAtRank(taxid, self._genusidx) + + def getFamily(self,taxid): + return self.getTaxonAtRank(taxid, self._familyidx) + + def getOrder(self,taxid): + return self.getTaxonAtRank(taxid, self._orderidx) + + def rankIterator(self): + for x in imap(None,self._ranks,xrange(len(self._ranks))): + yield x + + def groupTaxa(self,taxa,groupname): + t=[self.findTaxonByTaxid(x) for x in taxa] + a=set(x[2] for x in t) + assert len(a)==1,"All taxa must have the same parent" + newtaxid=max([2999999]+[x[0] for x in self._taxonomy if x[0]>=3000000 and x[0]<4000000])+1 + newidx=len(self._taxonomy) + if 'GROUP' not in self._ranks: + self._ranks.append('GROUP') + rankid=self._ranks.index('GROUP') + self._taxonomy.append((newtaxid,rankid,a.pop(),groupname)) + for x in t: + x[2]=newidx + + def addLocalTaxon(self,name,rank,parent,minimaltaxid=10000000): + newtaxid = minimaltaxid if (self._bigestTaxid < minimaltaxid) else self._bigestTaxid+1 + + rankid=self.findRankByName(rank) + parentidx = self.findIndex(int(parent)) + tx = (newtaxid,rankid,parentidx,name,'local') + self._taxonomy.append(tx) + newidx=len(self._taxonomy)-1 + self._name.append((name,'scientific name',newidx)) + self._nameidx[name]=newidx + self._index[newtaxid]=newidx + + self._bigestTaxid=newtaxid + + return newtaxid + + def removeLocalTaxon(self,taxid): + raise NotImplemented + txidx = self.findIndex(taxid) + taxon = self.findTaxonByIdx(txidx) + + assert txidx >= self._localtaxon,"Only local taxon can be deleted" + + for t in self._taxonomy: + if t[2] == txidx: + self.removeLocalTaxon(t[0]) + + + + + return taxon + + def addPreferedName(self,taxid,name): + idx = self.findIndex(taxid) + self._preferedName.append(name,'obi',idx) + self._preferedidx[idx]=name + return taxid + +class EcoTaxonomyDB(Taxonomy,EcoPCRDBFile): + ''' + A taxonomy database class + ''' + + + def __init__(self,path): + ''' + The taxonomy database constructor + + @param path: path to the ecoPCR database including the database prefix name + @type path: C{str} + ''' + self._path = path + self._taxonFile = "%s.tdx" % self._path + self._localTaxonFile = "%s.ldx" % self._path + self._ranksFile = "%s.rdx" % self._path + self._namesFile = "%s.ndx" % self._path + self._preferedNamesFile = "%s.pdx" % self._path + self._aliasFile = "%s.adx" % self._path + + print >> sys.stderr,"Reading binary taxonomy database...", + + self.__readNodeTable() + + print >> sys.stderr," ok" + + Taxonomy.__init__(self) + + + ##### + # + # Iterator functions + # + ##### + + def __ecoNameIterator(self,file): + for record in self._ecoRecordIterator(file): + lrecord = len(record) + lnames = lrecord - 16 + (isScientificName,namelength,classLength,indextaxid,names)=struct.unpack('> I I I I %ds' % lnames, record) + name=names[:namelength] + classname=names[namelength:] + yield (name,classname,indextaxid) + + + def __ecoTaxonomicIterator(self): + for record in self._ecoRecordIterator(self._taxonFile): + lrecord = len(record) + lnames = lrecord - 16 + (taxid,rankid,parentidx,nameLength,name)=struct.unpack('> I I I I %ds' % lnames, record) + yield (taxid,rankid,parentidx,name,'ncbi') + + try : + lt=0 + for record in self._ecoRecordIterator(self._localTaxonFile): + lrecord = len(record) + lnames = lrecord - 16 + (taxid,rankid,parentidx,nameLength,name)=struct.unpack('> I I I I %ds' % lnames, record) + lt+=1 + yield (taxid,rankid,parentidx,name,'local') + print >> sys.stderr, " [INFO : Local taxon file found] : %d added taxa" % lt + except: + print >> sys.stderr, " [INFO : Local taxon file not found] " + + def __ecoRankIterator(self): + for record in self._ecoRecordIterator(self._ranksFile): + yield record + + def __ecoAliasIterator(self): + for record in self._ecoRecordIterator(self._aliasFile): + (taxid,index) = struct.unpack('> I i',record) + yield taxid,index + + ##### + # + # Indexes + # + ##### + + def __ecoNameIndex(self): + indexName = [x for x in self.__ecoNameIterator(self._namesFile)] + return indexName + + def __ecoRankIndex(self): + rank = [r for r in self.__ecoRankIterator()] + return rank + + def __ecoTaxonomyIndex(self): + taxonomy = [] + + try : + index = dict(self.__ecoAliasIterator()) + print >> sys.stderr, " [INFO : Taxon alias file found] " + buildIndex=False + except: + print >> sys.stderr, " [INFO : Taxon alias file not found] " + index={} + i = 0; + buildIndex=True + + localtaxon=0 + i=0 + for x in self.__ecoTaxonomicIterator(): + taxonomy.append(x) + if x[4]=='ncbi': + localtaxon+=1 + + if buildIndex or x[4]!='ncbi': + index[x[0]] = i + i+=1 + + + print >> sys.stderr,"Taxonomical tree read", + return taxonomy, index,localtaxon + + def __readNodeTable(self): + self._taxonomy, self._index, self._localtaxon= self.__ecoTaxonomyIndex() + self._ranks = self.__ecoRankIndex() + self._name = self.__ecoNameIndex() + + # Add local taxon tame to the name index + i=self._localtaxon + for t in self._taxonomy[self._localtaxon:]: + self._name.append((t[3],'scientific name',i)) + i+=1 + + try : + self._preferedName = [(x[0],'obi',x[2]) + for x in self.__ecoNameIterator(self._preferedNamesFile)] + print >> sys.stderr, " [INFO : Prefered taxon name file found] : %d added taxa" % len(self._preferedName) + except: + print >> sys.stderr, " [INFO : Prefered taxon name file not found]" + self._preferedName = [] + + + + +class TaxonomyDump(Taxonomy): + + def __init__(self,taxdir): + + self._path=taxdir + self._readNodeTable('%s/nodes.dmp' % taxdir) + + print >>sys.stderr,"Adding scientific name..." + + self._name=[] + for taxid,name,classname in self._nameIterator('%s/names.dmp' % taxdir): + self._name.append((name,classname,self._index[taxid])) + if classname == 'scientific name': + self._taxonomy[self._index[taxid]].extend([name,'ncbi']) + + print >>sys.stderr,"Adding taxid alias..." + for taxid,current in self._mergedNodeIterator('%s/merged.dmp' % taxdir): + self._index[taxid]=self._index[current] + + print >>sys.stderr,"Adding deleted taxid..." + for taxid in self._deletedNodeIterator('%s/delnodes.dmp' % taxdir): + self._index[taxid]=None + + self._nameidx=dict((x[0],x[2]) for x in self._name) + + + def _taxonCmp(t1,t2): + if t1[0] < t2[0]: + return -1 + elif t1[0] > t2[0]: + return +1 + return 0 + + _taxonCmp=staticmethod(_taxonCmp) + + def _bsearchTaxon(self,taxid): + taxCount = len(self._taxonomy) + begin = 0 + end = taxCount + oldcheck=taxCount + check = begin + end / 2 + while check != oldcheck and self._taxonomy[check][0]!=taxid : + if self._taxonomy[check][0] < taxid: + begin=check + else: + end=check + oldcheck=check + check = (begin + end) / 2 + + + if self._taxonomy[check][0]==taxid: + return check + else: + return None + + + + def _readNodeTable(self,file): + + file = universalOpen(file) + + nodes = ColumnFile(file, + sep='|', + types=(int,int,str, + str,str,bool, + int,bool,int, + bool,bool,bool,str)) + print >>sys.stderr,"Reading taxonomy dump file..." + # (taxid,rank,parent) + taxonomy=[[n[0],n[2],n[1]] for n in nodes] + print >>sys.stderr,"List all taxonomy rank..." + ranks =list(set(x[1] for x in taxonomy)) + ranks.sort() + rankidx = dict(map(None,ranks,xrange(len(ranks)))) + + print >>sys.stderr,"Sorting taxons..." + taxonomy.sort(TaxonomyDump._taxonCmp) + + self._taxonomy=taxonomy + self._localtaxon=len(taxonomy) + + print >>sys.stderr,"Indexing taxonomy..." + index = {} + for t in self._taxonomy: + index[t[0]]=self._bsearchTaxon(t[0]) + + print >>sys.stderr,"Indexing parent and rank..." + for t in self._taxonomy: + t[1]=rankidx[t[1]] + t[2]=index[t[2]] + + self._ranks=ranks + self._index=index + self._preferedName = [] + + def _nameIterator(self,file): + file = universalOpen(file) + names = ColumnFile(file, + sep='|', + types=(int,str, + str,str)) + for taxid,name,unique,classname,white in names: + yield taxid,name,classname + + def _mergedNodeIterator(self,file): + file = universalOpen(file) + merged = ColumnFile(file, + sep='|', + types=(int,int,str)) + for taxid,current,white in merged: + yield taxid,current + + def _deletedNodeIterator(self,file): + file = universalOpen(file) + deleted = ColumnFile(file, + sep='|', + types=(int,str)) + for taxid,white in deleted: + yield taxid + +##### +# +# +# Binary writer +# +# +##### + +def ecoTaxonomyWriter(prefix, taxonomy,onlyLocal=False): + + def ecoTaxPacker(tx): + + namelength = len(tx[3]) + + totalSize = 4 + 4 + 4 + 4 + namelength + + packed = struct.pack('> I I I I I %ds' % namelength, + totalSize, + tx[0], + tx[1], + tx[2], + namelength, + tx[3]) + + return packed + + def ecoRankPacker(rank): + + namelength = len(rank) + + packed = struct.pack('> I %ds' % namelength, + namelength, + rank) + + return packed + + def ecoAliasPacker(taxid,index): + + totalSize = 4 + 4 + try: + packed = struct.pack('> I I i', + totalSize, + taxid, + index) + except struct.error,e: + print >>sys.stderr,(totalSize,taxid,index) + print >>sys.stderr,"Total size : %d taxid : %d index : %d" %(totalSize,taxid,index) + raise e + + return packed + + def ecoNamePacker(name): + + namelength = len(name[0]) + classlength= len(name[1]) + totalSize = namelength + classlength + 4 + 4 + 4 + 4 + + packed = struct.pack('> I I I I I %ds %ds' % (namelength,classlength), + totalSize, + int(name[1]=='scientific name'), + namelength, + classlength, + name[2], + name[0], + name[1]) + + return packed + + + def ecoTaxWriter(file,taxonomy): + output = open(file,'wb') + nbtaxon = reduce(lambda x,y:x+y,(1 for t in taxonomy if t[4]=='ncbi'),0) + + output.write(struct.pack('> I',nbtaxon)) + + for tx in taxonomy: + if tx[4]=='ncbi': + output.write(ecoTaxPacker(tx)) + + output.close() + return nbtaxon < len(taxonomy) + + def ecoLocalTaxWriter(file,taxonomy): + nbtaxon = reduce(lambda x,y:x+y,(1 for t in taxonomy if t[4]!='ncbi'),0) + + if nbtaxon: + output = open(file,'wb') + + output.write(struct.pack('> I',nbtaxon)) + + for tx in taxonomy: + if tx[4]!='ncbi': + output.write(ecoTaxPacker(tx)) + + output.close() + + + def ecoRankWriter(file,ranks): + output = open(file,'wb') + output.write(struct.pack('> I',len(ranks))) + + for rank in ranks: + output.write(ecoRankPacker(rank)) + + output.close() + + def ecoAliasWriter(file,index): + output = open(file,'wb') + output.write(struct.pack('> I',len(index))) + + for taxid in index: + i=index[taxid] + if i is None: + i=-1 + output.write(ecoAliasPacker(taxid, i)) + + output.close() + + def nameCmp(n1,n2): + name1=n1[0].upper() + name2=n2[0].upper() + if name1 < name2: + return -1 + elif name1 > name2: + return 1 + return 0 + + + def ecoNameWriter(file,names): + output = open(file,'wb') + output.write(struct.pack('> I',len(names))) + + names.sort(nameCmp) + + for name in names: + output.write(ecoNamePacker(name)) + + output.close() + + def ecoPreferedNameWriter(file,names): + output = open(file,'wb') + output.write(struct.pack('> I',len(names))) + for name in names: + output.write(ecoNamePacker(name)) + + output.close() + + localtaxon=True + if not onlyLocal: + ecoRankWriter('%s.rdx' % prefix, taxonomy._ranks) + localtaxon = ecoTaxWriter('%s.tdx' % prefix, taxonomy._taxonomy) + ecoNameWriter('%s.ndx' % prefix, [x for x in taxonomy._name if x[2] < taxonomy._localtaxon]) + ecoAliasWriter('%s.adx' % prefix, taxonomy._index) + if localtaxon: + ecoLocalTaxWriter('%s.ldx' % prefix, taxonomy._taxonomy) + if taxonomy._preferedName: + ecoNameWriter('%s.pdx' % prefix, taxonomy._preferedName) diff --git a/obitools/ecotag/__init__.py b/obitools/ecotag/__init__.py new file mode 100644 index 0000000..26c94d3 --- /dev/null +++ b/obitools/ecotag/__init__.py @@ -0,0 +1,2 @@ +class EcoTagResult(dict): + pass \ No newline at end of file diff --git a/obitools/ecotag/parser.py b/obitools/ecotag/parser.py new file mode 100644 index 0000000..f431e34 --- /dev/null +++ b/obitools/ecotag/parser.py @@ -0,0 +1,150 @@ +from itertools import imap +from obitools import utils + +from obitools.ecotag import EcoTagResult + +class EcoTagFileIterator(utils.ColumnFile): + + @staticmethod + def taxid(x): + x = int(x) + if x < 0: + return None + else: + return x + + @staticmethod + def scientificName(x): + if x=='--': + return None + else: + return x + + @staticmethod + def value(x): + if x=='--': + return None + else: + return float(x) + + @staticmethod + def count(x): + if x=='--': + return None + else: + return int(x) + + + def __init__(self,stream): + utils.ColumnFile.__init__(self, + stream, '\t', True, + (str,str,str, + EcoTagFileIterator.value, + EcoTagFileIterator.value, + EcoTagFileIterator.value, + EcoTagFileIterator.count, + EcoTagFileIterator.count, + EcoTagFileIterator.taxid, + EcoTagFileIterator.scientificName, + str, + EcoTagFileIterator.taxid, + EcoTagFileIterator.scientificName, + EcoTagFileIterator.taxid, + EcoTagFileIterator.scientificName, + EcoTagFileIterator.taxid, + EcoTagFileIterator.scientificName, + str + )) + self._memory=None + + _colname = ['identification', + 'seqid', + 'best_match_ac', + 'max_identity', + 'min_identity', + 'theorical_min_identity', + 'count', + 'match_count', + 'taxid', + 'scientific_name', + 'rank', + 'order_taxid', + 'order_sn', + 'family_taxid', + 'family_sn', + 'genus_taxid', + 'genus_sn', + 'species_taxid', + 'species_sn', + 'sequence'] + + def next(self): + if self._memory is not None: + data=self._memory + self._memory=None + else: + data = utils.ColumnFile.next(self) + data = EcoTagResult(imap(None,EcoTagFileIterator._colname[:len(data)],data)) + + if data['identification']=='ID': + data.cd=[] + try: + nextone = utils.ColumnFile.next(self) + nextone = EcoTagResult(imap(None,EcoTagFileIterator._colname[:len(nextone)],nextone)) + except StopIteration: + nextone = None + while nextone is not None and nextone['identification']=='CD': + data.cd.append(nextone) + try: + nextone = utils.ColumnFile.next(self) + nextone = EcoTagResult(imap(None,EcoTagFileIterator._colname[:len(nextone)],nextone)) + except StopIteration: + nextone = None + self._memory=nextone + + return data + +def ecoTagIdentifiedFilter(ecoTagIterator): + for x in ecoTagIterator: + if x['identification']=='ID': + yield x + + +class EcoTagAbstractIterator(utils.ColumnFile): + + _colname = ['scientific_name', + 'taxid', + 'rank', + 'count', + 'max_identity', + 'min_identity'] + + + @staticmethod + def taxid(x): + x = int(x) + if x < 0: + return None + else: + return x + + def __init__(self,stream): + utils.ColumnFile.__init__(self, + stream, '\t', True, + (str, + EcoTagFileIterator.taxid, + str, + int, + float,float,float)) + + def next(self): + data = utils.ColumnFile.next(self) + data = dict(imap(None,EcoTagAbstractIterator._colname,data)) + + return data + +def ecoTagAbstractFilter(ecoTagAbsIterator): + for x in ecoTagAbsIterator: + if x['taxid'] is not None: + yield x + \ No newline at end of file diff --git a/obitools/eutils/__init__.py b/obitools/eutils/__init__.py new file mode 100644 index 0000000..1e7d3b2 --- /dev/null +++ b/obitools/eutils/__init__.py @@ -0,0 +1,54 @@ +import time +from urllib2 import urlopen +import shelve +from threading import Lock +import sys + +class EUtils(object): + ''' + + ''' + + _last_request=0 + _interval=3 + + def __init__(self): + self._lock = Lock() + + def wait(self): + now=time.time() + delta = now - EUtils._last_request + while delta < EUtils._interval: + time.sleep(delta) + now=time.time() + delta = now - EUtils._last_request + + def _sendRequest(self,url): + self.wait() + EUtils._last_request=time.time() + t = EUtils._last_request + print >>sys.stderr,"Sending request to NCBI @ %f" % t + data = urlopen(url).read() + print >>sys.stderr,"Data red from NCBI @ %f (%f)" % (t,time.time()-t) + return data + + def setInterval(self,seconde): + EUtils._interval=seconde + + +class EFetch(EUtils): + ''' + + ''' + def __init__(self,db,tool='OBITools', + retmode='text',rettype="native", + server='eutils.ncbi.nlm.nih.gov'): + EUtils.__init__(self) + self._url = "http://%s/entrez/eutils/efetch.fcgi?db=%s&tool=%s&retmode=%s&rettype=%s" + self._url = self._url % (server,db,tool,retmode,rettype) + + + def get(self,**args): + key = "&".join(['%s=%s' % x for x in args.items()]) + return self._sendRequest(self._url +"&" + key) + diff --git a/obitools/fast.py b/obitools/fast.py new file mode 100644 index 0000000..760f493 --- /dev/null +++ b/obitools/fast.py @@ -0,0 +1,56 @@ +""" + implement fastn/fastp sililarity search algorithm for BioSequence. +""" + +class Fast(object): + + def __init__(self,seq,kup=2): + ''' + @param seq: sequence to hash + @type seq: BioSequence + @param kup: word size used for hashing process + @type kup: int + ''' + hash={} + seq = str(seq) + for word,pos in ((seq[i:i+kup].upper(),i) for i in xrange(len(seq)-kup)): + if word in hash: + hash[word].append(pos) + else: + hash[word]=[pos] + + self._kup = kup + self._hash= hash + self._seq = seq + + def __call__(self,seq): + ''' + Align one sequence with the fast hash table. + + @param seq: the sequence to align + @type seq: BioSequence + + @return: where smax is the + score of the largest diagonal and pmax the + associated shift + @rtype: a int tuple (smax,pmax) + ''' + histo={} + seq = str(seq).upper() + hash= self._hash + kup = self._kup + + for word,pos in ((seq[i:i+kup],i) for i in xrange(len(seq)-kup)): + matchedpos = hash.get(word,[]) + for p in matchedpos: + delta = pos - p + histo[delta]=histo.get(delta,0) + 1 + smax = max(histo.values()) + pmax = [x for x in histo if histo[x]==smax] + return smax,pmax + + def __len__(self): + return len(self._seq) + + + diff --git a/obitools/fasta/__init__.py b/obitools/fasta/__init__.py new file mode 100644 index 0000000..d5b90c5 --- /dev/null +++ b/obitools/fasta/__init__.py @@ -0,0 +1,384 @@ +""" +fasta module provides functions to read and write sequences in fasta format. + + +""" + +#from obitools.format.genericparser import fastGenericEntryIteratorGenerator +from obitools.format.genericparser import genericEntryIteratorGenerator +from obitools import bioSeqGenerator,BioSequence,AASequence,NucSequence +from obitools import _default_raw_parser + +#from obitools.alignment import alignmentReader +#from obitools.utils import universalOpen + +import re +from obitools.ecopcr.options import loadTaxonomyDatabase +from obitools.format import SequenceFileIterator + +#from _fasta import parseFastaDescription,fastaParser +#from _fasta import _fastaJoinSeq +#from _fasta import _parseFastaTag + + +#fastaEntryIterator=fastGenericEntryIteratorGenerator(startEntry='>') +fastaEntryIterator=genericEntryIteratorGenerator(startEntry='>') +rawFastaEntryIterator=genericEntryIteratorGenerator(startEntry='\s*>') + +def _fastaJoinSeq(seqarray): + return ''.join([x.strip() for x in seqarray]) + + +def parseFastaDescription(ds,tagparser): + + m = tagparser.search(' '+ds) + if m is not None: + info=m.group(0) + definition = ds[m.end(0):].strip() + else: + info=None + definition=ds + + return definition,info + +def fastaParser(seq,bioseqfactory,tagparser,rawparser,joinseq=_fastaJoinSeq): + ''' + Parse a fasta record. + + @attention: internal purpose function + + @param seq: a sequence object containing all lines corresponding + to one fasta sequence + @type seq: C{list} or C{tuple} of C{str} + + @param bioseqfactory: a callable object return a BioSequence + instance. + @type bioseqfactory: a callable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: a C{BioSequence} instance + ''' + seq = seq.split('\n') + title = seq[0].strip()[1:].split(None,1) + id=title[0] + if len(title) == 2: + definition,info=parseFastaDescription(title[1], tagparser) + else: + info= None + definition=None + + seq=joinseq(seq[1:]) + return bioseqfactory(id, seq, definition,info,rawparser) + + +def fastaNucParser(seq,tagparser=_default_raw_parser,joinseq=_fastaJoinSeq): + return fastaParser(seq,NucSequence,tagparser=tagparser,joinseq=_fastaJoinSeq) + +def fastaAAParser(seq,tagparser=_default_raw_parser,joinseq=_fastaJoinSeq): + return fastaParser(seq,AASequence,tagparser=tagparser,joinseq=_fastaJoinSeq) + +def fastaIterator(file,bioseqfactory=bioSeqGenerator, + tagparser=_default_raw_parser, + joinseq=_fastaJoinSeq): + ''' + iterate through a fasta file sequence by sequence. + Returned sequences by this iterator will be BioSequence + instances + + @param file: a line iterator containing fasta data or a filename + @type file: an iterable object or str + @param bioseqfactory: a callable object return a BioSequence + instance. + @type bioseqfactory: a callable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{BioSequence} instance + + @see: L{fastaNucIterator} + @see: L{fastaAAIterator} + + >>> from obitools.format.sequence.fasta import fastaIterator + >>> f = fastaIterator('monfichier') + >>> s = f.next() + >>> print s + gctagctagcatgctagcatgcta + >>> + ''' + rawparser=tagparser + allparser = tagparser % '[a-zA-Z][a-zA-Z0-9_]*' + tagparser = re.compile('( *%s)+' % allparser) + + for entry in fastaEntryIterator(file): + yield fastaParser(entry,bioseqfactory,tagparser,rawparser,joinseq) + +def rawFastaIterator(file,bioseqfactory=bioSeqGenerator, + tagparser=_default_raw_parser, + joinseq=_fastaJoinSeq): + + rawparser=tagparser + allparser = tagparser % '[a-zA-Z][a-zA-Z0-9_]*' + tagparser = re.compile('( *%s)+' % allparser) + + for entry in rawFastaEntryIterator(file): + entry=entry.strip() + yield fastaParser(entry,bioseqfactory,tagparser,rawparser,joinseq) + +def fastaNucIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fasta file sequence by sequence. + Returned sequences by this iterator will be NucSequence + instances + + @param file: a line iterator containint fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{NucBioSequence} instance + @rtype: a generator object + + @see: L{fastaIterator} + @see: L{fastaAAIterator} + ''' + return fastaIterator(file, NucSequence,tagparser) + +def fastaAAIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fasta file sequence by sequence. + Returned sequences by this iterator will be AASequence + instances + + @param file: a line iterator containing fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{AABioSequence} instance + + @see: L{fastaIterator} + @see: L{fastaNucIterator} + ''' + return fastaIterator(file, AASequence,tagparser) + +def formatFasta(data,gbmode=False,upper=False,restrict=None): + ''' + Convert a seqence or a set of sequences in a + string following the fasta format + + @param data: sequence or a set of sequences + @type data: BioSequence instance or an iterable object + on BioSequence instances + + @param gbmode: if set to C{True} identifier part of the title + line follows recommendation from nbci to allow + sequence indexing with the blast formatdb command. + @type gbmode: bool + + @param restrict: a set of key name that will be print in the formated + output. If restrict is set to C{None} (default) then + all keys are formated. + @type restrict: any iterable value or None + + @return: a fasta formated string + @rtype: str + ''' + if isinstance(data, BioSequence): + data = [data] + + if restrict is not None and not isinstance(restrict, set): + restrict = set(restrict) + + rep = [] + for sequence in data: + seq = str(sequence) + if sequence.definition is None: + definition='' + else: + definition=sequence.definition + if upper: + frgseq = '\n'.join([seq[x:x+60].upper() for x in xrange(0,len(seq),60)]) + else: + frgseq = '\n'.join([seq[x:x+60] for x in xrange(0,len(seq),60)]) + info='; '.join(['%s=%s' % x + for x in sequence.rawiteritems() + if restrict is None or x[0] in restrict]) + if info: + info=info+';' + if sequence._rawinfo is not None and sequence._rawinfo: + info+=" " + sequence._rawinfo.strip() + + id = sequence.id + if gbmode: + if 'gi' in sequence: + id = "gi|%s|%s" % (sequence['gi'],id) + else: + id = "lcl|%s|" % (id) + title='>%s %s %s' %(id,info,definition) + rep.append("%s\n%s" % (title,frgseq)) + return '\n'.join(rep) + +def formatSAPFastaGenerator(options): + loadTaxonomyDatabase(options) + + taxonomy=None + if options.taxonomy is not None: + taxonomy=options.taxonomy + + assert taxonomy is not None,"SAP formating require indication of a taxonomy database" + + ranks = ('superkingdom', 'kingdom', 'subkingdom', 'superphylum', + 'phylum', 'subphylum', 'superclass', 'class', 'subclass', + 'infraclass', 'superorder', 'order', 'suborder', 'infraorder', + 'parvorder', 'superfamily', 'family', 'subfamily', 'supertribe', 'tribe', + 'subtribe', 'supergenus', 'genus', 'subgenus', 'species group', + 'species subgroup', 'species', 'subspecies') + + trank=set(taxonomy._ranks) + ranks = [taxonomy._ranks.index(x) for x in ranks if x in trank] + + strict= options.strictsap + + def formatSAPFasta(data,gbmode=False,upper=False,restrict=None): + ''' + Convert a seqence or a set of sequences in a + string following the fasta format as recommended for the SAP + software + + http://ib.berkeley.edu/labs/slatkin/munch/StatisticalAssignmentPackage.html + + @param data: sequence or a set of sequences + @type data: BioSequence instance or an iterable object + on BioSequence instances + + @param gbmode: if set to C{True} identifier part of the title + line follows recommendation from nbci to allow + sequence indexing with the blast formatdb command. + @type gbmode: bool + + @param restrict: a set of key name that will be print in the formated + output. If restrict is set to C{None} (default) then + all keys are formated. + @type restrict: any iterable value or None + + @return: a fasta formated string + @rtype: str + ''' + if isinstance(data, BioSequence): + data = [data] + + if restrict is not None and not isinstance(restrict, set): + restrict = set(restrict) + + rep = [] + for sequence in data: + seq = str(sequence) + + if upper: + frgseq = '\n'.join([seq[x:x+60].upper() for x in xrange(0,len(seq),60)]) + else: + frgseq = '\n'.join([seq[x:x+60] for x in xrange(0,len(seq),60)]) + + try: + taxid = sequence["taxid"] + except KeyError: + if strict: + raise AssertionError('All sequence must have a taxid') + else: + continue + + definition=' ;' + + for r in ranks: + taxon = taxonomy.getTaxonAtRank(taxid,r) + if taxon is not None: + definition+=' %s: %s,' % (taxonomy._ranks[r],taxonomy.getPreferedName(taxon)) + + definition='%s ; %s' % (definition[0:-1],taxonomy.getPreferedName(taxid)) + + id = sequence.id + if gbmode: + if 'gi' in sequence: + id = "gi|%s|%s" % (sequence['gi'],id) + else: + id = "lcl|%s|" % (id) + title='>%s%s' %(id,definition) + rep.append("%s\n%s" % (title,frgseq)) + return '\n'.join(rep) + + return formatSAPFasta + +class FastaIterator(SequenceFileIterator): + + + entryIterator = genericEntryIteratorGenerator(startEntry='>') + classmethod(entryIterator) + + def __init__(self,inputfile,bioseqfactory=bioSeqGenerator, + tagparser=_default_raw_parser, + joinseq=_fastaJoinSeq): + + SequenceFileIterator.__init__(self, inputfile, bioseqfactory) + + self.__file = FastaIterator.entryIterator(self._inputfile) + + self._tagparser = tagparser + self._joinseq = joinseq + + def get_tagparser(self): + return self.__tagparser + + + def set_tagparser(self, value): + self._rawparser = value + allparser = value % '[a-zA-Z][a-zA-Z0-9_]*' + self.__tagparser = re.compile('( *%s)+' % allparser) + + def _parseFastaDescription(self,ds): + + m = self._tagparser.search(' '+ds) + if m is not None: + info=m.group(0) + definition = ds[m.end(0):].strip() + else: + info=None + definition=ds + + return definition,info + + + def _parser(self): + ''' + Parse a fasta record. + + @attention: internal purpose function + + @return: a C{BioSequence} instance + ''' + seq = self._seq.split('\n') + title = seq[0].strip()[1:].split(None,1) + id=title[0] + if len(title) == 2: + definition,info=self._parseFastaDescription(title[1]) + else: + info= None + definition=None + + seq=self._joinseq(seq[1:]) + + return self._bioseqfactory(id, seq, definition,info,self._rawparser) + + _tagparser = property(get_tagparser, set_tagparser, None, "_tagparser's docstring") diff --git a/obitools/fasta/_fasta.so b/obitools/fasta/_fasta.so new file mode 100755 index 0000000000000000000000000000000000000000..de300ce8a4e1a471ad38019e589ccf0fbf6e9124 GIT binary patch literal 75428 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 literal 0 HcmV?d00001 diff --git a/obitools/fastq/__init__.py b/obitools/fastq/__init__.py new file mode 100644 index 0000000..1cf3535 --- /dev/null +++ b/obitools/fastq/__init__.py @@ -0,0 +1,190 @@ +''' +Created on 29 aout 2009 + +@author: coissac +''' + +from obitools import BioSequence +from obitools import _default_raw_parser +from obitools.format.genericparser import genericEntryIteratorGenerator +from obitools import bioSeqGenerator,AASequence,NucSequence +from obitools.fasta import parseFastaDescription +from _fastq import fastqQualitySangerDecoder,fastqQualitySolexaDecoder +from _fastq import qualityToSangerError,qualityToSolexaError +from _fastq import errorToSangerFastQStr +from _fastq import formatFastq +from _fastq import fastqParserGenetator +from obitools.utils import universalOpen + +import re + +fastqEntryIterator=genericEntryIteratorGenerator(startEntry='^@',endEntry="^\+",strip=True,join=False) + +#def fastqParserGenetator(fastqvariant='sanger',bioseqfactory=NucSequence,tagparser=_parseFastaTag): +# +# qualityDecoder,errorDecoder = {'sanger' : (fastqQualitySangerDecoder,qualityToSangerError), +# 'solexa' : (fastqQualitySolexaDecoder,qualityToSolexaError), +# 'illumina' : (fastqQualitySolexaDecoder,qualityToSangerError)}[fastqvariant] +# +# def fastqParser(seq): +# ''' +# Parse a fasta record. +# +# @attention: internal purpose function +# +# @param seq: a sequence object containing all lines corresponding +# to one fasta sequence +# @type seq: C{list} or C{tuple} of C{str} +# +# @param bioseqfactory: a callable object return a BioSequence +# instance. +# @type bioseqfactory: a callable object +# +# @param tagparser: a compiled regular expression usable +# to identify key, value couples from +# title line. +# @type tagparser: regex instance +# +# @return: a C{BioSequence} instance +# ''' +# +# title = seq[0][1:].split(None,1) +# id=title[0] +# if len(title) == 2: +# definition,info=parseFastaDescription(title[1], tagparser) +# else: +# info= {} +# definition=None +# +# quality=errorDecoder(qualityDecoder(seq[3])) +# +# seq=seq[1] +# +# seq = bioseqfactory(id, seq, definition,False,**info) +# seq.quality = quality +# +# return seq +# +# return fastqParser + + +def fastqIterator(file,fastqvariant='sanger',bioseqfactory=NucSequence,tagparser=_default_raw_parser): + ''' + iterate through a fasta file sequence by sequence. + Returned sequences by this iterator will be BioSequence + instances + + @param file: a line iterator containing fasta data or a filename + @type file: an iterable object or str + @param bioseqfactory: a callable object return a BioSequence + instance. + @type bioseqfactory: a callable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{BioSequence} instance + + @see: L{fastaNucIterator} + @see: L{fastaAAIterator} + + ''' + fastqParser=fastqParserGenetator(fastqvariant, bioseqfactory, tagparser) + file = universalOpen(file) + for entry in fastqEntryIterator(file): + title=entry[0] + seq="".join(entry[1:-1]) + quality='' + lenseq=len(seq) + while (len(quality) < lenseq): + quality+=file.next().strip() + + yield fastqParser([title,seq,'+',quality]) + +def fastqSangerIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fastq file sequence by sequence. + Returned sequences by this iterator will be NucSequence + instances + + @param file: a line iterator containint fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{NucBioSequence} instance + + @see: L{fastqIterator} + @see: L{fastqAAIterator} + ''' + return fastqIterator(file,'sanger',NucSequence,tagparser) + +def fastqSolexaIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fastq file sequence by sequence. + Returned sequences by this iterator will be NucSequence + instances + + @param file: a line iterator containint fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{NucBioSequence} instance + + @see: L{fastqIterator} + @see: L{fastqAAIterator} + ''' + return fastqIterator(file,'solexa',NucSequence,tagparser) + +def fastqIlluminaIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fastq file sequence by sequence. + Returned sequences by this iterator will be NucSequence + instances + + @param file: a line iterator containint fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{NucBioSequence} instance + + @see: L{fastqIterator} + @see: L{fastqAAIterator} + ''' + return fastqIterator(file,'illumina',NucSequence,tagparser) + +def fastqAAIterator(file,tagparser=_default_raw_parser): + ''' + iterate through a fastq file sequence by sequence. + Returned sequences by this iterator will be AASequence + instances + + @param file: a line iterator containing fasta data + @type file: an iterable object + + @param tagparser: a compiled regular expression usable + to identify key, value couples from + title line. + @type tagparser: regex instance + + @return: an iterator on C{AABioSequence} instance + + @see: L{fastqIterator} + @see: L{fastqNucIterator} + ''' + return fastqIterator(file,'sanger',AASequence,tagparser) + + diff --git a/obitools/fastq/_fastq.so b/obitools/fastq/_fastq.so new file mode 100755 index 0000000000000000000000000000000000000000..4e3b942439c7e6d7d4a2a4afedf6ebcca1a1426a GIT binary patch literal 159528 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

*c^PS!O~|t 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": + if options.withqualfile is not None: + qualfile=qualityIterator(options.withqualfile) + reader=withQualIterator(qualfile) + options.outputFormater=formatFastq + options.outputFormat="fastq" + elif options.moltype=='nuc': + reader=fastaNucIterator + elif options.moltype=='pep': + reader=fastaAAIterator + else: + reader=fastaIterator + elif first[0]=='@': + reader=fastqSangerIterator + options.outputFormater=formatFastq + options.outputFormat="fastq" + elif first[0:3]=='ID ': + reader=emblIterator + elif first[0:6]=='LOCUS ': + reader=genbankIterator + elif first[0]=="#" or ecopcr_pattern.search(first): + reader=EcoPCRFile + else: + raise AssertionError,'file is not in fasta, fasta, embl, genbank or ecoPCR format' + + input = reader(chain([first],lineiterator)) + + return input + + if options.seqinformat is None: + reader = autoSequenceIterator + else: + if options.seqinformat=='fasta': + if options.moltype=='nuc': + reader=fastaNucIterator + elif options.moltype=='pep': + reader=fastaAAIterator + else: + reader=fastaIterator + elif options.seqinformat=='rawfasta': + reader=annotatedIterator(rawFastaIterator) + elif options.seqinformat=='genbank': + reader=annotatedIterator(genbankIterator) + elif options.seqinformat=='embl': + reader=annotatedIterator(emblIterator) + elif options.seqinformat=='fna': + reader=fnaFastaIterator + elif options.seqinformat=='sanger': + options.outputFormater=formatFastq + options.outputFormat="fastq" + reader=fastqSangerIterator + elif options.seqinformat=='solexa': + options.outputFormater=formatFastq + options.outputFormat="fastq" + reader=fastqSolexaIterator + elif options.seqinformat=='illumina': + options.outputFormater=formatFastq + options.outputFormat="fastq" + reader=fastqIlluminaIterator + elif options.seqinformat=='ecopcr': + reader=EcoPCRFile + + if options.seqinformat=='fna' and options.withqualfile is not None: + qualfile=qualityIterator(options.withqualfile) + reader=withQualIterator(qualfile) + options.outputFormater=formatFastq + options.outputFormat="fastq" + +# if options.addrank: +# reader = withRankIterator(reader) + return reader + +def sequenceWriterGenerator(options,output=sys.stdout): + class SequenceWriter: + def __init__(self,options,file=sys.stdout): + self._format=None + self._file=file + self._upper=options.uppercase + def put(self,seq): + if self._format is None: + self._format=formatFasta + if options.output is not None: + self._format=options.output + if self._format is formatSAPFastaGenerator: + self._format=formatSAPFastaGenerator(options) + elif options.outputFormater is not None: + self._format=options.outputFormater + s = self._format(seq,upper=self._upper) + try: + self._file.write(s) + self._file.write("\n") + except IOError: + sys.exit(0) + + if options.ecopcroutput is not None: + taxo = loadTaxonomyDatabase(options) + writer=EcoPCRDBSequenceWriter(options.ecopcroutput,taxonomy=taxo) + else: + writer=SequenceWriter(options,output) + + def sequenceWriter(sequence): + writer.put(sequence) + + return sequenceWriter + + \ No newline at end of file diff --git a/obitools/format/sequence/__init__.py b/obitools/format/sequence/__init__.py new file mode 100644 index 0000000..3918761 --- /dev/null +++ b/obitools/format/sequence/__init__.py @@ -0,0 +1,24 @@ +from obitools.fasta import fastaIterator +from obitools.fastq import fastqSangerIterator +from obitools.seqdb.embl.parser import emblIterator +from obitools.seqdb.genbank.parser import genbankIterator +from itertools import chain +from obitools.utils import universalOpen + +def autoSequenceIterator(file): + lineiterator = universalOpen(file) + first = lineiterator.next() + if first[0]==">": + reader=fastaIterator + elif first[0]=='@': + reader=fastqSangerIterator + elif first[0:3]=='ID ': + reader=emblIterator + elif first[0:6]=='LOCUS ': + reader=genbankIterator + else: + raise AssertionError,'file is not in fasta, fasta, embl, or genbank format' + + input = reader(chain([first],lineiterator)) + + return input diff --git a/obitools/format/sequence/embl.py b/obitools/format/sequence/embl.py new file mode 100644 index 0000000..f59f14a --- /dev/null +++ b/obitools/format/sequence/embl.py @@ -0,0 +1,2 @@ +from obitools.seqdb.embl.parser import emblIterator,emblParser + diff --git a/obitools/format/sequence/fasta.py b/obitools/format/sequence/fasta.py new file mode 100644 index 0000000..1d7bd49 --- /dev/null +++ b/obitools/format/sequence/fasta.py @@ -0,0 +1,4 @@ +from obitools.fasta import fastaIterator,fastaParser +from obitools.fasta import fastaAAIterator,fastaAAParser +from obitools.fasta import fastaNucIterator,fastaNucParser +from obitools.fasta import formatFasta diff --git a/obitools/format/sequence/fastq.py b/obitools/format/sequence/fastq.py new file mode 100644 index 0000000..54fdf89 --- /dev/null +++ b/obitools/format/sequence/fastq.py @@ -0,0 +1,13 @@ +''' +Created on 15 janv. 2010 + +@author: coissac +''' + +from obitools.fastq import fastqIterator,fastqParserGenetator +from obitools.fastq import fastqSangerIterator,fastqSolexaIterator, \ + fastqIlluminaIterator +from obitools.fastq import fastqAAIterator +from obitools.fastq import formatFastq + + diff --git a/obitools/format/sequence/fnaqual.py b/obitools/format/sequence/fnaqual.py new file mode 100644 index 0000000..ab69916 --- /dev/null +++ b/obitools/format/sequence/fnaqual.py @@ -0,0 +1,8 @@ +''' +Created on 12 oct. 2009 + +@author: coissac +''' + +from obitools.fnaqual.fasta import fnaFastaIterator +from obitools.fnaqual.quality import qualityIterator diff --git a/obitools/format/sequence/genbank.py b/obitools/format/sequence/genbank.py new file mode 100644 index 0000000..8524b6f --- /dev/null +++ b/obitools/format/sequence/genbank.py @@ -0,0 +1,4 @@ +from obitools.seqdb.genbank.parser import genpepIterator,genpepParser +from obitools.seqdb.genbank.parser import genbankIterator,genbankParser + + diff --git a/obitools/format/sequence/tagmatcher.py b/obitools/format/sequence/tagmatcher.py new file mode 100644 index 0000000..60ad8d8 --- /dev/null +++ b/obitools/format/sequence/tagmatcher.py @@ -0,0 +1,5 @@ +from obitools.tagmatcher.parser import tagMatcherParser +from obitools.tagmatcher.parser import TagMatcherIterator +from obitools.tagmatcher.parser import formatTagMatcher + +tagMatcherIterator=TagMatcherIterator diff --git a/obitools/goa/__init__.py b/obitools/goa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/goa/parser.py b/obitools/goa/parser.py new file mode 100644 index 0000000..8ffd1e3 --- /dev/null +++ b/obitools/goa/parser.py @@ -0,0 +1,33 @@ +from itertools import imap +from obitools import utils + +class GoAFileIterator(utils.ColumnFile): + def __init__(self,stream): + utils.ColumnFile.__init__(self, + stream, '\t', True, + (str,)) + + _colname = ['database', + 'ac', + 'symbol', + 'qualifier', + 'goid', + 'origin', + 'evidence', + 'evidnce_origine', + 'namespace', + 'db_object_name', + 'gene', + 'object_type', + 'taxid', + 'date', + 'assigned_by'] + + def next(self): + data = utils.ColumnFile.next(self) + data = dict(imap(None,GoAFileIterator._colname,data)) + + return data + + + diff --git a/obitools/graph/__init__.py b/obitools/graph/__init__.py new file mode 100644 index 0000000..fbc5253 --- /dev/null +++ b/obitools/graph/__init__.py @@ -0,0 +1,962 @@ +''' +**obitool.graph** for representing graph structure in obitools +-------------------------------------------------------------- + +.. codeauthor:: Eric Coissac + + +This module offert classes to manipulate graphs, mainly trough the +:py:class:`obitools.graph.Graph` class. + +.. inheritance-diagram:: Graph DiGraph UndirectedGraph + :parts: 2 + +''' + +import sys + + +from obitools.utils import progressBar + + +class Indexer(dict): + ''' + Allow to manage convertion between an arbitrarly hashable python + value and an unique integer key + ''' + + def __init__(self): + + self.__max=0 + self.__reverse=[] + + def getLabel(self,index): + ''' + Return the python value associated to an integer index. + + :param index: an index value + :type index: int + + :raises: IndexError if the index is not used in this + Indexer instance + ''' + return self.__reverse[index] + + def getIndex(self,key,strict=False): + ''' + Return the index associated to a **key** in the indexer. Two + modes are available : + + - strict mode : + + if the key is not known by the :py:class:`Indexer` instance + a :py:exc:`KeyError` exception is raised. + + - non strict mode : + + in this mode if the requested *key** is absent, it is added to + the :py:class:`Indexer` instance and the new index is returned + + :param key: the requested key + :type key: a hashable python value + + :param strict: select the looking for mode + :type strict: bool + + :return: the index corresponding to the key + :rtype: int + + :raises: - :py:exc:`KeyError` in strict mode is key is absent + of the :py:class:`Indexer` instance + + - :py:exc:`TypeError` if key is not an hashable value. + ''' + if dict.__contains__(self,key): + return dict.__getitem__(self,key) + elif strict: + raise KeyError,key + else: + value = self.__max + self[key]= value + self.__reverse.append(key) + self.__max+=1 + return value + + def __getitem__(self,key): + ''' + Implement the [] operateor to emulate the standard dictionnary + behaviour on :py:class:`Indexer` and returns the integer key + associated to a python value. + + Actually this method call the:py:meth:`getIndex` method in + non strict mode so it only raises an :py:exc:`TypeError` + if key is not an hashable value. + + :param key: the value to index + :type key: an hashable python value + + :return: an unique integer value associated to the key + :rtype: int + + :raises: :py:exc:`TypeError` if **key** is not an hashable value. + + ''' + return self.getIndex(key) + + def __equal__(self,index): + ''' + Implement equal operator **==** for comparing two :py:class:`Indexer` instances. + Two :py:class:`Indexer` instances are equals only if they are physically + the same instance + + :param index: the second Indexer + :type index: an :py:class:`Indexer` instance + + :return: True is the two :py:class:`Indexer` instances are the same + :rtype: bool + ''' + return id(self)==id(index) + + +class Graph(object): + ''' + Class used to represent directed or undirected graph. + + .. warning:: + + Only one edge can connect two nodes in a given direction. + + .. warning:: + + Specifying nodes through their index seepud your code but as no check + is done on index value, it may result in inconsistency. So prefer the + use of node label to specify a node. + + + ''' + def __init__(self,label='G',directed=False,indexer=None,nodes=None,edges=None): + ''' + :param label: Graph name, set to 'G' by default + :type label: str + + :param directed: true for directed graph, set to False by defalt + :type directed: boolean + + :param indexer: node label indexer. This allows to define several graphs + sharing the same indexer (see : :py:meth:`newEmpty`) + :type indexer: :py:class:`Indexer` + + :param nodes: set of nodes to add to the graph + :type nodes: iterable value + + :param edges: set of edges to add to the graph + :type edges: iterable value + ''' + + self._directed=directed + if indexer is None: + indexer = Indexer() + self._index = indexer + self._node = {} + self._node_attrs = {} + self._edge_attrs = {} + self._label=label + + def newEmpty(self): + """ + Build a new empty graph using the same :py:class:`Indexer` instance. + This allows two graph for sharing their vertices through their indices. + """ + n = Graph(self._label+"_compact",self._directed,self._index) + + return n + + def addNode(self,node=None,index=None,**data): + ''' + Add a new node or update an existing one. + + :param node: the new node label or the label of an existing node + for updating it. + :type node: an hashable python value + + :param index: the index of an existing node for updating it. + :type index: int + + :return: the index of the node + :rtype: int + + :raises: :py:exc:`IndexError` is index is not **None** and + corresponds to a not used index in this graph. + ''' + if index is None: + index = self._index[node] + + if index not in self._node: + self._node[index]=set() + else: + if index not in self._node: + raise IndexError,"This index is not used in this graph" + + if data: + if index in self._node_attrs: + self._node_attrs[index].update(data) + else: + self._node_attrs[index]=dict(data) + + return index + + def __contains__(self,node): + try: + index = self._index.getIndex(node,strict=True) + r = index in self._node + except KeyError: + r=False + return r + + def getNode(self,node=None,index=None): + """ + :param node: a node label. + :type node: an hashable python value + + :param index: the index of an existing node. + :type index: int + + .. note:: Index value are prevalent over node label. + + :return: the looked for node + :rtype: :py:class:`Node` + + :raises: :py:exc:`IndexError` if specified node lablel + corresponds to a non-existing node. + + .. warning:: no check on index value + """ + if index is None: + index = self._index.getIndex(node, True) + return Node(index,self) + + def getBestNode(self,estimator): + ''' + Select the node maximizing the estimator function + + :param estimator: the function to maximize + :type estimator: a function returning a numerical value and accepting one + argument of type :py:class:`Node` + + :return: the best node + :rtype: py:class:`Node` + ''' + + bestScore=0 + best=None + for n in self: + score = estimator(n) + if best is None or score > bestScore: + bestScore = score + best=n + return best + + + def delNode(self,node=None,index=None): + """ + Delete a node from a graph and all associated edges. + + :param node: a node label. + :type node: an hashable python value + + :param index: the index of an existing node. + :type index: int + + .. note:: Index value are prevalent over node label. + + :raises: :py:exc:`IndexError` if specified node lablel + corresponds to a non-existing node. + + .. warning:: no check on index value + """ + if index is None: + index = self._index[node] + + for n in self._node: + if n!=index: + e = self._node[n] + if index in e: + if (n,index) in self._edge_attrs: + del self._edge_attrs[(n,index)] + e.remove(index) + + e = self._node[index] + + for n in e: + if (index,n) in self._edge_attrs: + del self._edge_attrs[(index,n)] + + del self._node[index] + if index in self._node_attrs: + del self._node_attrs[index] + + + def addEdge(self,node1=None,node2=None,index1=None,index2=None,**data): + ''' + Create a new edge in the graph between both the specified nodes. + + .. note:: Nodes can be specified using their label or their index in the graph + if both values are indicated the index is used. + + :param node1: The first vertex label + :type node1: an hashable python value + :param node2: The second vertex label + :type node2: an hashable python value + :param index1: The first vertex index + :type index1: int + :param index2: The second vertex index + :type index2: int + + :raises: :py:exc:`IndexError` if one of both the specified node lablel + corresponds to a non-existing node. + + + .. warning:: no check on index value + ''' + + index1=self.addNode(node1, index1) + index2=self.addNode(node2, index2) + + self._node[index1].add(index2) + + if not self._directed: + self._node[index2].add(index1) + + if data: + if (index1,index2) not in self._edge_attrs: + data =dict(data) + self._edge_attrs[(index1,index2)]=data + if not self._directed: + self._edge_attrs[(index2,index1)]=data + else: + self._edge_attrs[(index2,index1)].update(data) + + return (index1,index2) + + def getEdge(self,node1=None,node2=None,index1=None,index2=None): + ''' + Extract the :py:class:`Edge` instance linking two nodes of the graph. + + .. note:: Nodes can be specified using their label or their index in the graph + if both values are indicated the index is used. + + :param node1: The first vertex label + :type node1: an hashable python value + :param node2: The second vertex label + :type node2: an hashable python value + :param index1: The first vertex index + :type index1: int + :param index2: The second vertex index + :type index2: int + + :raises: :py:exc:`IndexError` if one of both the specified node lablel + corresponds to a non-existing node. + + + .. warning:: no check on index value + ''' + node1=self.getNode(node1, index1) + node2=self.getNode(node2, index2) + return Edge(node1,node2) + + def delEdge(self,node1=None,node2=None,index1=None,index2=None): + """ + Delete the edge linking node 1 to node 2. + + .. note:: Nodes can be specified using their label or their index in the graph + if both values are indicated the index is used. + + + :param node1: The first vertex label + :type node1: an hashable python value + :param node2: The second vertex label + :type node2: an hashable python value + :param index1: The first vertex index + :type index1: int + :param index2: The second vertex index + :type index2: int + + :raises: :py:exc:`IndexError` if one of both the specified node lablel + corresponds to a non-existing node. + + + .. warning:: no check on index value + """ + if index1 is None: + index1 = self._index[node1] + if index2 is None: + index2 = self._index[node2] + if index1 in self._node and index2 in self._node[index1]: + self._node[index1].remove(index2) + if (index1,index2) in self._node_attrs: + del self._node_attrs[(index1,index2)] + if not self._directed: + self._node[index2].remove(index1) + if (index2,index1) in self._node_attrs: + del self._node_attrs[(index2,index1)] + + def edgeIterator(self,predicate=None): + """ + Iterate through a set of selected vertices. + + :param predicate: a function allowing node selection. Default value + is **None** and indicate that all nodes are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Edge` + + :return: an iterator over selected edge + :rtype: interator over :py:class:`Edge` instances + + .. seealso:: + function :py:func:`selectEdgeAttributeFactory` for simple predicate. + + """ + for n1 in self._node: + for n2 in self._node[n1]: + if self._directed or n1 <= n2: + e = self.getEdge(index1=n1, index2=n2) + if predicate is None or predicate(e): + yield e + + + def nodeIterator(self,predicate=None): + """ + Iterate through a set of selected vertices. + + :param predicate: a function allowing edge selection. Default value + is **None** and indicate that all edges are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Node` + + :return: an iterator over selected nodes. + :rtype: interator over :py:class:`Node` instances + + """ + for n in self._node: + node = self.getNode(index=n) + if predicate is None or predicate(node): + yield node + + def nodeIndexIterator(self,predicate=None): + """ + Iterate through the indexes of a set of selected vertices. + + :param predicate: a function allowing edge selection. Default value + is **None** and indicate that all edges are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Node` + + :return: an iterator over selected node indices. + :rtype: interator over `int` + + """ + for n in self._node: + node = self.getNode(index=n) + if predicate is None or predicate(node): + yield n + + def neighbourIndexSet(self,node=None,index=None): + if index is None: + index=self.getNode(node).index + return self._node[index] + + def edgeCount(self): + n = reduce(lambda x,y:x+y, (len(z) for z in self._node.itervalues()),0) + if not self._directed: + n=n/2 + return n + + def subgraph(self,nodes,name='G'): + sub = Graph(name,self._directed,self._index) + if not isinstance(nodes, set): + nodes = set(nodes) + for n in nodes: + sub._node[n]=nodes & self._node[n] + if n in self._node_attrs: + sub._node_attrs[n]=dict(self._node_attrs[n]) + for n2 in sub._node[n]: + if not self._directed: + if n <= n2: + if (n,n2) in self._edge_attrs: + data=dict(self._edge_attrs[(n,n2)]) + sub._edge_attrs[(n,n2)]=data + sub._edge_attrs[(n2,n)]=data + else: + if (n,n2) in self._edge_attrs: + data=dict(self._edge_attrs[(n,n2)]) + sub._edge_attrs[(n,n2)]=data + return sub + + def __len__(self): + return len(self._node) + + def __getitem__(self,key): + return self.getNode(node=key) + + def __delitem__(self,key): + self.delNode(node=key) + + def __iter__(self): + return self.nodeIterator() + + def __str__(self): + if self._directed: + kw ='digraph' + else: + kw='graph' + + nodes = "\n ".join([str(x) for x in self]) + edges = "\n ".join([str(x) for x in self.edgeIterator()]) + + return "%s %s {\n %s\n\n %s\n}" % (kw,self._label,nodes,edges) + +class Node(object): + """ + Class used for representing one node or vertex in a graph + + """ + def __init__(self,index,graph): + ''' + .. warning:: + + :py:class:`Node` constructor is usualy called through the :py:class:`Graph` methods + + :param index: Index of the node in the graph + :type index: int + :param graph: graph instance owning the node + :type graph: :py:class:`obitools.graph.Graph` + ''' + self.index = index + self.__graph = graph + + def getGraph(self): + ''' + return graph owning this node. + + :rtype: :py:class:`obitools.graph.Graph` + ''' + return self.__graph + + + def getLabel(self): + ''' + return label associated to this node. + ''' + return self.__graph._index.getLabel(self.index) + + + def has_key(self,key): + ''' + test is the node instance has a property named 'key'. + + :param key: the name of a property + :type key: str + + :return: True if the nade has a property named + :rtype: bool + ''' + if self.index in self.__graph._node_attrs: + return key in self.__graph._node_attrs[self.index] + else: + return False + + def neighbourIterator(self,nodePredicat=None,edgePredicat=None): + ''' + iterate through the nodes directly connected to + this node. + + :param nodePredicat: a function accepting one node as parameter + and returning **True** if this node must be + returned by the iterator. + :type nodePredicat: function + + :param edgePredicat: a function accepting one edge as parameter + and returning True if the edge linking self and + the current must be considered. + :type edgePredicat: function + + + :rtype: iterator on Node instances + ''' + for n in self.neighbourIndexIterator(nodePredicat, edgePredicat): + node = self.graph.getNode(index=n) + yield node + + def neighbourIndexSet(self): + ''' + Return a set of node indexes directely connected + to this node. + + .. warning:: + + do not change this set unless you know + exactly what you do. + + @rtype: set of int + ''' + return self.__graph._node[self.index] + + def neighbourIndexIterator(self,nodePredicat=None,edgePredicat=None): + ''' + iterate through the node indexes directly connected to + this node. + + :param nodePredicat: a function accepting one node as parameter + and returning True if this node must be + returned by the iterator. + :type nodePredicat: function + + :param edgePredicat: a function accepting one edge as parameter + and returning True if the edge linking self and + the current must be considered. + :type edgePredicat: function + + :rtype: iterator on int + ''' + for n in self.neighbourIndexSet(): + if nodePredicat is None or nodePredicat(self.__graph.getNode(index=n)): + if edgePredicat is None or edgePredicat(self.__graph.getEdge(index1=self.index,index2=n)): + yield n + + def degree(self,nodeIndexes=None): + ''' + return count of edges linking this node to the + set of nodes describes by their index in nodeIndexes + + :param nodeIndexes: set of node indexes. + if set to None, all nodes of the + graph are take into account. + Set to None by default. + :type nodeIndexes: set of int + + :rtype: int + ''' + if nodeIndexes is None: + return len(self.__graph._node[self.index]) + else: + return len(self.__graph._node[self.index] & nodeIndexes) + + def componentIndexSet(self,nodePredicat=None,edgePredicat=None): + ''' + Return the set of node index in the same connected component. + + :param nodePredicat: a function accepting one node as parameter + and returning True if this node must be + returned by the iterator. + :type nodePredicat: function + + :param edgePredicat: a function accepting one edge as parameter + and returning True if the edge linking self and + the current must be considered. + :type edgePredicat: function + + + :rtype: set of int + ''' + cc=set([self.index]) + added = set(x for x in self.neighbourIndexIterator(nodePredicat, edgePredicat)) + while added: + cc |= added + added = reduce(lambda x,y : x | y, + (set(z for z in self.graph.getNode(index=c).neighbourIndexIterator(nodePredicat, edgePredicat)) + for c in added), + set()) + added -= cc + return cc + + def componentIterator(self,nodePredicat=None,edgePredicat=None): + ''' + Iterate through the nodes in the same connected + component. + + :rtype: iterator on :py:class:`Node` instance + ''' + for c in self.componentIndexSet(nodePredicat, edgePredicat): + yield self.graph.getNode(c) + + def shortestPathIterator(self,nodes=None): + ''' + Iterate through the shortest path sourcing + from this node. if nodes is not None, iterates + only path linkink this node to one node listed in + nodes + + :param nodes: set of node index + :type nodes: iterable on int + + :return: an iterator on list of int describing path + :rtype: iterator on list of int + ''' + if nodes is not None: + nodes = set(nodes) + + + Q=[(self.index,-1)] + + gray = set([self.index]) + paths = {} + + while Q and (nodes is None or nodes): + u,p = Q.pop() + paths[u]=p + next = self.graph._node[u] - gray + gray|=next + Q.extend((x,u) for x in next) + if nodes is None or u in nodes: + if nodes: + nodes.remove(u) + path = [u] + while p >= 0: + path.append(p) + p = paths[p] + path.reverse() + yield path + + def shortestPathTo(self,node=None,index=None): + ''' + return one of the shortest path linking this + node to specified node. + + :param node: a node label or None + :param index: a node index or None. the parameter index + has a priority on the parameter node. + :type index: int + + :return: list of node index corresponding to the path or None + if no path exists. + :rtype: list of int or None + ''' + if index is None: + index=self.graph.getNode(node).index + for p in self.shortestPathIterator([index]): + return p + + + def __getitem__(self,key): + ''' + return the value of the property of this node + + :param key: the name of a property + :type key: str + ''' + return self.__graph._node_attrs.get(self.index,{})[key] + + def __setitem__(self,key,value): + ''' + set the value of a node property. In the property doesn't + already exist a new property is added to this node. + + :param key: the name of a property + :type key: str + :param value: the value of the property + + .. seealso:: + + :py:meth:`Node.__getitem__` + ''' + if self.index in self.__graph._node_attrs: + data = self.__graph._node_attrs[self.index] + data[key]=value + else: + self.graph._node_attrs[self.index]={key:value} + + def __len__(self): + ''' + Count neighbour of this node + + :rtype: int + + .. seealso:: + + :py:meth:`Node.degree` + ''' + return len(self.__graph._node[self.index]) + + def __iter__(self): + ''' + iterate through neighbour of this node + + :rtype: iterator in :py:class:`Node` instances + + .. seealso:: + + :py:meth:`Node.neighbourIterator` + ''' + return self.neighbourIterator() + + def __contains__(self,key): + return self.has_key(key) + + def __str__(self): + + if self.index in self.__graph._node_attrs: + keys = " ".join(['%s="%s"' % (x[0],str(x[1]).replace('"','\\"').replace('\n','\\n')) + for x in self.__graph._node_attrs[self.index].iteritems()] + ) + else: + keys='' + + return '%d [label="%s" %s]' % (self.index, + str(self.label).replace('"','\\"').replace('\n','\\n'), + keys) + + def keys(self): + if self.index in self.__graph._node_attrs: + k = self.__graph._node_attrs[self.index].keys() + else: + k=[] + return k + + label = property(getLabel, None, None, "Label of the node") + + graph = property(getGraph, None, None, "Graph owning this node") + + + +class Edge(object): + """ + Class used for representing one edge of a graph + + """ + + def __init__(self,node1,node2): + ''' + .. warning:: + + :py:class:`Edge` constructor is usualy called through the :py:class:`Graph` methods + + :param node1: First node likend by the edge + :type node1: :py:class:`Node` + :param node2: Seconde node likend by the edge + :type node2: :py:class:`Node` + ''' + self.node1 = node1 + self.node2 = node2 + + def getGraph(self): + """ + Return the :py:class:`Graph` instance owning this edge. + """ + return self.node1.graph + + def has_key(self,key): + ''' + test is the :py:class:`Edge` instance has a property named **key**. + + :param key: the name of a property + :type key: str + + :return: True if the edge has a property named + :rtype: bool + ''' + if (self.node1.index,self.node2.index) in self.graph._edge_attrs: + return key in self.graph._edge_attrs[(self.node1.index,self.node2.index)] + else: + return False + + + def getDirected(self): + return self.node1.graph._directed + + def __getitem__(self,key): + return self.graph._edge_attrs.get((self.node1.index,self.node2.index),{})[key] + + def __setitem__(self,key,value): + e = (self.node1.index,self.node2.index) + if e in self.graph._edge_attrs: + data = self.graph._edge_attrs[e] + data[key]=value + else: + self.graph._edge_attrs[e]={key:value} + + def __str__(self): + e = (self.node1.index,self.node2.index) + if e in self.graph._edge_attrs: + keys = "[%s]" % " ".join(['%s="%s"' % (x[0],str(x[1]).replace('"','\\"')) + for x in self.graph._edge_attrs[e].iteritems()] + ) + else: + keys = "" + + if self.directed: + link='->' + else: + link='--' + + return "%d %s %d %s" % (self.node1.index,link,self.node2.index,keys) + + def __contains__(self,key): + return self.has_key(key) + + + graph = property(getGraph, None, None, "Graph owning this edge") + + directed = property(getDirected, None, None, "Directed's Docstring") + + +class DiGraph(Graph): + """ + :py:class:`DiGraph class`is a specialisation of the :py:class:`Graph` class + dedicated to directed graph representation + + .. seealso:: + + :py:class:`UndirectedGraph` + + """ + def __init__(self,label='G',indexer=None,nodes=None,edges=None): + ''' + :param label: Graph name, set to 'G' by default + :type label: str + :param indexer: node label indexer + :type indexer: Indexer instance + :param nodes: set of nodes to add to the graph + :type nodes: iterable value + :param edges: set of edges to add to the graph + :type edges: iterable value + ''' + + Graph.__init__(self, label, True, indexer, nodes, edges) + +class UndirectedGraph(Graph): + """ + :py:class:`UndirectGraph class`is a specialisation of the :py:class:`Graph` class + dedicated to undirected graph representation + + .. seealso:: + + :py:class:`DiGraph` + + """ + def __init__(self,label='G',indexer=None,nodes=None,edges=None): + ''' + :param label: Graph name, set to 'G' by default + :type label: str + :param indexer: node label indexer + :type indexer: Indexer instance + :param nodes: set of nodes to add to the graph + :type nodes: iterable value + :param edges: set of edges to add to the graph + :type edges: iterable value + ''' + + Graph.__init__(self, label, False, indexer, nodes, edges) + + + +def selectEdgeAttributeFactory(attribut,value): + """ + This function help in building predicat function usable for selecting edge + in the folowing :py:class:`Graph` methods : + + - :py:meth:`Graph.edgeIterator` + + """ + def selectEdge(e): + return attribut in e and e[attribut]==value + return selectEdge diff --git a/obitools/graph/__init__.pyc b/obitools/graph/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..397e5c0a4c37a6116f0c9d512b073f6704dd927f GIT binary patch literal 29446 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 literal 0 HcmV?d00001 diff --git a/obitools/graph/algorithms/clique.py b/obitools/graph/algorithms/clique.py new file mode 100644 index 0000000..2007c1a --- /dev/null +++ b/obitools/graph/algorithms/clique.py @@ -0,0 +1,134 @@ +import time +import sys + + + +_maxsize=0 +_solution=0 +_notbound=0 +_sizebound=0 +_lastyield=0 +_maxclique=None + +def cliqueIterator(graph,minsize=1,node=None,timeout=None): + global _maxsize,_solution,_notbound,_sizebound,_lastyield + _maxsize=0 + _solution=0 + _notbound=0 + _sizebound=0 + starttime = time.time() + + if node: + node = graph.getNode(node) + index = node.index + clique= set([index]) + candidates= set(graph.neighbourIndexSet(index=index)) + else: + clique=set() + candidates = set(x.index for x in graph) + + +# candidates = set(x for x in candidates +# if len(graph.neighbourIndexSet(index=x) & candidates) >= (minsize - 1)) + + _lastyield=time.time() + for c in _cliqueIterator(graph,clique,candidates,set(),minsize,start=starttime,timeout=timeout): + yield c + + + + + +def _cliqueIterator(graph,clique,candidates,notlist,minsize=0,start=None,timeout=None): + global _maxsize,_maxclique,_solution,_notbound,_sizebound,_lastyield + + # Speed indicator + lclique = len(clique) + lcandidates = len(candidates) + notmin = lcandidates + notfix = None + + for n in notlist: + nnc = candidates - graph.neighbourIndexSet(index=n) + nc = len(nnc) + if nc < notmin: + notmin=nc + notfix=n + notfixneib = nnc + + if lclique > _maxsize or not _solution % 1000 : + if start is not None: + top = time.time() + delta = top - start + if delta==0: + delta=1e-6 + speed = _solution / delta + start = top + else: + speed = 0 + print >>sys.stderr,"\rCandidates : %-5d Maximum clique size : %-5d Solutions explored : %10d speed = %5.2f solutions/sec sizebound=%10d notbound=%10d " % (lcandidates,_maxsize,_solution,speed,_sizebound,_notbound), + sys.stderr.flush() + if lclique > _maxsize: + _maxsize=lclique + +# print >>sys.stderr,'koukou' + + timer = time.time() - _lastyield + + if not candidates and not notlist: + if lclique==_maxsize: + _maxclique=set(clique) + if lclique >= minsize: + yield set(clique) + if timeout is not None and timer > timeout and _maxclique is not None: + yield _maxclique + _maxclique=None + + else: + while notmin and candidates and ((lclique + len(candidates)) >= minsize or (timeout is not None and timer > timeout)): + # count explored solution + _solution+=1 + + if notfix is None: + nextcandidate = candidates.pop() + else: + nextcandidate = notfixneib.pop() + candidates.remove(nextcandidate) + + clique.add(nextcandidate) + + neighbours = graph.neighbourIndexSet(index=nextcandidate) + + nextcandidates = candidates & neighbours + nextnot = notlist & neighbours + + nnc = candidates - neighbours + lnnc=len(nnc) + + for c in _cliqueIterator(graph, + set(clique), + nextcandidates, + nextnot, + minsize, + start, + timeout=timeout): + yield c + + + clique.remove(nextcandidate) + + notmin-=1 + + if lnnc < notmin: + notmin = lnnc + notfix = nextcandidate + notfixneib = nnc + + if notmin==0: + _notbound+=1 + + notlist.add(nextcandidate) + else: + if (lclique + len(candidates)) < minsize: + _sizebound+=1 + diff --git a/obitools/graph/algorithms/compact.py b/obitools/graph/algorithms/compact.py new file mode 100644 index 0000000..8065a93 --- /dev/null +++ b/obitools/graph/algorithms/compact.py @@ -0,0 +1,8 @@ + +def compactGraph(graph,nodeSetIterator): + compact = graph.newEmpty() + for ns in nodeSetIterator(graph): + nlabel = "\n".join([str(graph.getNode(index=x).label) for x in ns]) + compact.addNode(nlabel) + print + print compact diff --git a/obitools/graph/algorithms/component.py b/obitools/graph/algorithms/component.py new file mode 100644 index 0000000..a17c8dd --- /dev/null +++ b/obitools/graph/algorithms/component.py @@ -0,0 +1,82 @@ +""" +Iterate through the connected components of a graph +--------------------------------------------------- + +the module :py:mod:`obitools.graph.algorithm.component` provides +two functions to deal with the connected component of a graph +represented as a :py:class:`obitools.graph.Graph` instance. + +The whole set of connected component of a graph is a partition of this graph. +So a node cannot belongs to two distinct connected component. + +Two nodes are in the same connected component if it exits a path through +the graph edges linking them. + +TODO: THere is certainly a bug with DirectedGraph + +""" + +def componentIterator(graph,nodePredicat=None,edgePredicat=None): + ''' + Build an iterator over the connected component of a graph. + Each connected component returned by the iterator is represented + as a `set` of node indices. + + :param graph: the graph to partitionne + :type graph: :py:class:`obitools.graph.Graph` + + :param predicate: a function allowing edge selection. Default value + is **None** and indicate that all edges are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Node` + + :param predicate: a function allowing node selection. Default value + is **None** and indicate that all nodes are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Edge` + + :return: an iterator over the connected component set + :rtype: an iterator over `set` of `int` + + .. seealso:: + the :py:meth:`obitools.graph.Graph.componentIndexSet` method + on which is based this function. + ''' + seen = set() + for n in graph.nodeIterator(nodePredicat): + if n.index not in seen: + cc=n.componentIndexSet(nodePredicat, edgePredicat) + yield cc + seen |= cc + +def componentCount(graph,nodePredicat=None,edgePredicat=None): + ''' + Count the connected componnent in a graph. + + :param graph: the graph to partitionne + :type graph: :py:class:`obitools.graph.Graph` + + :param predicate: a function allowing edge selection. Default value + is **None** and indicate that all edges are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Node` + + :param predicate: a function allowing node selection. Default value + is **None** and indicate that all nodes are selected. + :type predicate: a function returning a boolean value + and accepting one argument of class :py:class:`Edge` + + :return: an iterator over the connected component set + :rtype: an iterator over `set` of `int` + + .. seealso:: + the :py:func:`componentIterator` function + on which is based this function. + ''' + n=0 + for c in componentIterator(graph,nodePredicat, edgePredicat): + n+=1 + return n + + + \ No newline at end of file diff --git a/obitools/graph/algorithms/component.pyc b/obitools/graph/algorithms/component.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3b62981bc02b19efc679b4f642096ba39efbabe GIT binary patch literal 950 zcmd5)&2AGh5FYO){ek`jdgr_nN(>yRDj`IFF1>6a0V-AEu<=@{>FzqU9g#}Ar}V~y z@Ls$E4*=g7)JnVnSf25CJRZ+{pVJ@P559i?mMi)*Qmk(vK7%ST1yw{B(SThhbeqsc zLS;&~@TVLw$$AJjKQVH}3DzL&;;jB1R3KUrbtx?msU`2#h{rh=9gg$+s2mqt<$AZMTibv&h8!Dg4amFk2N8IR*G?FZ4PT7*rJb4( z17n4I8MD;|r!P+U2mupC4DQz%f@30gj3 zTENCifRF)o5ql84kHhOdItLSzh;VLji45n8xQ{p4;*{g4)?r1A{s(Y&P|O`#$9B<# zhw%6fC=zR8Pwa^JU4MU@8y}eE96u#?HRj-ZivKOoT=T2imWxWiX7D|ix|Qs1`~upN B= 2GB when viewed as a 32-bit unsigned int, return a long. + """ + if i < 0: + i += 1L << 32 + return i + +def LOWU32(i): + """Return the low-order 32 bits of an int, as a non-negative int.""" + return i & 0xFFFFFFFFL + +def write32(output, value): + output.write(struct.pack("' + + def _init_write(self, filename): + if filename[-3:] != '.gz': + filename = filename + '.gz' + self.filename = filename + self.crc = zlib.crc32("") + self.size = 0 + self.writebuf = [] + self.bufsize = 0 + + def _write_gzip_header(self): + self.fileobj.write('\037\213') # magic header + self.fileobj.write('\010') # compression method + fname = self.filename[:-3] + flags = 0 + if fname: + flags = FNAME + self.fileobj.write(chr(flags)) + write32u(self.fileobj, long(time.time())) + self.fileobj.write('\002') + self.fileobj.write('\377') + if fname: + self.fileobj.write(fname + '\000') + + def _init_read(self): + self.crc = zlib.crc32("") + self.size = 0 + + def _read_internal(self, size): + if len(self.inputbuf) < size: + self.inputbuf += self.fileobj.read(size-len(self.inputbuf)) + chunk = self.inputbuf[:size] + # need to use len(chunk) bellow instead of size in case it's EOF. + if len(chunk) < 8: + self.last8 = self.last8[len(chunk):] + chunk + else: + self.last8 = chunk[-8:] + self.inputbuf = self.inputbuf[size:] + return chunk + + def _read_gzip_header(self): + magic = self._read_internal(2) + if len(magic) != 2: + raise EOFError, "Reached EOF" + if magic != '\037\213': + raise IOError, 'Not a gzipped file' + method = ord( self._read_internal(1) ) + if method != 8: + raise IOError, 'Unknown compression method' + flag = ord( self._read_internal(1) ) + # modtime = self.fileobj.read(4) + # extraflag = self.fileobj.read(1) + # os = self.fileobj.read(1) + self._read_internal(6) + + if flag & FEXTRA: + # Read & discard the extra field, if present + xlen = ord(self._read_internal(1)) + xlen = xlen + 256*ord(self._read_internal(1)) + self._read_internal(xlen) + if flag & FNAME: + # Read and discard a null-terminated string containing the filename + while True: + s = self._read_internal(1) + if not s or s=='\000': + break + if flag & FCOMMENT: + # Read and discard a null-terminated string containing a comment + while True: + s = self._read_internal(1) + if not s or s=='\000': + break + if flag & FHCRC: + self._read_internal(2) # Read & discard the 16-bit header CRC + + + def write(self,data): + if self.mode != WRITE: + import errno + raise IOError(errno.EBADF, "write() on read-only GzipFile object") + + if self.fileobj is None: + raise ValueError, "write() on closed GzipFile object" + if len(data) > 0: + self.size = self.size + len(data) + self.crc = zlib.crc32(data, self.crc) + self.fileobj.write( self.compress.compress(data) ) + self.offset += len(data) + + def read(self, size=-1): + if self.mode != READ: + import errno + raise IOError(errno.EBADF, "read() on write-only GzipFile object") + + if self.extrasize <= 0 and self.fileobj is None: + return '' + + readsize = 1024 + if size < 0: # get the whole thing + try: + while True: + self._read(readsize) + readsize = min(self.max_read_chunk, readsize * 2) + except EOFError: + size = self.extrasize + else: # just get some more of it + try: + while size > self.extrasize: + self._read(readsize) + readsize = min(self.max_read_chunk, readsize * 2) + except EOFError: + if size > self.extrasize: + size = self.extrasize + + chunk = self.extrabuf[:size] + self.extrabuf = self.extrabuf[size:] + self.extrasize = self.extrasize - size + + self.offset += size + return chunk + + def _unread(self, buf): + self.extrabuf = buf + self.extrabuf + self.extrasize = len(buf) + self.extrasize + self.offset -= len(buf) + + def _read(self, size=1024): + if self.fileobj is None: + raise EOFError, "Reached EOF" + + if self._new_member: + # If the _new_member flag is set, we have to + # jump to the next member, if there is one. + # + # _read_gzip_header will raise EOFError exception + # if there no more members to read. + self._init_read() + self._read_gzip_header() + self.decompress = zlib.decompressobj(-zlib.MAX_WBITS) + self._new_member = False + + # Read a chunk of data from the file + buf = self._read_internal(size) + + # If the EOF has been reached, flush the decompression object + # and mark this object as finished. + + if buf == "": + uncompress = self.decompress.flush() + self._read_eof() + self._add_read_data( uncompress ) + raise EOFError, 'Reached EOF' + + uncompress = self.decompress.decompress(buf) + self._add_read_data( uncompress ) + + if self.decompress.unused_data != "": + # Ending case: we've come to the end of a member in the file, + # so put back unused_data and initialize last8 by reading them. + self.inputbuf = self.decompress.unused_data + self.inputbuf + self._read_internal(8) + + # Check the CRC and file size, and set the flag so we read + # a new member on the next call + self._read_eof() + self._new_member = True + + def _add_read_data(self, data): + self.crc = zlib.crc32(data, self.crc) + self.extrabuf = self.extrabuf + data + self.extrasize = self.extrasize + len(data) + self.size = self.size + len(data) + + def _read_eof(self): + # We've read to the end of the file, so we have to rewind in order + # to reread the 8 bytes containing the CRC and the file size. + # We check the that the computed CRC and size of the + # uncompressed data matches the stored values. Note that the size + # stored is the true file size mod 2**32. + crc32 = unpack32(self.last8[:4]) + isize = U32(unpack32(self.last8[4:])) # may exceed 2GB + if U32(crc32) != U32(self.crc): + raise IOError, "CRC check failed" + elif isize != LOWU32(self.size): + raise IOError, "Incorrect length of data produced" + + def close(self): + if self.mode == WRITE: + self.fileobj.write(self.compress.flush()) + # The native zlib crc is an unsigned 32-bit integer, but + # the Python wrapper implicitly casts that to a signed C + # long. So, on a 32-bit box self.crc may "look negative", + # while the same crc on a 64-bit box may "look positive". + # To avoid irksome warnings from the `struct` module, force + # it to look positive on all boxes. + write32u(self.fileobj, LOWU32(self.crc)) + # self.size may exceed 2GB, or even 4GB + write32u(self.fileobj, LOWU32(self.size)) + self.fileobj = None + elif self.mode == READ: + self.fileobj = None + if self.myfileobj: + self.myfileobj.close() + self.myfileobj = None + + def __del__(self): + try: + if (self.myfileobj is None and + self.fileobj is None): + return + except AttributeError: + return + self.close() + + def flush(self,zlib_mode=zlib.Z_SYNC_FLUSH): + if self.mode == WRITE: + # Ensure the compressor's buffer is flushed + self.fileobj.write(self.compress.flush(zlib_mode)) + self.fileobj.flush() + + def fileno(self): + """Invoke the underlying file object's fileno() method. + + This will raise AttributeError if the underlying file object + doesn't support fileno(). + """ + return self.fileobj.fileno() + + def isatty(self): + return False + + def tell(self): + return self.offset + + def rewind(self): + '''Return the uncompressed stream file position indicator to the + beginning of the file''' + if self.mode != READ: + raise IOError("Can't rewind in write mode") + self.fileobj.seek(0) + self._new_member = True + self.extrabuf = "" + self.extrasize = 0 + self.offset = 0 + + def seek(self, offset): + if self.mode == WRITE: + if offset < self.offset: + raise IOError('Negative seek in write mode') + count = offset - self.offset + for i in range(count // 1024): + self.write(1024 * '\0') + self.write((count % 1024) * '\0') + elif self.mode == READ: + if offset < self.offset: + # for negative seek, rewind and do positive seek + self.rewind() + count = offset - self.offset + for i in range(count // 1024): + self.read(1024) + self.read(count % 1024) + + def readline(self, size=-1): + if size < 0: + size = sys.maxint + readsize = self.min_readsize + else: + readsize = size + bufs = [] + while size != 0: + c = self.read(readsize) + i = c.find('\n') + + # We set i=size to break out of the loop under two + # conditions: 1) there's no newline, and the chunk is + # larger than size, or 2) there is a newline, but the + # resulting line would be longer than 'size'. + if (size <= i) or (i == -1 and len(c) > size): + i = size - 1 + + if i >= 0 or c == '': + bufs.append(c[:i + 1]) # Add portion of last chunk + self._unread(c[i + 1:]) # Push back rest of chunk + break + + # Append chunk to list, decrease 'size', + bufs.append(c) + size = size - len(c) + readsize = min(size, readsize * 2) + if readsize > self.min_readsize: + self.min_readsize = min(readsize, self.min_readsize * 2, 512) + return ''.join(bufs) # Return resulting line + + def readlines(self, sizehint=0): + # Negative numbers result in reading all the lines + if sizehint <= 0: + sizehint = sys.maxint + L = [] + while sizehint > 0: + line = self.readline() + if line == "": + break + L.append(line) + sizehint = sizehint - len(line) + + return L + + def writelines(self, L): + for line in L: + self.write(line) + + def __iter__(self): + return self + + def next(self): + line = self.readline() + if line: + return line + else: + raise StopIteration + + +def _test(): + # Act like gzip; with -d, act like gunzip. + # The input file is not deleted, however, nor are any other gzip + # options or features supported. + args = sys.argv[1:] + decompress = args and args[0] == "-d" + if decompress: + args = args[1:] + if not args: + args = ["-"] + for arg in args: + if decompress: + if arg == "-": + f = GzipFile(filename="", mode="rb", fileobj=sys.stdin) + g = sys.stdout + else: + if arg[-3:] != ".gz": + print "filename doesn't end in .gz:", repr(arg) + continue + f = open(arg, "rb") + g = __builtin__.open(arg[:-3], "wb") + else: + if arg == "-": + f = sys.stdin + g = GzipFile(filename="", mode="wb", fileobj=sys.stdout) + else: + f = __builtin__.open(arg, "rb") + g = open(arg + ".gz", "wb") + while True: + chunk = f.read(1024) + if not chunk: + break + g.write(chunk) + if g is not sys.stdout: + g.close() + if f is not sys.stdin: + f.close() + +if __name__ == '__main__': + _test() diff --git a/obitools/gzip.pyc b/obitools/gzip.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c44a43330109a91b1f029e3e152f998543005ad GIT binary patch literal 17277 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 y.begin: + return 1 + if self.isDirect() == y.isDirect(): + return 0 + if self.isDirect() and not y.isDirect(): + return -1 + return 1 + +class SimpleLocation(Location): + """ + A simple location is describe a continuous region of + a sequence define by a C{begin} and a C{end} position. + """ + + def __init__(self,begin,end): + ''' + Build a new C{SimpleLocation} instance. Valid + position are define on M{[1,N]} with N the length + of the sequence. + + @param begin: start position of the location + @type begin: int + @param end: end position of the location + @type end: int + ''' + assert begin > 0 and end > 0 + + self._begin = begin + self._end = end + self._before=False + self._after=False + + def _extractSequence(self,sequence): + + assert ( self._begin < len(sequence) + and self._end <= len(sequence)), \ + "Sequence length %d is too short" % len(sequence) + + return sequence[self._begin-1:self._end] + + def _extractQuality(self,sequence): + + assert ( self._begin < len(sequence) + and self._end <= len(sequence)), \ + "Sequence length %d is too short" % len(sequence) + + return sequence.quality[self._begin-1:self._end] + + + def isDirect(self): + return True + + def isSimple(self): + return True + + def isFullLength(self): + return not (self.before or self.after) + + def simplify(self): + if self._begin == self._end: + return PointLocation(self._begin) + else: + return self + + def needNucleic(self): + return False + + def __str__(self): + before = {True:'<',False:''}[self.before] + after = {True:'>',False:''}[self.after] + return "%s%d..%s%d" % (before,self._begin,after,self._end) + + def shift(self,s): + assert (self._begin + s) > 0,"shift to large (%d)" % s + if s == 0: + return self + return SimpleLocation(self._begin + s, self._end + s) + + def _getglocpos(self): + return (self.begin,self.end) + + def getGloc(self): + positions = ','.join([str(x) for x in self._getglocpos()]) + return "(%s,%s)" % ({True:'T',False:'F'}[self.isDirect()], + positions) + + def getBegin(self): + return self._begin + + def getEnd(self): + return self._end + + + begin = property(getBegin,None,None,"beginning position of the location") + end = property(getEnd,None,None,"ending position of the location") + + def getBefore(self): + return self._before + + def getAfter(self): + return self._after + + def setBefore(self,value): + assert isinstance(value, bool) + self._before=value + + def setAfter(self,value): + assert isinstance(value, bool) + self._after=value + + before=property(getBefore,setBefore,None) + after=property(getAfter,setAfter,None) + + + + +class PointLocation(Location): + """ + A point location describes a location on a sequence + limited to a single position + """ + + def __init__(self,position): + assert position > 0 + self._pos=position + + def _extractSequence(self,sequence): + + assert self._end <= len(sequence), \ + "Sequence length %d is too short" % len(sequence) + + return sequence[self._pos-1] + + def _extractQuality(self,sequence): + + assert self._end <= len(sequence), \ + "Sequence length %d is too short" % len(sequence) + + return sequence[self._pos-1:self._pos] + + def isDirect(self): + return True + + def isSimple(self): + return True + + def isFullLength(self): + return True + + def simplify(self): + return self + + def needNucleic(self): + return False + + def shift(self,s): + assert (self._pos + s) > 0,"shift to large (%d)" % s + if s == 0: + return self + return PointLocation(self._pos + s) + + def _getglocpos(self): + return (self._pos,self._pos) + + def getBegin(self): + return self._pos + + def getEnd(self): + return self._pos + + begin = property(getBegin,None,None,"beginning position of the location") + end = property(getEnd,None,None,"ending position of the location") + + def __str__(self): + return str(self._pos) + +class CompositeLocation(Location): + """ + """ + def __init__(self,locations): + self._locs = tuple(locations) + + + def _extractSequence(self,sequence): + seq = ''.join([x._extractSequence(sequence) + for x in self._locs]) + return seq + + def _extractQuality(self,sequence): + rep=array.array('d',[]) + for x in self._locs: + rep.extend(x._extractQuality(sequence)) + return rep + + def isDirect(self): + hasDirect,hasReverse = reduce(lambda x,y: (x[0] or y,x[1] or not y), + (z.isDirect() for z in self._locs),(False,False)) + + if hasDirect and not hasReverse: + return True + if hasReverse and not hasDirect: + return False + + return None + + + def isSimple(self): + return False + + + def simplify(self): + if len(self._locs)==1: + return self._locs[0] + + rep = CompositeLocation(x.simplify() for x in self._locs) + + if reduce(lambda x,y : x and y, + (isinstance(z, ComplementLocation) + for z in self._locs)): + rep = ComplementLocation(CompositeLocation(x._loc.simplify() + for x in rep._locs[::-1])) + + return rep + + def isFullLength(self): + return reduce(lambda x,y : x and y, (z.isFullLength() for z in self._locs),1) + + def needNucleic(self): + return reduce(lambda x,y : x or y, + (z.needNucleic for z in self._locs), + False) + + def _getglocpos(self): + return reduce(lambda x,y : x + y, + (z._getglocpos() for z in self._locs)) + + + def getBegin(self): + return min(x.getBegin() for x in self._locs) + + def getEnd(self): + return max(x.getEnd() for x in self._locs) + + def shift(self,s): + assert (self.getBegin() + s) > 0,"shift to large (%d)" % s + if s == 0: + return self + return CompositeLocation(x.shift(s) for x in self._locs) + + + begin = property(getBegin,None,None,"beginning position of the location") + end = property(getEnd,None,None,"ending position of the location") + + + def __str__(self): + return "join(%s)" % ','.join([str(x) + for x in self._locs]) + +class ComplementLocation(Location): + """ + """ + + _comp={'a': 't', 'c': 'g', 'g': 'c', 't': 'a', + 'r': 'y', 'y': 'r', 'k': 'm', 'm': 'k', + 's': 's', 'w': 'w', 'b': 'v', 'd': 'h', + 'h': 'd', 'v': 'b', 'n': 'n', 'u': 'a', + '-': '-'} + + def __init__(self,location): + self._loc = location + + def _extractSequence(self,sequence): + seq = self._loc._extractSequence(sequence) + seq = ''.join([ComplementLocation._comp.get(x.lower(),'n') for x in seq[::-1]]) + return seq + + def _extractQuality(self,sequence): + return sequence.quality[::-1] + + def isDirect(self): + return False + + def isSimple(self): + return self._loc.isSimple() + + def isFullLength(self): + return self._loc.isFullLength() + + def simplify(self): + if isinstance(self._loc, ComplementLocation): + return self._loc._loc.simplify() + else: + return self + + def needNucleic(self): + return True + + def __str__(self): + return "complement(%s)" % self._loc + + def shift(self,s): + assert (self.getBegin() + s) > 0,"shift to large (%d)" % s + if s == 0: + return self + return ComplementLocation(self._loc.shift(s)) + + def _getglocpos(self): + return self._loc._getglocpos() + + def getBegin(self): + return self._loc.getBegin() + + def getEnd(self): + return self._loc.getEnd() + + def getFivePrime(self): + return self.getEnd() + + def getThreePrime(self): + return self.getBegin() + + + begin = property(getBegin,None,None,"beginning position of the location") + end = property(getEnd,None,None,"ending position of the location") + fivePrime=property(getFivePrime,None,None,"5' potisition of the location") + threePrime=property(getThreePrime,None,None,"3' potisition of the location") + + + # + # Internal functions used for location parsing + # + +def __sublocationIterator(text): + sl = [] + plevel=0 + for c in text: + assert plevel>=0,"Misformated location : %s" % text + if c == '(': + plevel+=1 + sl.append(c) + elif c==')': + plevel-=1 + sl.append(c) + elif c==',' and plevel == 0: + assert sl,"Misformated location : %s" % text + yield ''.join(sl) + sl=[] + else: + sl.append(c) + assert sl and plevel==0,"Misformated location : %s" % text + yield ''.join(sl) + + + + # + # Internal functions used for location parsing + # + +__simplelocparser = re.compile('(?P[0-9]+)(\.\.(?P>?)(?P[0-9]+))?') + + +def __locationParser(text): + text=text.strip() + if text[0:5]=='join(': + assert text[-1]==')',"Misformated location : %s" % text + return CompositeLocation(__locationParser(sl) for sl in __sublocationIterator(text[5:-1])) + elif text[0:11]=='complement(': + assert text[-1]==')',"Misformated location : %s" % text + subl = tuple(__locationParser(sl) for sl in __sublocationIterator(text[11:-1])) + if len(subl)>1: + subl = CompositeLocation(subl) + else: + subl = subl[0] + return ComplementLocation(subl) + else: + data = __simplelocparser.match(text) + assert data is not None,"Misformated location : %s" % text + data = data.groupdict() + if not data['to'] : + sl = PointLocation(int(data['from'])) + else: + sl = SimpleLocation(int(data['from']),int(data['to'])) + sl.before=data['before']=='<' + sl.after=data['after']=='>' + return sl + +def locationGenerator(locstring): + ''' + Parse a location string as present in genbank or embl file. + + @param locstring: string description of the location in embl/gb format + @type locstring: str + + @return: a Location instance + @rtype: C{Location} subclass instance + ''' + return __locationParser(locstring) + + +_matchExternalRef = re.compile('[A-Za-z0-9_|]+(\.[0-9]+)?(?=:)') + +def extractExternalRefs(locstring): + ''' + When a location describe external references (ex: D28156.1:1..>1292) + separate the external reference part of the location and the location + by itself. + + @param locstring: text representation of the location. + @type locstring: str + + @return: a tuple with a set of string describing accession number + of the referred sequences and a C{Location} instance. + + @rtype: tuple(set,Location) + ''' + m = set(x.group() for x in _matchExternalRef.finditer(locstring)) + clean = re.compile(':|'.join([re.escape(x) for x in m])+':') + cloc = locationGenerator(clean.sub('',locstring)) + + return m,cloc + + + + + diff --git a/obitools/location/__init__.pyc b/obitools/location/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..545f02466716c0a3360e283e71dbe080c6ee6f32 GIT binary patch literal 30440 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 0,"shift to large (%d)" % s + if s == 0: + return self + f = Feature(self._fttype,self._loc.shift(s)) + f.update(self) + return f + + + def getBegin(self): + return self._loc.getBegin() + + def getEnd(self): + return self._loc.getEnd() + + begin = property(getBegin,None,None,"beginning position of the location") + end = property(getEnd,None,None,"ending position of the location") + + +def featureFactory(featureDescription): + fttype,location,qualifiers = ftParser(featureDescription) + location = locationGenerator(location) + feature = Feature(fttype,location) + feature.raw = featureDescription + + for k,v in qualifierIterator(qualifiers): + feature.setdefault(k,[]).append(v) + + return feature + +def featureIterator(featureTable,skipError=False): + for tft in textFeatureIterator(featureTable): + try: + feature = featureFactory(tft) + except AssertionError,e: + logging.debug("Parsing error on feature :\n===============\n%s\n===============" % tft) + if not skipError: + raise e + logging.debug("\t===> Error skipped") + continue + + yield feature + \ No newline at end of file diff --git a/obitools/metabarcoding/__init__.py b/obitools/metabarcoding/__init__.py new file mode 100644 index 0000000..3b29b17 --- /dev/null +++ b/obitools/metabarcoding/__init__.py @@ -0,0 +1,265 @@ +from obitools.ecopcr.options import addTaxonomyFilterOptions,\ + loadTaxonomyDatabase +from obitools.graph import UndirectedGraph +from obitools.align import lenlcs,isLCSReachable +from obitools.graph.algorithms.component import componentIterator +from obitools.utils.bioseq import uniqSequence +from obitools.utils import progressBar +import math +import sys +from obitools.graph.rootedtree import RootedTree + +def average(x): + x=list(x) + s = sum(i*j for (i,j) in x) + n = sum(i[1] for i in x) + return (float(s)/float(n),n) + +def minimum(x): + x=list(x) + m = min(i[0] for i in x) + n = sum(i[1] for i in x) + return (float(m),n) + +def ecoPCRReader(entries,options): + + taxonomy = loadTaxonomyDatabase(options) + + norankid =options.taxonomy.findRankByName('no rank') + speciesid=options.taxonomy.findRankByName('species') + genusid =options.taxonomy.findRankByName('genus') + familyid =options.taxonomy.findRankByName('family') + + minrankseq = set([speciesid,genusid,familyid]) + + usedrankid = {} + + ingroup = [] + outgroup= [] + + for s in entries: + if 'taxid' in s : + taxid = s['taxid'] + if taxid in taxonomy: + allrank = set() + for p in options.taxonomy.parentalTreeIterator(taxid): + if p[1]!=norankid: + allrank.add(p[1]) + if len(minrankseq & allrank) == 3: + for r in allrank: + usedrankid[r]=usedrankid.get(r,0) + 1 + + if taxonomy.isAncestor(options.ingroup,taxid): + ingroup.append(s) + else: + outgroup.append(s) + + keptrank = set(r for r in usedrankid + if float(usedrankid[r])/float(len(ingroup)) > options.rankthresold) + + return { 'ingroup' : ingroup, + 'outgroup': outgroup, + 'ranks' : keptrank + } + +def buildSimilarityGraph(dbseq,ranks,taxonomy,dcmax=5): + + ldbseq = len(dbseq) + pos = 1 + digit = int(math.ceil(math.log10(ldbseq))) + header = "Alignment : %%0%dd x %%0%dd -> %%0%dd " % (digit,digit,digit) + aligncount = ldbseq*(ldbseq+1)/2 + edgecount = 0 + print >>sys.stderr + + progressBar(1,aligncount,True,"Alignment : %s x %s -> %s " % ('-'*digit,'-'*digit, '0'*digit)) + + + sim = UndirectedGraph() + + i=0 + for s in dbseq: + taxid = s['taxid'] + + rtaxon = dict((rid,taxonomy.getTaxonAtRank(taxid,rid)) + for rid in ranks) + + sim.addNode(i, seq=s,taxid=taxid,rtaxon=rtaxon) + + i+=1 + +# aligner = LCS() + + for is1 in xrange(ldbseq): + s1 = dbseq[is1] + ls1= len(s1) +# aligner.seqA=s1 + + for is2 in xrange(is1+1,ldbseq): + + s2=dbseq[is2] + ls2=len(s2) + + lm = max(ls1,ls2) + lcsmin = lm - dcmax + + if isLCSReachable(s1,s2,lcsmin): + llcs,lali=lenlcs(s1,s2) + ds1s2 = lali - llcs + + if ds1s2 <= dcmax: + sim.addEdge(node1=is1, node2=is2,ds1s2=ds1s2,label=ds1s2) + edgecount+=1 + + progressBar(pos,aligncount,head=header % (is1,is2,edgecount)) + pos+=(ldbseq-is1-1) + + return sim + +def buildTsr(component): + ''' + Build for each consider taxonomic rank the list of taxa + present in the connected component + + :param component: the analyzed connected component + :type component: :py:class:`UndirectedGraph` + + :return: a dictionary indexed by rankid containing a `dict` indexed by taxid and containing count of sequences for this taxid + :rtype: `dict` indexed by `int` containing `dict` indexed by `int` and containing of `int` + + ''' + taxalist = {} + for n in component: + for r in n['rtaxon']: + rtaxid = n['rtaxon'][r] + if rtaxid is not None: + ts = taxalist.get(r,{}) + ts[rtaxid]=ts.get(rtaxid,0)+1 + taxalist[r]=ts + + return taxalist + +def edgeDistSelector(dcmax): + def predicate(e): + return e['ds1s2'] <= dcmax + return predicate + +def distanceOfConfusion(simgraph,dcmax=5,aggregate=average): + + alltaxa = set() + + for n in simgraph: + alltaxa|=set(n['rtaxon'].values()) + + taxacount = len(alltaxa) + + result = {} + + pos = [1] + header = "Component : %-5d Identified : %-8d " + progressBar(1,taxacount,True,header % (0,0)) + + def _idc(cc,dcmax): + composante=[] + for x in cc: + composante.extend(simgraph.subgraph(c) + for c in componentIterator(x, + edgePredicat=edgeDistSelector(dcmax))) + + good = set() + bad = {} + + complexe = [] + + for c in composante: + tsr = buildTsr(c) + newbad=False + for r in tsr: + if len(tsr[r]) == 1: + taxid = tsr[r].keys()[0] + good.add((taxid,tsr[r][taxid])) + else: + newbad=True + for taxid in tsr[r]: + bad[taxid]=bad.get(taxid,0)+tsr[r][taxid] + if newbad: + complexe.append(c) + +# good = good - bad + + for taxid,weight in good: + if taxid not in result: + result[taxid]=[] + result[taxid].append((dcmax+1,weight)) + + + progressBar(pos[0],taxacount,False,header % (len(composante),pos[0])) + pos[0]=len(result) + + if dcmax > 0: + dcmax-=1 + _idc(complexe,dcmax) + + else: + for taxid in bad: + if taxid not in result: + result[taxid]=[] + result[taxid].append((0,bad[taxid])) + + progressBar(pos[0],taxacount,False,header % (len(composante),pos[0])) + pos[0]=len(result) + + _idc([simgraph],dcmax) + + for taxid in result: + result[taxid]=aggregate(result[taxid]) + return result + +def propagateDc(tree,node=None,aggregate=min): + if node is None: + node = tree.getRoots()[0] + dca=aggregate(n['dc'] for n in node.leavesIterator()) + node['dc']=dca + for n in node: + propagateDc(tree, n, aggregate) + +def confusionTree(distances,ranks,taxonomy,aggregate=min,bsrank='species',dcmax=1): + + def Bs(node,rank,dcmax): + n = len(node) + if n: + g = [int(x['dc']>=dcmax) for x in node.subgraphIterator() if x['rank']==bsrank] + n = len(g) + g = sum(g) + bs= float(g)/float(n) + node['bs']=bs + node['bs_label']="%3.2f (%d)" % (bs,n) + + for n in node: + Bs(n,rank,dcmax) + + tree = RootedTree() + ranks = set(ranks) + tset = set(distances) + + for taxon in distances: + tree.addNode(taxon, rank=taxonomy.getRank(taxon), + name=taxonomy.getScientificName(taxon), + dc=float(distances[taxon][0]), + n=distances[taxon][1], + dc_label="%4.2f (%d)" % (float(distances[taxon][0]),distances[taxon][1]) + ) + + for taxon in distances: + piter = taxonomy.parentalTreeIterator(taxon) + taxon = piter.next() + for parent in piter: + if taxon[0] in tset and parent[0] in distances: + tset.remove(taxon[0]) + tree.addEdge(parent[0], taxon[0]) + taxon=parent + + root = tree.getRoots()[0] + Bs(root,bsrank,dcmax) + + return tree diff --git a/obitools/metabarcoding/options.py b/obitools/metabarcoding/options.py new file mode 100644 index 0000000..08ff423 --- /dev/null +++ b/obitools/metabarcoding/options.py @@ -0,0 +1,34 @@ +''' +Created on 30 oct. 2011 + +@author: coissac +''' + +from obitools.ecopcr.options import addTaxonomyDBOptions + + +def addMetabarcodingOption(optionManager): + + addTaxonomyDBOptions(optionManager) + + optionManager.add_option('--dcmax', + action="store", dest="dc", + metavar="###", + type="int", + default=0, + help="Maximum confusion distance considered") + + optionManager.add_option('--ingroup', + action="store", dest="ingroup", + metavar="###", + type="int", + default=1, + help="ncbi taxid delimitation the in group") + + optionManager.add_option('--rank-thresold', + action="store", dest="rankthresold", + metavar="#.##", + type="float", + default=0.5, + help="minimum fraction of the ingroup sequences " + "for concidering the rank") diff --git a/obitools/obischemas/__init__.py b/obitools/obischemas/__init__.py new file mode 100644 index 0000000..6bcafde --- /dev/null +++ b/obitools/obischemas/__init__.py @@ -0,0 +1,28 @@ +from obitools.obischemas import kb +__connection__ = None + +def initConnection(options): + global __connection__ + param = {} + if hasattr(options, "dbname") and options.dbname is not None: + param["database"]=options.dbname + if hasattr(options, "dbhost") and options.dbhost is not None: + param["host"]=options.dbhost + if hasattr(options, "dbuser") and options.dbuser is not None: + param["username"]=options.dbuser + if hasattr(options, "dbpassword") and options.dbpassword is not None: + param["password"]=options.dbpassword + + __connection__=kb.getConnection(**param) + __connection__.autocommit=options.autocommit + +def getConnection(options=None): + global __connection__ + + if options is not None: + initConnection(options) + + assert __connection__ is not None,"database connection is not initialized" + + return __connection__ + \ No newline at end of file diff --git a/obitools/obischemas/kb/__init__.py b/obitools/obischemas/kb/__init__.py new file mode 100644 index 0000000..7d35dcb --- /dev/null +++ b/obitools/obischemas/kb/__init__.py @@ -0,0 +1,55 @@ +""" + kb package is devoted to manage access to postgresql database from python + script +""" + + +class Connection(object): + + def __init__(self): + raise RuntimeError('pyROM.KB.Connection is an abstract class') + + def cursor(self): + raise RuntimeError('pyROM.KB.Connection.cursor is an abstract function') + + def commit(self): + raise RuntimeError('pyROM.KB.Connection.commit is an abstract function') + + def rollback(self): + raise RuntimeError('pyROM.KB.Connection.rollback is an abstract function') + + def __call__(self,query): + return self.cursor().execute(query) + + +class Cursor(object): + + def __init__(self,db): + raise RuntimeError('pyROM.KB.Cursor is an abstract class') + + def execute(self,query): + raise RuntimeError('pyROM.KB.Cursor.execute is an abstract function') + + __call__=execute + + +_current_connection = None # Static variable used to store connection to KB + +def getConnection(*args,**kargs): + """ + return a connection to the database. + When call from database backend no argument are needed. + All connection returned by this function + """ + global _current_connection + + if _current_connection==None or args or kargs : + try: + from obischemas.kb import backend + _current_connection = backend.Connection() + except ImportError: + from obischemas.kb import extern + _current_connection = extern.Connection(*args,**kargs) + return _current_connection + + diff --git a/obitools/obischemas/kb/extern.py b/obitools/obischemas/kb/extern.py new file mode 100644 index 0000000..ce2ff84 --- /dev/null +++ b/obitools/obischemas/kb/extern.py @@ -0,0 +1,78 @@ +""" +Module : KB.extern +Author : Eric Coissac +Date : 03/05/2004 + +Module wrapping psycopg interface module to allow connection +to a postgresql databases with the same interface from +backend and external script. + +This module define a class usable from external script +""" + + +import psycopg2 +import sys +from obischemas import kb + +class Connection(kb.Connection): + + def __init__(self,*connectParam,**kconnectParam): + if connectParam: + self.connectParam=={'dsn':connectParam} + else: + self.connectParam=kconnectParam + print self.connectParam + self.db = psycopg2.connect(**(self.connectParam)) + + def restart(self): + ok=1 + while (ok and ok < 1000): + try: + self.db = psycopg2.connect(**self.connectParam) + except: + ok+=1 + else: + ok=0 + + + def cursor(self): + curs = Cursor(self.db) + if hasattr(self,'autocommit') and self.autocommit: + curs.autocommit = self.autocommit + return curs + + def commit(self): + self.db.commit() + + def rollback(self): + if hasattr(self,'db'): + self.db.rollback() + + def __del__(self): + if hasattr(self,'db'): + self.rollback() + +class Cursor(kb.Cursor): + + def __init__(self,db): + self.db = db + self.curs = db.cursor() + + def execute(self,query): + try: + self.curs.execute(query) + if hasattr(self,'autocommit') and self.autocommit: + self.db.commit() + except psycopg2.ProgrammingError,e: + print >>sys.stderr,"===> %s" % query + raise e + except psycopg2.IntegrityError,e: + print >>sys.stderr,"---> %s" % query + raise e + try: + label = [x[0] for x in self.curs.description] + return [dict(map(None,label,y)) + for y in self.curs.fetchall()] + except TypeError: + return [] diff --git a/obitools/obischemas/options.py b/obitools/obischemas/options.py new file mode 100644 index 0000000..66f5138 --- /dev/null +++ b/obitools/obischemas/options.py @@ -0,0 +1,31 @@ +def addConnectionOptions(optionManager): + + optionManager.add_option('-d','--dbname', + action="store", dest="dbname", + metavar="", + type="string", + help="OBISchema database name containing" + "taxonomical data") + + optionManager.add_option('-H','--host', + action="store", dest="dbhost", + metavar="", + type="string", + help="host hosting OBISchema database") + + optionManager.add_option('-U','--user', + action="store", dest="dbuser", + metavar="", + type="string", + help="user for OBISchema database connection") + + optionManager.add_option('-W','--password', + action="store", dest="dbpassword", + metavar="", + type="string", + help="password for OBISchema database connection") + + optionManager.add_option('-A','--autocommit', + action="store_true",dest="autocommit", + default=False, + help="add commit action after each query") \ No newline at end of file diff --git a/obitools/obo/__init__.py b/obitools/obo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/obo/go/__init__.py b/obitools/obo/go/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/obo/go/parser.py b/obitools/obo/go/parser.py new file mode 100644 index 0000000..6902974 --- /dev/null +++ b/obitools/obo/go/parser.py @@ -0,0 +1,53 @@ +from obitools.obo.parser import OBOTerm +from obitools.obo.parser import OBOEntry +from obitools.obo.parser import stanzaIterator +from logging import debug + +class GOEntry(OBOEntry): + ''' + An entry of a GeneOntology .obo file. It can be a header (without a stanza name) or + a stanza (with a stanza name between brackets). It inherits from the class dict. + ''' + + +class GOTerm(OBOTerm): + + ''' + A stanza named 'Term'. It inherits from the class OBOTerm. + ''' + + def __init__(self,stanza): + + ## use of the OBOEntry constructor. + OBOTerm.__init__(self, stanza) + + assert 'namespace' in self and len(self['namespace'])==1, "An OBOTerm must belong to one of the cell_component, molecular_function or biological_process namespace" + + +def GOEntryFactory(stanza): + ''' + Dispatcher of stanza. + + @param stanza: a stanza composed of several lines. + @type stanza: text + + @return: an C{OBOTerm} | C{OBOEntry} instance + + @note: The dispatcher treats differently the stanza which are OBO "Term" + and the others. + ''' + + stanzaType = OBOEntry.parseStanzaName(stanza) + + if stanzaType=="Term": + return GOTerm(stanza) + else: + return OBOEntry(stanza) + + +def GOEntryIterator(file): + entries = stanzaIterator(file) + for e in entries: + debug(e) + yield GOEntryFactory(e) + diff --git a/obitools/obo/parser.py b/obitools/obo/parser.py new file mode 100644 index 0000000..f6f05f3 --- /dev/null +++ b/obitools/obo/parser.py @@ -0,0 +1,707 @@ +from obitools.utils import skipWhiteLineIterator,multiLineWrapper +from obitools.utils import universalOpen +from obitools.format.genericparser import genericEntryIteratorGenerator +from logging import debug,warning + +import re + + +################################################################################# +## Stanza preparation area ## +################################################################################# + + +class FileFormatError(Exception): + ''' + An error derived from the class Exception. + ''' + pass + +_oboEntryIterator = genericEntryIteratorGenerator(endEntry='^ *$', + strip=True) + +def stanzaIterator(inputfile): + ''' + Iterator of stanza. The stanza are the basic units of OBO files. + + @param inputfile: a stream of strings from an opened OBO file. + @type inputfile: a stream of strings + + @return: a stream of stanza + @rtype: a stream of aggregated strings + + @note: The iterator constructs stanza by aggregate strings from the + OBO file. + ''' + inputfile = universalOpen(inputfile) + inputfile = multiLineWrapper(inputfile) + return _oboEntryIterator(inputfile) + + + +################################################################################# +## Trailing Modifiers treatment area ## +################################################################################# + + +class TrailingModifier(dict): + ''' + A class object which inherits from the class dict. Trailing modifiers can be found + at the end of TaggedValue objects when they exist. + ''' + + _match_brace = re.compile('(?<=\ {)[^\]]*(\}) *( !|$)') + + def __init__(self,string): + + ## search for trailing modifiers signals + trailing_modifiers = TrailingModifier._match_brace.search(string) + + ## the trailing modifiers exist + if trailing_modifiers: + trailing_modifiers=trailing_modifiers.group(0).strip() + print trailing_modifiers + ## creates and feeds the dictionary of trailing modifiers + dict.__init__(self,(x.strip().split('=',1) for x in trailing_modifiers.split(','))) + + +def trailingModifierFactory(string): + ''' + Dispatcher of trailing modifiers. + + @param string: a string from a TaggedValue object with a trailing modifiers signal. + @type string: string + + @return: a class object + + @note: The dispatcher is currently very simple. Only one case is treated by the function. + `the function returns a class object inherited from the class dict if the trailing modifiers + exist, None if they don't. + ''' + + trailing_modifiers = TrailingModifier(string) + if not trailing_modifiers: + trailing_modifiers=None + return trailing_modifiers + + +################################################################################# +## TaggedValue treatment area ## +################################################################################# + + +class TaggedValue(object): + ''' + A couple 'tag:value' of an OBOEntry. + ''' + + _match_value = re.compile('(("(\\\\"|[^\"])*")|(\\\\"|[^\"]))*?( !| {|$)') + _split_comment = re.compile('^!| !') + _match_quotedString = re.compile('(?<=")(\\\\"|[^\"])*(?=")') + _match_bracket = re.compile('\[[^\]]*\]') + + def __init__(self,line): + ''' + Constructor of the class TaggedValue. + + @param line: a line of an OBOEntry composed of a tag and a value. + @type line: string + + @note: The constructor separates tags from right terms. 'value' is extracted + from right terms using a regular expression (value is at the beginning of the + string, between quotes or not). Then, 'comment' is extracted from the rest of the + string using another regular expression ('comment' is at the end of the string + after a '!'. By default, 'comment' is set to None). Finally, 'trailing_modifiers' + are extracted from the last string using another regular expression. + The tag, the value, the comment and the trailing_modifiers are saved. + ''' + + debug("tagValueParser : %s" % line) + + ## by default : + trailing_modifiers = None + comment = None + + ## the tag is saved. 'right' is composed of the value, the comment and the trailing modifiers + tag,rigth = line.split(':',1) + + ## the value is saved + value = TaggedValue._match_value.search(rigth).group(0) + debug("Extracted value : %s" % value) + + ## if there is a value AND a sign of a comment or trailing modifiers + if value and value[-1] in '!{': + lvalue = len(value) + ## whatever it is a comment or trailing modifiers, it is saved into 'extra' + extra = rigth[lvalue-1:].strip() + ## a comment is extracted + extra =TaggedValue._split_comment.split(extra,1) + ## and saved if it exists + if len(extra)==2: + comment=extra[1].strip() + ## trailing modifiers are extracted + extra=extra[0] + trailing_modifiers = trailingModifierFactory(extra) + ## the value is cleaned of any comment or trailing modifiers signals + value = value[0:-1] + + if tag=='use_term': + tag='consider' + raise DeprecationWarning,"user_term is a deprecated tag, you should instead use consider" + + ## recording zone + self.value =value.strip() + self.tag = tag + self.__doc__=comment + self.trailing_modifiers=trailing_modifiers + + def __str__(self): + return str(self.value) + + def __repr__(self): + return '''"""%s"""''' % str(self) + + +class NameValue(TaggedValue): + ''' + A couple 'name:value' inherited from the class TaggedValue. Used to manage name tags. + ''' + + def __init__(self,line): + + ## no use of the TaggedValue constructor. The NameValue is very simple. + tag,rigth = line.split(':',1) + + ## recording zone + self.value = rigth.strip() + self.tag = 'name' + self.__doc__=None + self.trailing_modifiers=None + + + +class DefValue(TaggedValue): + ''' + A couple 'def:value' inherited from the class TaggedValue. Used to manage def tags. + ''' + + def __init__(self,line): + ''' + Constructor of the class DefValue. + + @param line: a line of an OBOEntry composed of a tag named 'def' and a value. + @type line: string + + @note: The constructor calls the TaggedValue constructor. A regular expression + is used to extract the 'definition' from TaggedValue.value (definition is a not + quoted TaggedValue.value). A regular expression is used to extract 'dbxrefs' + from the aggedValue.value without the definition (dbxrefs are between brackets + and definition can be so). Definition is saved as the new value of the DefValue. + dbxrefs are saved. + ''' + + ## use of the TaggedValue constructor + TaggedValue.__init__(self, line) + + ## definition, which is quoted, is extracted from the standard value of a TaggedValue. + definition = TaggedValue._match_quotedString.search(self.value).group(0) + + ## the standard value is cleaned of the definition. + cleanvalue = self.value.replace(definition,'') + cleanvalue = cleanvalue.replace(' ',' ') + + ## dbxrefs are searched into the rest of the standard value. + dbxrefs = TaggedValue._match_bracket.search(cleanvalue).group(0) + + ## recording zone + self.tag = 'def' + ## the value of a DefValue is not the standard value but the definition. + self.value=definition + self.dbxrefs=xrefFactory(dbxrefs) + + +class SynonymValue(TaggedValue): + ''' + A couple 'synonym:value' inherited from the class TaggedValue. Used to manage + synonym tags, exact_synonym tags, broad_synonym tags and narrow_synonym tags. + ''' + + _match_scope = re.compile('(?<="")[^\[]*(?=\[|$)') + + def __init__(self,line): + ''' + Constructor of the class SynonymValue. + + @param line: a line of an OBOEntry composed of a tag named 'synonym' or + 'exact_synonym' or 'broad_synonym' or 'narrow_synonym' and a value. + @type line: string + + @note: SynonymValue is composed of a tag, a value, a scope, a list of types and + dbxrefs. + The constructor calls the TaggedValue constructor. A regular expression + is used to extract 'definition' from TaggedValue.value (definition is a not + quoted TaggedValue.value). Definition is saved as the new value of the class + SynonymValue. + A regular expression is used to extract 'attributes' from the rest of the + string. Attributes may contain an optional synonym scope and an optional list + of synonym types. The scope is extracted from attributes or set by default to + 'RELATED'. It is saved as the scope of the class. The types are the rest of the + attributes and are saved as the list of types of the class. + For deprecated tags 'exact_synonym', 'broad_synonym' and 'narrow_synonym', tag + is set to 'synonym' and scope is set respectively to 'EXACT', 'BROAD' and 'NARROW'. + A regular expression is used to extract 'dbxrefs' from the TaggedValue.value + without the definition (dbxrefs are between brackets and definition can be so). + dbxrefs are saved. + ''' + + ## use of the TaggedValue constructor + TaggedValue.__init__(self, line) + + ## definition, which is quoted, is extracted from the standard value of a TaggedValue. + definition = TaggedValue._match_quotedString.search(self.value).group(0) + + ## the standard value is cleaned of the definition. + cleanvalue = self.value.replace(definition,'') + cleanvalue = cleanvalue.replace(' ',' ') + + ## 1) attributes are searched into the rest of the standard value. + ## 2) then they are stripped. + ## 3) then they are split on every ' '. + ## 4) finally they are ordered into a set. + attributes = set(SynonymValue._match_scope.search(cleanvalue).group(0).strip().split()) + + ## the scopes are the junction between the attributes and a set of specific terms. + scopes = attributes & set(['RELATED','EXACT','BROAD','NARROW']) + + ## the types are the rest of the attributes. + types = attributes - scopes + + ## this is a constraint of the OBO format + assert len(scopes)< 2,"Only one synonym scope allowed" + + ## the scope of the SynonymValue is into scopes or set by default to RELATED + if scopes: + scope = scopes.pop() + else: + scope = 'RELATED' + + ## Specific rules are defined for the following tags : + if self.tag == 'exact_synonym': + raise DeprecationWarning,'exact_synonym is a deprecated tag use instead synonym tag' + self.tag = 'synonym' + scope = 'EXACT' + + if self.tag == 'broad_synonym': + raise DeprecationWarning,'broad_synonym is a deprecated tag use instead synonym tag' + self.tag = 'synonym' + scope = 'BROAD' + + if self.tag == 'narrow_synonym': + raise DeprecationWarning,'narrow_synonym is a deprecated tag use instead synonym tag' + self.tag = 'synonym' + scope = 'NARROW' + + if self.tag == 'systematic_synonym': + #raise DeprecationWarning,'narrow_synonym is a deprecated tag use instead sysnonym tag' + self.tag = 'synonym' + scope = 'SYSTEMATIC' + + ## this is our own constraint. deprecated tags are not saved by this parser. + assert self.tag =='synonym',"%s synonym type is not managed" % self.tag + + ## dbxrefs are searched into the rest of the standard value. + dbxrefs = TaggedValue._match_bracket.search(cleanvalue).group(0) + + ## recording zone + ## the value of a SynonymValue is not the standard value but the definition. + self.value = definition + self.dbxrefs = xrefFactory(dbxrefs) + self.scope = scope + self.types = list(types) + + def __eq__(self,b): + return ((self.value==b.value) and (self.dbxrefs==b.dbxrefs) + and (self.scope==b.scope) and (self.types==b.types) + and (self.__doc__==b.__doc__) and (self.tag==b.tag) + and (self.trailing_modifiers==b.trailing_modifiers)) + + def __hash__(self): + return (reduce(lambda x,y:x+y,(hash(z) for z in [self.__doc__, + self.value, + frozenset(self.dbxrefs), + self.scope, + frozenset(self.types), + self.tag, + self.trailing_modifiers]),0)) % (2**31) + + +class XrefValue(TaggedValue): + ''' + A couple 'xref:value' inherited from the class TaggedValue. Used to manage + xref tags. + ''' + + def __init__(self,line): + + ## use of the TaggedValue constructor + TaggedValue.__init__(self, line) + + ## use the same function as the dbxrefs + self.value=xrefFactory(self.value) + + if self.tag in ('xref_analog','xref_unk'): + raise DeprecationWarning,'%s is a deprecated tag use instead sysnonym tag' % self.tag + self.tag='xref' + + ## this is our own constraint. deprecated tags are not saved by this parser. + assert self.tag=='xref' + + +class RelationshipValue(TaggedValue): + ''' + A couple 'xref:value' inherited from the class TaggedValue. Used to manage + xref tags. + ''' + + def __init__(self,line): + + ## use of the TaggedValue constructor + TaggedValue.__init__(self, line) + + ## the value is split on the first ' '. + value = self.value.split(None,1) + + ## succesful split ! + if len(value)==2: + relationship=value[0] + term=value[1] + ## unsuccesful split. The relationship is set by default to IS_A + else: + relationship='is_a' + term=value[0] + + ## recording zone + self.value=term + self.relationship=relationship + + +class NamespaceValue(TaggedValue): + def __init__(self,line): + TaggedValue.__init__(self, line) + +class RemarkValue(TaggedValue): + def __init__(self,line): + TaggedValue.__init__(self, line) + label,value = self.value.split(':',1) + label = label.strip() + value = value.strip() + self.value=value + self.label=label + + +def taggedValueFactory(line): + ''' + A function used to dispatch lines of an OBOEntry between the class TaggedValue + and its inherited classes. + + @param line: a line of an OBOEntry composed of a tag and a value. + @type line: string + + @return: a class object + ''' + + if (line[0:9]=='namespace' or + line[0:17]=='default-namespace'): + return NamespaceValue(line) + ## DefValue is an inherited class of TaggedValue + elif line[0:3]=='def': + return DefValue(line) + ## SynonymValue is an inherited class of TaggedValue + elif ((line[0:7]=="synonym" and line[0:14]!="synonymtypedef") or + line[0:13]=="exact_synonym" or + line[0:13]=="broad_synonym" or + line[0:14]=="narrow_synonym"): + return SynonymValue(line) + ## XrefValue is an inherited class of TaggedValue + elif line[0:4]=='xref': + return XrefValue(line) + ## NameValue is an inherited class of TaggedValue + elif line[0:4]=='name': + return NameValue(line) + ## RelationshipValue is an inherited class of TaggedValue + elif (line[0:15]=='intersection_of' or + line[0:8] =='union_of' or + line[0:12]=='relationship'): + return RelationshipValue(line) + elif (line[0:6]=='remark'): + return RemarkValue(line) + ## each line is a couple : tag / value (and some more features) + else: + return TaggedValue(line) + + +################################################################################# +## Xref treatment area ## +################################################################################# + + + +class Xref(object): + ''' + A xref object of an OBOentry. It may be the 'dbxrefs' of SynonymValue and + DefValue objects or the 'value' of XrefValue objects. + ''' + + __splitdata__ = re.compile(' +(?=["{])') + + def __init__(self,ref): + if ref == '' : # + ref = None # + data = '' # + else : # Modifs JJ sinon erreur : list index out of range + data = Xref.__splitdata__.split(ref,1) # + ref = data[0] # + description=None + trailing_modifiers = None + if len(data)> 1: + extra = data[1] + description = TaggedValue._match_quotedString.search(extra) + if description is not None: + description = description.group(0) + extra.replace(description,'') + trailing_modifiers=trailingModifierFactory(extra) + self.reference=ref + self.description=description + self.trailing_modifiers=trailing_modifiers + + def __eq__(self,b): + return ((self.reference==b.reference) and (self.description==b.description) + and (self.trailing_modifiers==b.trailing_modifiers)) + + def __hash__(self): + return (reduce(lambda x,y:x+y,(hash(z) for z in [self.reference, + self.description, + self.trailing_modifiers]),0)) % (2**31) + + +def xrefFactory(string): + ''' + Dispatcher of xrefs. + + @param string: a string (between brackets) from an inherited TaggedValue object with a dbxrefs + signal (actually, the signal can only be found into SynonymValue and DefValue + objects) or a string (without brackets) from a XrefValue object. + @type string: string + + @return: a class object + + @note: The dispatcher treats differently the strings between brackets (from SynonymValue and + DefValue objects) and without brackets (from XrefValue objects). + ''' + + string = string.strip() + if string[0]=='[': + return [Xref(x.strip()) for x in string[1:-1].split(',')] + else: + return Xref(string) + + +################################################################################# +## Stanza treatment area ## +################################################################################# + + +class OBOEntry(dict): + ''' + An entry of an OBOFile. It can be a header (without a stanza name) or + a stanza (with a stanza name between brackets). It inherits from the class dict. + ''' + _match_stanza_name = re.compile('(?<=^\[)[^\]]*(?=\])') + + def __init__(self,stanza): + ## tests if it is the header of the OBO file (returns TRUE) or not (returns FALSE) + self.isHeader = stanza[0]!='[' + lines = stanza.split('\n') + ## not the header : there is a [stanzaName] + if not self.isHeader: + self.stanzaName = lines[0].strip()[1:-1] + lines=lines[1:] + self["stanza"] = [stanza.strip()] + + ## whatever the stanza is. + for line in lines: + ## each line is a couple : tag / value + taggedvalue = taggedValueFactory(line) + if taggedvalue.tag in self: + self[taggedvalue.tag].append(taggedvalue) + else: + self[taggedvalue.tag]=[taggedvalue] + + + def parseStanzaName(stanza): + sm = OBOEntry._match_stanza_name.search(stanza) + if sm: + return sm.group(0) + else: + return None + + parseStanzaName=staticmethod(parseStanzaName) + + + +class OBOTerm(OBOEntry): + ''' + A stanza named 'Term'. It inherits from the class OBOEntry. + ''' + def __init__(self,stanza): + + ## use of the OBOEntry constructor. + OBOEntry.__init__(self, stanza) + + assert self.stanzaName=='Term' + assert 'stanza' in self + assert 'id' in self and len(self['id'])==1,"An OBOTerm must have an id" + assert 'name' in self and len(self['name'])==1,"An OBOTerm must have a name" + assert 'namespace' not in self or len(self['namespace'])==1, "Only one namespace is allowed for an OBO term" + + assert 'def' not in self or len(self['def'])==1,"Only one definition is allowed for an OBO term" + assert 'comment' not in self or len(self['comment'])==1,"Only one comment is allowed for an OBO term" + + assert 'union_of' not in self or len(self['union_of'])>=2,"Only one union relationship is allowed for an OBO term" + assert 'intersection_of' not in self or len(self['intersection_of'])>=2,"Only one intersection relationship is allowed for an OBO term" + + if self._isObsolete(): + #assert 'is_a' not in self + assert 'relationship' not in self + assert 'inverse_of' not in self + assert 'disjoint_from' not in self + assert 'union_of' not in self + assert 'intersection_of' not in self + + assert 'replaced_by' not in self or self._isObsolete() + assert 'consider' not in self or self._isObsolete() + + def _getStanza(self): + return self['stanza'][0] + + ## make-up functions. + def _getDefinition(self): + if 'def' in self: + return self['def'][0] + return None + + def _getId(self): + return self['id'][0] + + def _getNamespace(self): + return self['namespace'][0] + + def _getName(self): + return self['name'][0] + + def _getComment(self): + if 'comment' in self: + return self['comment'][0] + return None + + def _getAltIds(self): + if 'alt_id' in self: + return list(set(self.get('alt_id',None))) + return None + + def _getIsA(self): + if 'is_a' in self: + return list(set(self.get('is_a',None))) + return None + + def _getSynonym(self): + if 'synonym' in self : + return list(set(self.get('synonym',None))) + return None + + def _getSubset(self): + if self.get('subset',None) != None: + return list(set(self.get('subset',None))) + else: + return None + + def _getXref(self): + if 'xref' in self: + return list(set(self.get('xref',None))) + return None + + def _getRelationShip(self): + if 'relationship' in self: + return list(set(self.get('relationship',None))) + return None + + def _getUnion(self): + return list(set(self.get('union_of',None))) + + def _getIntersection(self): + return list(set(self.get('intersection_of',None))) + + def _getDisjonction(self): + return list(set(self.get('disjoint_from',None))) + + def _isObsolete(self): + return 'is_obsolete' in self and str(self['is_obsolete'][0])=='true' + + def _getReplacedBy(self): + if 'replaced_by' in self: + return list(set(self.get('replaced_by',None))) + return None + + def _getConsider(self): + if 'consider' in self: + return list(set(self.get('consider',None))) + return None + + ## automatically make-up ! + stanza = property(_getStanza,None,None) + definition = property(_getDefinition,None,None) + id = property(_getId,None,None) + namespace = property(_getNamespace,None,None) + name = property(_getName,None,None) + comment = property(_getComment,None,None) + alt_ids = property(_getAltIds,None,None) + is_a = property(_getIsA,None,None) + synonyms = property(_getSynonym,None,None) + subsets = property(_getSubset,None,None) + xrefs = property(_getXref,None,None) + relationship = property(_getRelationShip,None,None) + union_of = property(_getUnion,None,None) + intersection_of = property(_getIntersection,None,None) + disjoint_from = property(_getDisjonction,None,None) + is_obsolete = property(_isObsolete,None,None) + replaced_by = property(_getReplacedBy,None,None) + consider = property(_getConsider,None,None) + + +def OBOEntryFactory(stanza): + ''' + Dispatcher of stanza. + + @param stanza: a stanza composed of several lines. + @type stanza: text + + @return: an C{OBOTerm} | C{OBOEntry} instance + + @note: The dispatcher treats differently the stanza which are OBO "Term" + and the others. + ''' + + stanzaType = OBOEntry.parseStanzaName(stanza) + + if stanzaType=="Term": + return OBOTerm(stanza) + else: + return OBOEntry(stanza) + +def OBOEntryIterator(file): + entries = stanzaIterator(file) + for e in entries: + debug(e) + yield OBOEntryFactory(e) + + \ No newline at end of file diff --git a/obitools/options/__init__.py b/obitools/options/__init__.py new file mode 100644 index 0000000..d6793d6 --- /dev/null +++ b/obitools/options/__init__.py @@ -0,0 +1,137 @@ +""" + Module providing high level functions to manage command line options. +""" +import logging +import sys + +from logging import debug + +from optparse import OptionParser + +from obitools.utils import universalOpen +from obitools.utils import fileSize +from obitools.utils import universalTell +from obitools.utils import progressBar +from obitools.format.options import addInputFormatOption, addInOutputOption,\ + autoEntriesIterator +import time + + + +def getOptionManager(optionDefinitions,entryIterator=None,progdoc=None): + ''' + Build an option manager fonction. that is able to parse + command line options of the script. + + @param optionDefinitions: list of function describing a set of + options. Each function must allows as + unique parametter an instance of OptionParser. + @type optionDefinitions: list of functions. + + @param entryIterator: an iterator generator function returning + entries from the data files. + + @type entryIterator: an iterator generator function with only one + parametter of type file + ''' + parser = OptionParser(progdoc) + parser.add_option('--DEBUG', + action="store_true", dest="debug", + default=False, + help="Set logging in debug mode") + + parser.add_option('--no-psyco', + action="store_true", dest="noPsyco", + default=False, + help="Don't use psyco even if it installed") + + parser.add_option('--without-progress-bar', + action="store_false", dest="progressbar", + default=True, + help="desactivate progress bar") + + checkFormat=False + for f in optionDefinitions: + if f == addInputFormatOption or f == addInOutputOption: + checkFormat=True + f(parser) + + def commandLineAnalyzer(): + options,files = parser.parse_args() + if options.debug: + logging.root.setLevel(logging.DEBUG) + + if checkFormat: + ei=autoEntriesIterator(options) + else: + ei=entryIterator + + i = allEntryIterator(files,ei,with_progress=options.progressbar) + return options,i + + return commandLineAnalyzer + +_currentInputFileName=None +_currentFile = None +_currentFileSize = None + +def currentInputFileName(): + return _currentInputFileName + +def currentInputFile(): + return _currentFile + +def currentFileSize(): + return _currentFileSize + +def currentFileTell(): + return universalTell(_currentFile) + +def fileWithProgressBar(file,step=100): + try: + size = currentFileSize() + except: + size = None + + def fileBar(): + pos=1 + progressBar(pos, size, True,currentInputFileName()) + for l in file: + progressBar(currentFileTell,size,head=currentInputFileName()) + yield l + print >>sys.stderr,'' + if size is None: + return file + else: + f = fileBar() + return f + + +def allEntryIterator(files,entryIterator,with_progress=False,histo_step=102): + global _currentFile + global _currentInputFileName + global _currentFileSize + if files : + for f in files: + _currentInputFileName=f + f = universalOpen(f) + _currentFile=f + _currentFileSize=fileSize(_currentFile) + debug(f) + if with_progress: + f=fileWithProgressBar(f,step=histo_step) + if entryIterator is None: + for line in f: + yield line + else: + for entry in entryIterator(f): + yield entry + else: + if entryIterator is None: + for line in sys.stdin: + yield line + else: + for entry in entryIterator(sys.stdin): + yield entry + + \ No newline at end of file diff --git a/obitools/options/bioseqcutter.py b/obitools/options/bioseqcutter.py new file mode 100644 index 0000000..77189af --- /dev/null +++ b/obitools/options/bioseqcutter.py @@ -0,0 +1,85 @@ +from logging import debug + +def _beginOptionCallback(options,opt,value,parser): + def beginCutPosition(seq): + debug("begin = %s" % value ) + if hasattr(options, 'taxonomy') and options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq} + else: + environ = {'sequence':seq} + + return eval(value,environ,seq) - 1 + + parser.values.beginCutPosition=beginCutPosition + +def _endOptionCallback(options,opt,value,parser): + def endCutPosition(seq): + if hasattr(options, 'taxonomy') and options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq} + else: + environ = {'sequence':seq} + + return eval(value,environ,seq) + + parser.values.endCutPosition=endCutPosition + + + + +def addSequenceCuttingOptions(optionManager): + + optionManager.add_option('-b','--begin', + action="callback", callback=_beginOptionCallback, + metavar="", + type="string", + help="python expression to be evaluated in the " + "sequence context. The attribute name can be " + "used in the expression as variable name. " + "An extra variable named 'sequence' refers " + "to the sequence object itself. ") + + optionManager.add_option('-e','--end', + action="callback", callback=_endOptionCallback, + metavar="", + type="string", + help="python expression to be evaluated in the " + "sequence context. The attribute name can be " + "used in the expression as variable name ." + "An extra variable named 'sequence' refers" + "to the sequence object itself. ") + + +def cutterGenerator(options): + + def sequenceCutter(seq): + + lseq = len(seq) + + if hasattr(options, 'beginCutPosition'): + begin = int(options.beginCutPosition(seq)) + else: + begin = 0 + + if hasattr(options, 'endCutPosition'): + end = int(options.endCutPosition(seq)) + else: + end = lseq + + if begin > 0 or end < lseq: + seq = seq[begin:end] + seq['subsequence']="%d..%d" % (begin+1,end) + + return seq + + return sequenceCutter + +def cutterIteratorGenerator(options): + _cutter = cutterGenerator(options) + + def sequenceCutterIterator(seqIterator): + for seq in seqIterator: + yield _cutter(seq) + + return sequenceCutterIterator + + diff --git a/obitools/options/bioseqedittag.py b/obitools/options/bioseqedittag.py new file mode 100644 index 0000000..6eb1c36 --- /dev/null +++ b/obitools/options/bioseqedittag.py @@ -0,0 +1,237 @@ +import sys +from obitools.options.taxonomyfilter import loadTaxonomyDatabase +def addSequenceEditTagOptions(optionManager): + + optionManager.add_option('--rank', + action="store_true", dest='addrank', + default=False, + help="add a rank attribute to the sequence " + "indicating the sequence position in the input data") + + optionManager.add_option('-R','--rename-tag', + action="append", + dest='renameTags', + metavar="", + type="string", + default=[], + help="change tag name from OLD_NAME to NEW_NAME") + + optionManager.add_option('--delete-tag', + action="append", + dest='deleteTags', + metavar="", + type="string", + default=[], + help="delete tag TAG_NAME") + + optionManager.add_option('-S','--set-tag', + action="append", + dest='setTags', + metavar="", + type="string", + default=[], + help="Add a new tag named TAG_NAME with " + "a value computed from PYTHON_EXPRESSION") + + optionManager.add_option('--set-identifier', + action="store", + dest='setIdentifier', + metavar="", + type="string", + default=None, + help="Set sequence identifier with " + "a value computed from PYTHON_EXPRESSION") + + optionManager.add_option('--set-sequence', + action="store", + dest='setSequence', + metavar="", + type="string", + default=None, + help="Change the sequence itself with " + "a value computed from PYTHON_EXPRESSION") + + optionManager.add_option('-T','--set-definition', + action="store", + dest='setDefinition', + metavar="", + type="string", + default=None, + help="Set sequence definition with " + "a value computed from PYTHON_EXPRESSION") + + optionManager.add_option('-O','--only-valid-python', + action="store_true", + dest='onlyValid', + default=False, + help="only valid python expressions are allowed") + + optionManager.add_option('-C','--clear', + action="store_true", + dest='clear', + default=False, + help="clear all tags associated to the sequences") + + optionManager.add_option('-k','--keep', + action='append', + dest='keep', + default=[], + type="string", + help="only keep this tag") + + optionManager.add_option('--length', + action="store_true", + dest='length', + default=False, + help="add seqLength tag with sequence length") + + optionManager.add_option('--with-taxon-at-rank', + action='append', + dest='taxonrank', + default=[], + type="string", + help="add taxonomy annotation at a speciefied rank level") + + optionManager.add_option('-m','--mcl', + action="store", dest="mcl", + metavar="", + type="string", + default=None, + help="split following mcl graph clustering partition") + + +def readMCLFile(file): + partition=1 + parts = {} + for l in file: + for seq in l.strip().split(): + parts[seq]=partition + partition+=1 + return parts + + + + +def sequenceTaggerGenerator(options): + toDelete = options.deleteTags[:] + toRename = [x.split(':',1) for x in options.renameTags if len(x.split(':',1))==2] + toSet = [x.split(':',1) for x in options.setTags if len(x.split(':',1))==2] + newId = options.setIdentifier + newDef = options.setDefinition + newSeq = options.setSequence + clear = options.clear + keep = set(options.keep) + length = options.length + counter = [0] + loadTaxonomyDatabase(options) + if options.taxonomy is not None: + annoteRank=options.taxonrank + else: + annoteRank=[] + + if options.mcl is not None: + parts = readMCLFile(open(options.mcl)) + else: + parts = False + + def sequenceTagger(seq): + + if counter[0]>=0: + counter[0]+=1 + + if clear or keep: + ks = seq.keys() + for k in ks: + if k not in keep: + del seq[k] + else: + for i in toDelete: + if i in seq: + del seq[i] + for o,n in toRename: + if o in seq: + seq[n]=seq[o] + del seq[o] + + for rank in annoteRank: + if 'taxid' in seq: + taxid = seq['taxid'] + if taxid is not None: + rtaxid = options.taxonomy.getTaxonAtRank(taxid,rank) + if rtaxid is not None: + scn = options.taxonomy.getScientificName(rtaxid) + else: + scn=None + seq[rank]=rtaxid + seq["%s_name"%rank]=scn + + if parts and seq.id in parts: + seq['cluster']=parts[seq.id] + + if options.addrank: + seq['rank']=counter[0] + + for i,v in toSet: + try: + if options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq, 'counter':counter[0]} + else: + environ = {'sequence':seq, 'counter':counter[0]} + + val = eval(v,environ,seq) + except Exception,e: + if options.onlyValid: + raise e + val = v + seq[i]=val + + if length: + seq['seqLength']=len(seq) + + if newId is not None: + try: + if options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq, 'counter':counter[0]} + else: + environ = {'sequence':seq, 'counter':counter[0]} + + val = eval(newId,environ,seq) + except Exception,e: + if options.onlyValid: + raise e + val = newId + seq.id=val + if newDef is not None: + try: + if options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq, 'counter':counter[0]} + else: + environ = {'sequence':seq, 'counter':counter[0]} + + val = eval(newDef,environ,seq) + except Exception,e: + if options.onlyValid: + raise e + val = newDef + seq.definition=val + + if newSeq is not None: + try: + if options.taxonomy is not None: + environ = {'taxonomy' : options.taxonomy,'sequence':seq, 'counter':counter[0]} + else: + environ = {'sequence':seq, 'counter':counter[0]} + + val = eval(newSeq,environ,seq) + except Exception,e: + if options.onlyValid: + raise e + val = newSeq + if hasattr(seq, '_seq'): + seq._seq=str(val).lower() + if 'seqLength' in seq: + seq['seqLength']=len(seq) + + return seq + + return sequenceTagger \ No newline at end of file diff --git a/obitools/options/bioseqfilter.py b/obitools/options/bioseqfilter.py new file mode 100644 index 0000000..d52c9b5 --- /dev/null +++ b/obitools/options/bioseqfilter.py @@ -0,0 +1,179 @@ +import re + +from obitools.options.taxonomyfilter import addTaxonomyFilterOptions +from obitools.options.taxonomyfilter import taxonomyFilterGenerator + +def _sequenceOptionCallback(options,opt,value,parser): + parser.values.sequencePattern = re.compile(value,re.I) + +def _defintionOptionCallback(options,opt,value,parser): + parser.values.definitionPattern = re.compile(value) + +def _identifierOptionCallback(options,opt,value,parser): + parser.values.identifierPattern = re.compile(value) + +def _attributeOptionCallback(options,opt,value,parser): + if not hasattr(options, 'attributePatterns'): + parser.values.attributePatterns={} + attribute,pattern=value.split(':',1) + parser.values.attributePatterns[attribute]=re.compile(pattern) + +def _predicatOptionCallback(options,opt,value,parser): + if not hasattr(options, 'predicats'): + options.predicats=[] + parser.values.predicats.append(value) + + +def addSequenceFilteringOptions(optionManager): + + optionManager.add_option('-s','--sequence', + action="callback", callback=_sequenceOptionCallback, + metavar="", + type="string", + help="regular expression pattern used to select " + "the sequence. The pattern is case insensitive") + + optionManager.add_option('-D','--definition', + action="callback", callback=_defintionOptionCallback, + type="string", + metavar="", + help="regular expression pattern matched against " + "the definition of the sequence. " + "The pattern is case sensitive") + + optionManager.add_option('-I','--identifier', + action="callback", callback=_identifierOptionCallback, + type="string", + metavar="", + help="regular expression pattern matched against " + "the identifier of the sequence. " + "The pattern is case sensitive") + + optionManager.add_option('-a','--attribute', + action="callback", callback=_attributeOptionCallback, + type="string", + metavar=":", + help="regular expression pattern matched against " + "the attributes of the sequence. " + "the value of this atribute is of the form : " + "attribute_name:regular_pattern. " + "The pattern is case sensitive." + "Several -a option can be used on the same " + "commande line.") + + optionManager.add_option('-A','--has-attribute', + action="append", + type="string", + dest="has_attribute", + default=[], + metavar="", + help="select sequence with attribute " + "defined") + + optionManager.add_option('-p','--predicat', + action="append", dest="predicats", + metavar="", + help="python boolean expression to be evaluated in the " + "sequence context. The attribute name can be " + "used in the expression as variable name ." + "An extra variable named 'sequence' refers" + "to the sequence object itself. " + "Several -p option can be used on the same " + "commande line.") + + optionManager.add_option('-L','--lmax', + action='store', + metavar="<##>", + type="int",dest="lmax", + help="keep sequences shorter than lmax") + + optionManager.add_option('-l','--lmin', + action='store', + metavar="<##>", + type="int",dest="lmin", + help="keep sequences longer than lmin") + + optionManager.add_option('-v','--inverse-match', + action='store_true', + default=False, + dest="invertedFilter", + help="revert the sequence selection " + "[default : %default]") + + addTaxonomyFilterOptions(optionManager) + + + + + +def filterGenerator(options): + taxfilter = taxonomyFilterGenerator(options) + + def sequenceFilter(seq): + good = True + + if hasattr(options, 'sequencePattern'): + good = bool(options.sequencePattern.search(str(seq))) + + if good and hasattr(options, 'identifierPattern'): + good = bool(options.identifierPattern.search(seq.id)) + + if good and hasattr(options, 'definitionPattern'): + good = bool(options.definitionPattern.search(seq.definition)) + + if good : + good = reduce(lambda x,y:x and y, + (k in seq for k in options.has_attribute), + True) + + if good and hasattr(options, 'attributePatterns'): + good = (reduce(lambda x,y : x and y, + (bool(options.attributePatterns[p].search(str(seq[p]))) + for p in options.attributePatterns + if p in seq),True) + and + reduce(lambda x,y : x and y, + (bool(p in seq) + for p in options.attributePatterns),True) + ) + + if good and hasattr(options, 'predicats') and options.predicats is not None: + if options.taxonomy is not None: + e = {'taxonomy' : options.taxonomy,'sequence':seq} + else: + e = {'sequence':seq} + + good = (reduce(lambda x,y: x and y, + (bool(eval(p,e,seq)) + for p in options.predicats),True) + ) + + if good and hasattr(options, 'lmin') and options.lmin is not None: + good = len(seq) >= options.lmin + + if good and hasattr(options, 'lmax') and options.lmax is not None: + good = len(seq) <= options.lmax + + if good: + good = taxfilter(seq) + + if hasattr(options, 'invertedFilter') and options.invertedFilter: + good=not good + + + return good + + return sequenceFilter + +def sequenceFilterIteratorGenerator(options): + filter = filterGenerator(options) + + def sequenceFilterIterator(seqIterator): + for seq in seqIterator: + if filter(seq): + yield seq + + return sequenceFilterIterator + + + \ No newline at end of file diff --git a/obitools/options/taxonomyfilter.py b/obitools/options/taxonomyfilter.py new file mode 100644 index 0000000..5526c79 --- /dev/null +++ b/obitools/options/taxonomyfilter.py @@ -0,0 +1,6 @@ +from obitools.ecopcr.options import addTaxonomyDBOptions, \ + addTaxonomyFilterOptions, \ + loadTaxonomyDatabase, \ + taxonomyFilterGenerator, \ + taxonomyFilterIteratorGenerator + diff --git a/obitools/parallel/__init__.py b/obitools/parallel/__init__.py new file mode 100644 index 0000000..2aa1b07 --- /dev/null +++ b/obitools/parallel/__init__.py @@ -0,0 +1,99 @@ +import threading + +class TaskPool(object): + + def __init__(self,iterable,function,count=2): + self.pool = [] + self.queue= [] + self.plock= threading.Lock() + self.qlock= threading.Lock() + self.function=function + self.event=threading.Event() + self.iterable=iterable + for i in xrange(count): + Task(self) + + def register(self,task): + self.plock.acquire() + self.pool.append(task) + self.plock.release() + self.ready(task) + + def unregister(self,task): + task.thread.join() + self.plock.acquire() + self.pool.remove(task) + self.plock.release() + + + def ready(self,task): + self.qlock.acquire() + self.queue.append(task) + self.qlock.release() + self.event.set() + + def __iter__(self): + for data in self.iterable: + while not self.queue: + self.event.wait() + self.event.clear() + self.qlock.acquire() + task=self.queue.pop(0) + self.qlock.release() + if hasattr(task, 'rep'): + yield task.rep + #print "send ",data + if isinstance(data,dict): + task.submit(**data) + else: + task.submit(*data) + + while self.pool: + self.pool[0].finish() + while self.queue: + self.event.clear() + self.qlock.acquire() + task=self.queue.pop(0) + self.qlock.release() + if hasattr(task, 'rep'): + yield task.rep + + + + + +class Task(object): + def __init__(self,pool): + self.pool = pool + self.lock = threading.Lock() + self.dataOk = threading.Event() + self.repOk = threading.Event() + self.args = None + self.kwargs=None + self.stop=False + self.thread = threading.Thread(target=self) + self.thread.start() + self.pool.register(self) + + def __call__(self): + self.dataOk.wait() + while(not self.stop): + self.lock.acquire() + self.dataOk.clear() + self.rep=self.pool.function(*self.args,**self.kwargs) + self.pool.ready(self) + self.lock.release() + self.dataOk.wait() + + def submit(self,*args,**kwargs): + self.args=args + self.kwargs=kwargs + self.dataOk.set() + + def finish(self): + self.lock.acquire() + self.stop=True + self.dataOk.set() + self.pool.unregister(self) + + diff --git a/obitools/parallel/jobqueue.py b/obitools/parallel/jobqueue.py new file mode 100644 index 0000000..9df4804 --- /dev/null +++ b/obitools/parallel/jobqueue.py @@ -0,0 +1,183 @@ +import threading +from logging import warning,info +from time import sleep,time + +from obitools.parallel import TaskPool + + +class JobPool(dict): + ''' + JobPool is dedicated to manage a job queue. These jobs + will run in a limited number of thread. + ''' + + def __init__(self,count,precision=0.01): + ''' + + @param count: number of thread dedicated to this JobPool + @type count: int + @param precision: delay between two check for new job (in second) + @type precision: float + ''' + self._iterator = JobIterator(self) + self._taskPool = TaskPool(self._iterator, + self._runJob, + count) + self._precision=precision + self._toRun=set() + self._runnerThread = threading.Thread(target=self._runner) + self._runnerThread.start() + self._finalyzed=False + + def _runner(self): + for rep in self._taskPool: + info('Job %d finnished' % id(rep)) + info('All jobs in %d JobPool finished' % id(self)) + + def _jobIterator(self): + return self._iterator + + def _runJob(self,job): + job.started= time() + info('Job %d started' % id(job)) + job.result = job() + job.ended = time() + job.finished=True + return job + + def submit(self,job,priority=1.0,userid=None): + ''' + Submit a new job to the JobPool. + + @param job: the new submited job + @type job: Job instance + @param priority: priority level of this job (higher is better) + @type priority: float + @param userid: a user identifier (Default is None) + + @return: job identifier + @rtype: int + ''' + + assert not self._finalyzed,\ + "This jobPool does not accept new job" + if job.submitted is not None: + warning('Job %d was already submitted' % id(job)) + return id(job) + + job.submitted = time() + job.priority = priority + job.userid = userid + i=id(job) + job.id=id + self[i]=job + self._toRun.add(job) + + info('Job %d submitted' % i) + + return i + + def finalyze(self): + ''' + Indicate to the JobPool, that no new jobs will + be submitted. + ''' + self._iterator.finalyze() + self._finalyzed=True + + def __del__(self): + self.finalyze() + + +class JobIterator(object): + def __init__(self,pool): + self._pool = pool + self._finalyze=False + self._nextLock=threading.Lock() + + + def __iter__(self): + return self + + def finalyze(self): + ''' + Indicate to the JobIterator, that no new jobs will + be submitted. + ''' + self._finalyze=True + + + def next(self): + ''' + + @return: the next job to run + @rtype: Job instance + ''' + self._nextLock.acquire() + while self._pool._toRun or not self._finalyze: + rep = None + maxScore=0 + for k in self._pool._toRun: + s = k.runScore() + if s > maxScore: + maxScore=s + rep=k + if rep is not None: + self._pool._toRun.remove(rep) + self._nextLock.release() + return (rep,) + sleep(self._pool._precision) + self._nextLock.release() + info('No more jobs in %d JobPool' % id(self._pool)) + raise StopIteration + + + +class Job(object): + + def __init__(self,pool=None,function=None,*args,**kwargs): + ''' + Create a new job + + @param pool: the jobpool used to run job. Can be None to not + execute the job immediately. + @type pool: JobPool instance + + @param function: the function to run for the job + @type function: callable object + + @param args: parametters for function call + @param kwargs: named parametters for function call + + @precondition: function cannot be None + ''' + assert function is not None + self._args=args + self._kwargs = kwargs + self._function = function + self.running = False + self.finished= False + self.submitted = None + self.priority = None + self.userid = None + + if pool is not None: + pool.submit(self) + + def runScore(self): + ''' + @return: the score used to ordonnance job in the queue + @rtype: C{float} + ''' + + return (time() - self.submitted) * self.priority + + def __call__(self): + return self._function(*self._args,**self._kwargs) + + + + + + + \ No newline at end of file diff --git a/obitools/phylogeny/__init__.py b/obitools/phylogeny/__init__.py new file mode 100644 index 0000000..8eb1587 --- /dev/null +++ b/obitools/phylogeny/__init__.py @@ -0,0 +1,119 @@ + +from obitools.graph.tree import Forest,TreeNode +from obitools.graph import Edge + + + +class PhylogenicTree(Forest): + + def __init__(self,label='G',indexer=None,nodes=None,edges=None): + Forest.__init__(self, label, indexer, nodes, edges) + self.root=None + self.comment=None + + def addNode(self,node=None,index=None,**data): + if node is None and index is None: + node = '__%d' % (len(self._node)+1) + + return Forest.addNode(self, node, index, **data) + + def getNode(self,node=None,index=None): + if index is None: + index = self._index.getIndex(node, True) + return PhylogenicNode(index,self) + + def getEdge(self,node1=None,node2=None,index1=None,index2=None): + ''' + + @param node1: + @type node1: + @param node2: + @type node2: + @param index1: + @type index1: + @param index2: + @type index2: + ''' + node1=self.getNode(node1, index1) + node2=self.getNode(node2, index2) + return PhylogenicEdge(node1,node2) + + + +class PhylogenicNode(TreeNode): + + def getLabel(self): + label = TreeNode.getLabel(self) + if label[0:2]=='__': + return None + else: + return label + + def __str__(self): + + if self.index in self.graph._node_attrs: + keys = " ".join(['%s="%s"' % (x[0],str(x[1]).replace('"','\\"')) + for x in self.graph._node_attrs[self.index].iteritems()] + ) + else: + keys='' + + if self.label is None: + label='' + shape='point' + else: + label=self.label + shape='box' + + return '%d [label="%s" shape="%s" %s]' % (self.index,str(label).replace('"','\\"'),shape,keys) + + def distanceTo(self,node=None,index=None): + ''' + compute branch length between the two nodes. + If distances are not secified for this tree, None is returned. + + @param node: a node label or None + @param index: a node index or None. the parameter index + has a priority on the parameter node. + @type index: int + + @return: the evolutive distance between the two nodes + @rtype: int, float or None + ''' + path = self.shortestPathTo(node, index) + + start = path.pop(0) + dist=0 + for dest in path: + edge = self.graph.getEdge(index1=start,index2=dest) + if 'distance' in edge: + dist+=edge['distance'] + else: + return None + start=dest + + return dist + + label = property(getLabel, None, None, "Label of the node") + +class PhylogenicEdge(Edge): + + def __str__(self): + e = (self.node1.index,self.node2.index) + if e in self.graph._edge_attrs: + keys = "[%s]" % " ".join(['%s="%s"' % (x[0],str(x[1]).replace('"','\\"')) + for x in self.graph._edge_attrs[e].iteritems() + if x[0] not in ('distance','bootstrap')] + ) + else: + keys = "" + + + + if self.directed: + link='->' + else: + link='--' + + return "%d %s %d %s" % (self.node1.index,link,self.node2.index,keys) + diff --git a/obitools/phylogeny/newick.py b/obitools/phylogeny/newick.py new file mode 100644 index 0000000..cf0330c --- /dev/null +++ b/obitools/phylogeny/newick.py @@ -0,0 +1,123 @@ +import re +import sys + +from obitools.utils import universalOpen +from obitools.phylogeny import PhylogenicTree + +def subNodeIterator(data): + level=0 + start = 1 + if data[0]=='(': + for i in xrange(1,len(data)): + c=data[i] + if c=='(': + level+=1 + elif c==')': + level-=1 + if c==',' and not level: + yield data[start:i] + start = i+1 + yield data[start:i] + else: + yield data + + +_nodeParser=re.compile('\s*(?P\(.*\))?(?P[^ :]+)? *(?P[0-9.]+)?(:(?P-?[0-9.]+))?') + +def nodeParser(data): + parsedNode = _nodeParser.match(data).groupdict(0) + if not parsedNode['name']: + parsedNode['name']=None + + if not parsedNode['bootstrap']: + parsedNode['bootstrap']=None + else: + parsedNode['bootstrap']=float(parsedNode['bootstrap']) + + if not parsedNode['distance']: + parsedNode['distance']=None + else: + parsedNode['distance']=float(parsedNode['distance']) + + if not parsedNode['subnodes']: + parsedNode['subnodes']=None + + return parsedNode + +_cleanTreeData=re.compile('\s+') + +def treeParser(data,tree=None,parent=None): + if tree is None: + tree = PhylogenicTree() + data = _cleanTreeData.sub(' ',data).strip() + + parsedNode = nodeParser(data) + + if parent is not None: + son,parent = tree.addEdge(node1=parsedNode['name'], + index2=parent, + distance=parsedNode['distance'], + bootstrap=parsedNode['bootstrap']) + else: + son = tree.addNode(node1=parsedNode['name']) + tree.root=son + + + + if parsedNode['subnodes']: + for subnode in subNodeIterator(parsedNode['subnodes']): + treeParser(subnode,tree,son) + + return tree + +_treecomment=re.compile('\[.*\]') + +def treeIterator(file): + file = universalOpen(file) + data = file.read() + + comment = _treecomment.findall(data) + data=_treecomment.sub('',data).strip() + + if comment: + comment=comment[0] + else: + comment=None + for tree in data.split(';'): + t = treeParser(tree) + if comment: + t.comment=comment + yield t + +def nodeWriter(tree,node,deep=0): + name = node._name + if name is None: + name='' + + distance=node._dist + if distance is None: + distance='' + else: + distance = ':%6.5f' % distance + + bootstrap=node._bootstrap + if bootstrap is None: + bootstrap='' + else: + bootstrap=' %d' % int(bootstrap) + + nodeseparator = ',\n' + ' ' * (deep+1) + + subnodes = nodeseparator.join([nodeWriter(tree, x, deep+1) + for x in tree.childNodeIterator(node)]) + if subnodes: + subnodes='(\n' + ' ' * (deep+1) + subnodes + '\n' + ' ' * deep + ')' + + return '%s%s%s%s' % (subnodes,name,bootstrap,distance) + +def treeWriter(tree,startnode=None): + if startnode is not None: + root=startnode + else: + root = tree.getRoot() + return nodeWriter(tree,root)+';' diff --git a/obitools/profile/__init__.py b/obitools/profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/profile/_profile.so b/obitools/profile/_profile.so new file mode 100755 index 0000000000000000000000000000000000000000..7f52483878a6086ca676e0cb3b1bd3fd873c6374 GIT binary patch literal 129896 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 0): + + i=(lmax+lmin)/2 + #print i,lmin,lmax + if (xcumsum[i-1])): + #print "return 1 :",i,cumsum[i-1],"<",x,"<",cumsum[i] + return i + elif cumsum[i]==x: + while cumsum[i]==x: + i+=1 + #print "return 2 :",i,cumsum[i],"<",x,"<",cumsum[i+1] + return i + elif x0] + shuffle(entries) + cumul=[] + s=0 + for e in entries: + s+=events[e] + cumul.append(s) + + #print cumul + result={} + + for t in xrange(size): + e=lookfor(randrange(s), cumul) + k=entries[e] + result[k]=result.get(k,0)+1 + + return result + +def weigthedSampleWithoutReplacement(events,size): + entries = [k for k in events.iterkeys() if events[k]>0] + shuffle(entries) + cumul=[] + s=0 + for e in entries: + s+=events[e] + cumul.append(s) + + #print cumul + result={} + + for t in xrange(size): + # print s,cumul, + e=lookfor(randrange(s), cumul) + # print e + k=entries[e] + for x in xrange(e,len(cumul)): + cumul[x]-=1 + s-=1 + result[k]=result.get(k,0)+1 + + return result \ No newline at end of file diff --git a/obitools/seqdb/__init__.py b/obitools/seqdb/__init__.py new file mode 100644 index 0000000..274cbad --- /dev/null +++ b/obitools/seqdb/__init__.py @@ -0,0 +1,88 @@ +from obitools import NucSequence,AASequence +from obitools.format.genericparser import genericEntryIteratorGenerator +from obitools.location.feature import featureIterator + +from itertools import chain + +class AnnotatedSequence(object): + + def __init__(self,header,featureTable,secondaryAcs): + self._header = header + self._featureTableText = featureTable + self._featureTable=None + self._secondaryAcs=secondaryAcs + self._hasTaxid=None + + def getHeader(self): + return self._header + + + def getFeatureTable(self,skipError=False): + if self._featureTable is None: + self._featureTable = [x for x in featureIterator(self._featureTableText,skipError)] + return self._featureTable + + + def getSecondaryAcs(self): + return self._secondaryAcs + + def extractTaxon(self): + if self._hasTaxid is None: + + if self._featureTable is not None: + s = [f for f in self._featureTable if f.ftType=='source'] + else: + s = featureIterator(self._featureTableText).next() + if s.ftType=='source': + s = [s] + else: + s = [f for f in self.featureTable if f.ftType=='source'] + + t =set(int(v[6:]) for v in chain(*tuple(f['db_xref'] for f in s if 'db_xref' in f)) + if v[0:6]=='taxon:') + + self._hasTaxid=False + + if len(t)==1 : + taxid=t.pop() + if taxid >=0: + self['taxid']=taxid + self._hasTaxid=True + + + t =set(chain(*tuple(f['organism'] for f in s if 'organism' in f))) + + if len(t)==1: + self['organism']=t.pop() + + + header = property(getHeader, None, None, "Header's Docstring") + + featureTable = property(getFeatureTable, None, None, "FeatureTable's Docstring") + + secondaryAcs = property(getSecondaryAcs, None, None, "SecondaryAcs's Docstring") + +class AnnotatedNucSequence(AnnotatedSequence,NucSequence): + ''' + + ''' + def __init__(self,id,seq,de,header,featureTable,secondaryAcs,**info): + NucSequence.__init__(self, id, seq, de,**info) + AnnotatedSequence.__init__(self, header, featureTable, secondaryAcs) + + +class AnnotatedAASequence(AnnotatedSequence,AASequence): + ''' + + ''' + def __init__(self,id,seq,de,header,featureTable,secondaryAcs,**info): + AASequence.__init__(self, id, seq, de,**info) + AnnotatedSequence.__init__(self, header, featureTable, secondaryAcs) + + + +nucEntryIterator=genericEntryIteratorGenerator(endEntry='^//') +aaEntryIterator=genericEntryIteratorGenerator(endEntry='^//') + + + diff --git a/obitools/seqdb/blastdb/__init__.py b/obitools/seqdb/blastdb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/seqdb/dnaparser.py b/obitools/seqdb/dnaparser.py new file mode 100644 index 0000000..85b82a2 --- /dev/null +++ b/obitools/seqdb/dnaparser.py @@ -0,0 +1,16 @@ +from obitools.format.sequence import embl,fasta,genbank + +class UnknownFormatError(Exception): + pass + +def whichParser(seq): + if seq[0]=='>': + return fasta.fastaNucParser + if seq[0:2]=='ID': + return embl.emblParser + if seq[0:5]=='LOCUS': + return genbank.genbankParser + raise UnknownFormatError,"Unknown nucleic format" + +def nucleicParser(seq): + return whichParser(seq)(seq) diff --git a/obitools/seqdb/embl/__init__.py b/obitools/seqdb/embl/__init__.py new file mode 100644 index 0000000..94f9efc --- /dev/null +++ b/obitools/seqdb/embl/__init__.py @@ -0,0 +1,13 @@ +from obitools.seqdb import AnnotatedNucSequence, AnnotatedAASequence +from obitools.location import locationGenerator,extractExternalRefs + + + +class EmblSequence(AnnotatedNucSequence): + ''' + Class used to represent a nucleic sequence issued from EMBL. + ''' + + + + diff --git a/obitools/seqdb/embl/parser.py b/obitools/seqdb/embl/parser.py new file mode 100644 index 0000000..2e3624f --- /dev/null +++ b/obitools/seqdb/embl/parser.py @@ -0,0 +1,50 @@ +import re +import sys + +from obitools.seqdb import embl +from obitools.seqdb import nucEntryIterator + +_featureMatcher = re.compile('(^FT .*\n)+', re.M) +_cleanFT = re.compile('^FT',re.M) + +_headerMatcher = re.compile('^ID.+(?=\nFH )', re.DOTALL) +_seqMatcher = re.compile('(^ ).+(?=//\n)', re.DOTALL + re.M) +_cleanSeq = re.compile('[ \n0-9]+') +_acMatcher = re.compile('(?<=^AC ).+',re.M) +_deMatcher = re.compile('(^DE .+\n)+',re.M) +_cleanDe = re.compile('(^|\n)DE +') + +def __emblparser(text): + try: + header = _headerMatcher.search(text).group() + + ft = _featureMatcher.search(text).group() + ft = _cleanFT.sub(' ',ft) + + seq = _seqMatcher.search(text).group() + seq = _cleanSeq.sub('',seq).upper() + + acs = _acMatcher.search(text).group() + acs = acs.split() + ac = acs[0] + acs = acs[1:] + + de = _deMatcher.search(header).group() + de = _cleanDe.sub(' ',de).strip().strip('.') + except AttributeError,e: + print >>sys.stderr,'=======================================================' + print >>sys.stderr,text + print >>sys.stderr,'=======================================================' + raise e + + return (ac,seq,de,header,ft,acs) + +def emblParser(text): + return embl.EmblSequence(*__emblparser(text)) + + +def emblIterator(file): + for e in nucEntryIterator(file): + yield emblParser(e) + + \ No newline at end of file diff --git a/obitools/seqdb/genbank/__init__.py b/obitools/seqdb/genbank/__init__.py new file mode 100644 index 0000000..fb5b622 --- /dev/null +++ b/obitools/seqdb/genbank/__init__.py @@ -0,0 +1,84 @@ +from obitools.seqdb import AnnotatedNucSequence, AnnotatedAASequence +from obitools.location import locationGenerator,extractExternalRefs + + + +class GbSequence(AnnotatedNucSequence): + ''' + Class used to represent a nucleic sequence issued from Genbank. + ''' + + +class GpepSequence(AnnotatedAASequence): + ''' + Class used to represent a peptidic sequence issued from Genpep. + ''' + + def __init__(self,id,seq,de,header,featureTable,secondaryAcs,**info): + AnnotatedAASequence.__init__(self,id, seq, de, header, featureTable, secondaryAcs,**info) + self.__hasNucRef=None + + def __getGeneRef(self): + if self.__hasNucRef is None: + self.__hasNucRef=False + cds = [x for x in self.featureTable + if x.ftType=='CDS' + and 'coded_by' in x] + + if cds: + source = cds[0]['coded_by'][0] + if 'transl_table' in cds[0]: + tt = cds[0]['transl_table'][0] + else: + tt=None + ac,loc = extractExternalRefs(source) + + if len(ac)==1: + ac = ac.pop() + self.__hasNucRef=True + self.__nucRef = (ac,loc,tt) + + + + def geneAvailable(self): + ''' + Predicat indicating if reference to the nucleic sequence encoding + this protein is available in feature table. + + @return: True if gene description is available + @rtype: bool + ''' + self.__getGeneRef() + return self.__hasNucRef is not None and self.__hasNucRef + + + def getCDS(self,database): + ''' + Return the nucleic sequence coding for this protein if + data are available. + + @param database: a database object where looking for the sequence + @type database: a C{dict} like object + + @return: a NucBioseq instance carreponding to the CDS + @rtype: NucBioSeq + + @raise AssertionError: if no gene references are available + @see: L{geneAvailable} + + ''' + + assert self.geneAvailable(), \ + "No information available to retreive gene sequence" + + ac,loc,tt = self.__nucRef + seq = database[ac] + seq.extractTaxon() + gene = seq[loc] + if tt is not None: + gene['transl_table']=tt + return gene + + + + diff --git a/obitools/seqdb/genbank/ncbi.py b/obitools/seqdb/genbank/ncbi.py new file mode 100644 index 0000000..40ddf91 --- /dev/null +++ b/obitools/seqdb/genbank/ncbi.py @@ -0,0 +1,79 @@ +from urllib2 import urlopen +import sys +import re + +import cStringIO + +from obitools.eutils import EFetch +from parser import genbankParser,genpepParser +from parser import genbankIterator,genpepIterator + +from obitools.utils import CachedDB + + +class NCBIGenbank(EFetch): + def __init__(self): + EFetch.__init__(self,db='nucleotide', + rettype='gbwithparts') + + def __getitem__(self,ac): + if isinstance(ac,str): + text = self.get(id=ac) + seq = genbankParser(text) + return seq + else: + query = ','.join([x for x in ac]) + data = cStringIO.StringIO(self.get(id=query)) + return genbankIterator(data) + + + + +class NCBIGenpep(EFetch): + def __init__(self): + EFetch.__init__(self,db='protein', + rettype='gbwithparts') + + def __getitem__(self,ac): + if isinstance(ac,str): + text = self.get(id=ac) + seq = genpepParser(text) + return seq + else: + query = ','.join([x for x in ac]) + data = cStringIO.StringIO(self.get(id=query)) + return genpepIterator(data) + +class NCBIAccession(EFetch): + + _matchACS = re.compile(' +accession +"([^"]+)"') + + def __init__(self): + EFetch.__init__(self,db='nucleotide', + rettype='seqid') + + def __getitem__(self,ac): + if isinstance(ac,str): + text = self.get(id=ac) + rep = NCBIAccession._matchACS.search(text).group(1) + return rep + else: + query = ','.join([x for x in ac]) + text = self.get(id=query) + rep = (ac.group(1) for ac in NCBIAccession._matchACS.finditer(text)) + return rep + +def Genbank(cache=None): + gb = NCBIGenbank() + if cache is not None: + gb = CachedDB(cache, gb) + return gb + + +def Genpep(cache=None): + gp = NCBIGenpep() + if cache is not None: + gp = CachedDB(cache, gp) + return gp + + diff --git a/obitools/seqdb/genbank/parser.py b/obitools/seqdb/genbank/parser.py new file mode 100644 index 0000000..b52fe59 --- /dev/null +++ b/obitools/seqdb/genbank/parser.py @@ -0,0 +1,53 @@ +import re +import sys + +import obitools.seqdb.genbank as gb +from obitools.seqdb import nucEntryIterator,aaEntryIterator + +_featureMatcher = re.compile('^FEATURES.+\n(?=ORIGIN)',re.DOTALL + re.M) + +_headerMatcher = re.compile('^LOCUS.+(?=\nFEATURES)', re.DOTALL + re.M) +_seqMatcher = re.compile('(?<=ORIGIN).+(?=//\n)', re.DOTALL + re.M) +_cleanSeq = re.compile('[ \n0-9]+') +_acMatcher = re.compile('(?<=^ACCESSION ).+',re.M) +_deMatcher = re.compile('(?<=^DEFINITION ).+\n( .+\n)*',re.M) +_cleanDe = re.compile('\n *') + +def __gbparser(text): + try: + header = _headerMatcher.search(text).group() + ft = _featureMatcher.search(text).group() + seq = _seqMatcher.search(text).group() + seq = _cleanSeq.sub('',seq).upper() + acs = _acMatcher.search(text).group() + acs = acs.split() + ac = acs[0] + acs = acs[1:] + de = _deMatcher.search(header).group() + de = _cleanDe.sub(' ',de).strip().strip('.') + except AttributeError,e: + print >>sys.stderr,'=======================================================' + print >>sys.stderr,text + print >>sys.stderr,'=======================================================' + raise e + + return (ac,seq,de,header,ft,acs) + +def genbankParser(text): + return gb.GbSequence(*__gbparser(text)) + + +def genbankIterator(file): + for e in nucEntryIterator(file): + yield genbankParser(e) + + +def genpepParser(text): + return gb.GpepSequence(*__gbparser(text)) + + +def genpepIterator(file): + for e in aaEntryIterator(file): + yield genpepParser(e) + + \ No newline at end of file diff --git a/obitools/sequenceencoder/__init__.py b/obitools/sequenceencoder/__init__.py new file mode 100644 index 0000000..89a8a59 --- /dev/null +++ b/obitools/sequenceencoder/__init__.py @@ -0,0 +1,73 @@ +from obitools import location + +class SequenceEncoder(object): + pass + +class DNAComplementEncoder(SequenceEncoder): + _comp={'a': 't', 'c': 'g', 'g': 'c', 't': 'a', + 'r': 'y', 'y': 'r', 'k': 'm', 'm': 'k', + 's': 's', 'w': 'w', 'b': 'v', 'd': 'h', + 'h': 'd', 'v': 'b', 'n': 'n', 'u': 'a', + '-': '-'} + + _info={'complemented':True} + + @staticmethod + def _encode(seq,position=slice(None, None, -1)): + cseq = [DNAComplementEncoder._comp.get(x.lower(),'n') for x in seq[position]] + return ''.join(cseq) + + @staticmethod + def _check(seq): + assert seq.isNucleotide() + + @staticmethod + def _convertpos(position): + if isinstance(position, int): + return -(position+1) + elif isinstance(position, slice): + return slice(-(position.stop+1), + -(position.start+1), + -position.step) + elif isinstance(position, location.Location): + return location.ComplementLocation(position).simplify() + + raise TypeError,"position must be an int, slice or Location instance" + + @staticmethod + def complement(seq): + return seq + +class SeqFragmentEncoder(SequenceEncoder): + def __init__(self,begin,end): + assert begin < end and begin >=0 + self._limits = slice(begin,end) + self._info = {'cut' : [begin,end,1]} + self._len = end - begin + 1 + + def _check(self,seq): + lseq = len(seq) + assert self._limits.stop <= lseq + + def _encode(self,seq,position=None): + return str(seq)[self._limits] + + def _convertpos(self,position): + if isinstance(position, int): + if position < -self._len or position >= self._len: + raise IndexError,position + if position >=0: + return self._limits.start + position + else: + return self._limits.stop + position + 1 + elif isinstance(position, slice): + return slice(-(position.stop+1), + -(position.start+1), + -position.step) + elif isinstance(position, location.Location): + return location.ComplementLocation(position).simplify() + + raise TypeError,"position must be an int, slice or Location instance" + + + \ No newline at end of file diff --git a/obitools/sequenceencoder/__init__.pyc b/obitools/sequenceencoder/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..463f84ff7093942000e74bb1e064d1deeb704581 GIT binary patch literal 4297 zcmd5<-EtH~6h1TilWaC25Fi0XSOs-ek_EIDOD%s0SVd{ARkP93RLRz6rW0nfJF`sp z0;z%+D&iT%FI)%TN7Jm8bw^k_Q zUjffAaoZmtB3z#IMFyU1d*VgD7+-chi3;*u9_@GI-Nlpjr!>3vvewjeE`w;-kQud2KYj3LesdNhNFQ=oNYWxvwb`4 zLigRRyIX16w~d`q+>aXlcxPkK4O9Dg{ra_y=6aks(_g=_akG)`#7-yL@#bLwH{a-V z;v{yR&c^T{paFES<#x-SJA=pV)>oV9VAwbO^s5$^FhMQ;6OG*B_|!NoO+*86XyQhz zi9jJ-O#~w<;Q?0{@hD3)hsPYl(S-AHHKU5)YDNnZRq+4 z<8e}OHMf09rz<7&tA@Jvjte<~bUj(a57)(eBwvcR{M%;z4Cs<(|q=<$=mfl$sx}Sw%oY*H@uMhoZOonS%rX0drBX6g)x7o&}}OlwDMGDh+^9sg?*E{ zI5O@86kk~+EUmwjWof3GXw~{uB+s0M^53U?w}q zHt@4nZIAW7>c0^OfWb+C6Gz!ZUQe1OsUq1XOUlxl(^W%pXwP#Yu-B^)oNe$R*x?zd z0-YR=NMdaALx>5QYJ-t=wH;IICN)sn)tc?cp{b==?b|UgLv39boLsNc2$E}TCptwi z)+vH@sRGlwbeOALH*;f^83tIJbM@rqZUved2~O4XP}>;X81Ei97LBcgp?M+pZ$NeA zv8O+f$H_xL6aFNP0oC~V%U;D>_D*g$n zvRiegXF$RX3KYuSG-fLsnVi|;WH(i!1#Da#4vf2>M!`Fnqd;h-i3w_4dzoT`q5&}r zr?!rU&6J>-(@Iul?~->x{}%iuzu>)ITrMszDQ0m{`8w`l?a7rUz9sQo1W~K#9kz=fJH$o z`2>&)aMjU*oon!&J$w-71r8MW77Uz&dW{Nfgg9gjTQLMHJwnCrha;Dlt+&w0BdBcO zj|Z`{!FA5#x=!CDN-%k*oBpmMVaM!&8u%iUM6>W7O=QPM#Qa3|KK7(XN%1ZsLC<@7 z1mI2?JaswuOJ>$X4XQs}HLd>pp4cmN(aYtFu26H~j-ZK7@Gb?Nr``!ZX7>Vw`l){m zs<-CgHS8!a+uou#1>e*Yr?}yXCvrEOpYRqZsma!5YVs?F2FHP@m2Ezvk=(B2!Df>I z(_sIBMsDJ`Jja?%2)Qp+q@3ND^^tYiCo~$+KygjfGkG>dcoM?_~PmJqp|sFxP?)D6UehL+C*-&xT`x z57rb%3`%rm zuTriQ7YnUAtDBPHMM?gF}mT;@_*?+*F!A#d(vMmtX}>X DO-RCX literal 0 HcmV?d00001 diff --git a/obitools/solexa/__init__.py b/obitools/solexa/__init__.py new file mode 100644 index 0000000..60e35f8 --- /dev/null +++ b/obitools/solexa/__init__.py @@ -0,0 +1,45 @@ +from obitools import utils +from obitools import NucSequence +from obitools.dnahash import hashCodeIterator + + +class SolexaSequence(NucSequence): + def __init__(self,id,seq,definition=None,quality=None,**info): + NucSequence.__init__(self, id, seq, definition,**info) + self._quality=quality + self._hash=None + + def getQuality(self): + if isinstance(self._quality, str): + self._quality=[int(x) for x in self._quality.split()] + return self._quality + + + def __hash__(self): + if self._hash is None: + self._hash = hashCodeIterator(str(self), len(str(self)), 16, 0).next()[1].pop() + return self._hash + +class SolexaFile(utils.ColumnFile): + def __init__(self,stream): + utils.ColumnFile.__init__(self, + stream, ':', True, + (str, + int,int,int,int, + str, + str), "#") + + + def next(self): + data = utils.ColumnFile.next(self) + seq = SolexaSequence('%d_%d_%d_%d'%(data[1],data[2],data[3],data[4]), + data[5], + quality=data[6]) + seq['machine']=data[0] + seq['channel']=data[1] + seq['tile']=data[2] + seq['pos_x']=data[3] + seq['pos_y']=data[4] + + #assert len(seq['quality'])==len(seq),"Error in file format" + return seq diff --git a/obitools/statistics/__init__.py b/obitools/statistics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/statistics/hypergeometric.py b/obitools/statistics/hypergeometric.py new file mode 100644 index 0000000..9a9b812 --- /dev/null +++ b/obitools/statistics/hypergeometric.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +""" + Module de calcules statistiques. + + Le module `statistics` contient des fonctions permettant le calcule + des probabilités associées à la loi hypergéométrique et + hypergéométrique cumulée, ainsi d'une méthode de correction pour les + tests multiples. + +""" + +from decimal import * + +getcontext().prec = 28 + + +def _hyper0(N,n,r): + """ + Fonction interne permetant le calcule du terme 0 de la loi hypergéométrique. + + Le calcule est réalisé selon la méthode décrite dans l'article + + Trong Wu, An accurate computation of the hypergeometric distribution function, + ACM Trans. Math. Softw. 19 (1993), no. 1, 33–43. + + Paramètres: + + - `N` : La taille de la population + - `n` : Le nombre d'éléments marqués + - `r` : La taille de l'echantillon + + Retourne un *float* indiquant la probabilité de récupérer 0 élément + marqué parmi *n* dans une population de taille *N* lors du tirage + d'un échantillon de taille *r* + """ + + # + # au numerateur nous avons : + # [N -r + 1 -n;N - n + 1[ + # + # au denominateur : + # [N - r + 1; N + 1] + # + # avec X = N - r + 1 + # et Y = N + 1 + # + # Numerateur -> [ X - n; Y - n [ + # Denominateur -> [ X ; Y [ + # + # On peut donc siplifier + # + # Numerateur -> [X - n; X [ + # Denominateur -> [Y - n; Y [ + + numerateur = xrange(N - r + 1 - n, N - r + 1) + denominateur= xrange(N + 1 - n, N + 1) +# +# version original +# +# m = N - n +# numerateur = set(range(m-r+1,m+1)) +# denominateur = set(range(N-r+1,N+1)) +# simplification = numerateur & denominateur +# numerateur -= simplification +# denominateur -= simplification +# numerateur = list(numerateur) +# denominateur=list(denominateur) +# numerateur.sort() +# denominateur.sort() + + + p = reduce(lambda x,y:x*y,map(lambda i,j:Decimal(i)/Decimal(j),numerateur,denominateur)) + return p + + +def hypergeometric(x,N,n,r): + """ + Calcule le terme *x* d'une loi hypergéométrique + + Le calcule est réalisé selon la méthode décrite dans l'article + + Trong Wu, An accurate computation of the hypergeometric distribution function, + ACM Trans. Math. Softw. 19 (1993), no. 1, 33–43. + + Paramètres: + + - `x` : Nombre d'éléments marqués attendu + - `N` : La taille de la population + - `n` : Le nombre d'éléments marqués + - `r` : La taille de l'echantillon + + Retourne un *float* indiquant la probabilité de récupérer *x* éléments + marqués parmi *n* dans une population de taille *N* lors du tirage + d'un échantillon de taille *r* + """ + if n < r: + s = n + n = r + r = s + assert x>=0 and x <= r,"x out of limits" + if x > 0 : + return hypergeometric(x-1,N,n,r) * (n - x + 1)/x * (r - x + 1)/(N-n-r+x) + else: + return _hyper0(N,n,r) + +def chypergeometric(xmin,xmax,N,n,r): + """ + Calcule le terme *x* d'une loi hypergéométrique + + Le calcule est réalisé selon la méthode décrite dans l'article + + Trong Wu, An accurate computation of the hypergeometric distribution function, + ACM Trans. Math. Softw. 19 (1993), no. 1, 33–43. + + Paramètres: + + - `xmin` : Nombre d'éléments marqués minimum attendu + - `xmax` : Nombre d'éléments marqués maximum attendu + - `N` : La taille de la population + - `n` : Le nombre d'éléments marqués + - `r` : La taille de l'echantillon + + Retourne un *float* indiquant la probabilité de récupérer entre + *xmin* et *xmax* éléments marqués parmi *n* dans une population + de taille *N* lors du tirage d'un échantillon de taille *r* + """ + if n < r: + s = n + n = r + r = s + assert xmin>=0 and xmin <= r and xmax>=0 and xmax <= r and xmin <=xmax,"x out of limits" + hg = hypergeometric(xmin,N,n,r) + rep = hg + for x in xrange(xmin+1,xmax+1): + hg = hg * (n - x + 1)/x * (r - x + 1)/(N-n-r+x) + rep+=hg + return rep + +def multipleTest(globalPvalue,testList): + """ + Correction pour les tests multiples. + + Séléctionne parmis un ensemble de test le plus grand sous ensemble + telque le risque global soit inférieur à une pvalue déterminée. + + Paramètres: + + - `globalPvalue` : Risque global à prendre pour l'ensemble des tests + - `testList` : un élément itérable sur un ensemble de tests. + Chaque test est une liste ou un tuple dont le dernier élément + est la pvalue associée au test + + Retourne une liste contenant le sous ensemble des tests selectionnés dans + `testList` + """ + testList=list(testList) + testList.sort(lambda x,y:cmp(x[-1],y[-1])) + h0=1.0-globalPvalue + p=1.0 + rep = [] + for t in testList: + p*=1.0-t[-1] + if p > h0: + rep.append(t) + return rep + \ No newline at end of file diff --git a/obitools/statistics/noncentralhypergeo.py b/obitools/statistics/noncentralhypergeo.py new file mode 100644 index 0000000..e6a96ce --- /dev/null +++ b/obitools/statistics/noncentralhypergeo.py @@ -0,0 +1,208 @@ +from decimal import * +from math import log + +#from obitools.utils import moduleInDevelopment + +#moduleInDevelopment(__name__) + +# from : http://www.programmish.com/?p=25 + +def dec_log(self, base=10): + cur_prec = getcontext().prec + getcontext().prec += 2 + baseDec = Decimal(10) + retValue = self + + if isinstance(base, Decimal): + baseDec = base + elif isinstance(base, float): + baseDec = Decimal("%f" % (base)) + else: + baseDec = Decimal(base) + + integer_part = Decimal(0) + while retValue < 1: + integer_part = integer_part - 1 + retValue = retValue * baseDec + while retValue >= baseDec: + integer_part = integer_part + 1 + retValue = retValue / baseDec + + retValue = retValue ** 10 + decimal_frac = Decimal(0) + partial_part = Decimal(1) + while cur_prec > 0: + partial_part = partial_part / Decimal(10) + digit = Decimal(0) + while retValue >= baseDec: + digit += 1 + retValue = retValue / baseDec + decimal_frac = decimal_frac + digit * partial_part + retValue = retValue ** 10 + cur_prec -= 1 + getcontext().prec -= 2 + + return integer_part + decimal_frac + +class Interval(object): + def __init__(self,begin,end,facteur=1): + self._begin = begin + self._end = end + self._facteur=facteur + + def __str__(self): + return '[%d,%d] ^ %d' % (self._begin,self._end,self._facteur) + + def __repr__(self): + return 'Interval(%d,%d,%d)' % (self._begin,self._end,self._facteur) + + def begin(self): + return (self._begin,self._facteur,True) + + def end(self): + return (self._end,-self._facteur,False) + + +def cmpb(i1,i2): + x= cmp(i1[0],i2[0]) + if x==0: + x = cmp(i2[2],i1[2]) + return x + +class Product(object): + def __init__(self,i=None): + if i is not None: + self.prod=[i] + else: + self.prod=[] + self._simplify() + + def _simplify(self): + bornes=[] + prod =[] + + if self.prod: + + for i in self.prod: + bornes.append(i.begin()) + bornes.append(i.end()) + bornes.sort(cmpb) + + + j=0 + r=len(bornes) + for i in xrange(1,len(bornes)): + if bornes[i][0]==bornes[j][0] and bornes[i][2]==bornes[j][2]: + bornes[j]=(bornes[j][0],bornes[j][1]+bornes[i][1],bornes[i][2]) + r-=1 + else: + j+=1 + bornes[j]=bornes[i] + + bornes=bornes[0:r] + + facteur=0 + close=1 + + for b,level,open in bornes: + if not open: + close=0 + else: + close=1 + if facteur: + prod.append(Interval(debut,b-close,facteur)) + debut=b+1-close + facteur+=level + + self.prod=prod + + + + + def __mul__(self,p): + res = Product() + res.prod=list(self.prod) + res.prod.extend(p.prod) + res._simplify() + return res + + def __div__(self,p): + np = Product() + np.prod = [Interval(x._begin,x._end,-x._facteur) for x in p.prod] + return self * np + + def __str__(self): + return str(self.prod) + + def log(self): + p=Decimal(0) + for k in self.prod: + p+= Decimal(k._facteur) * reduce(lambda x,y:x+dec_log(Decimal(y),Decimal(10)),xrange(k._begin,k._end+1),Decimal(0)) + return p + + def product(self): + p=Decimal(1) + for k in self.prod: + p*= reduce(lambda x,y:x*Decimal(y),xrange(k._begin,k._end+1),Decimal(1)) ** Decimal(k._facteur) + return p + + def __call__(self,log=True): + if log: + return self.log() + else: + return self.product() + + +def fact(n): + return Product(Interval(1,n)) + +def cnp(n,p): + return fact(n)/fact(p)/fact(n-p) + +def hypergeometic(x,n,M,N): + ''' + + @param x: Variable aleatoire + @type x: int + @param n: taille du tirage + @type n: int + @param M: boule gagnante + @type M: int + @param N: nombre total dans l'urne + @type N: int + + p(x)= cnp(M,x) * cnp(N-M,n-x) / cnp(N,n) + ''' + return cnp(M,x) * cnp(N-M,n-x) / cnp(N,n) + +def nchypergeometique(x,n,M,N,r): + ''' + + @param x: Variable aleatoire + @type x: int + @param n: taille du tirage + @type n: int + @param M: boule gagnante + @type M: int + @param N: nombre total dans l'urne + @type N: int + @param r: odd ratio + @type r: float + + p(x)= cnp(M,x) * cnp(N-M,n-x) / cnp(N,n) + ''' + + xmin = max(0,n-N+M) + xmax = min(n,M) + lr = dec_log(r) + xlr = x * lr + num = cnp(M,x) * cnp(N-M,n-x) + den = [cnp(M,y) * cnp(N-M,n-y) / num for y in xrange(xmin,xmax+1)] + fden = [lr * y - xlr for y in xrange(xmin,xmax+1)] + + inverse=reduce(lambda x,y : x+y, + map(lambda i,j: i(False) * 10**j ,den,fden)) + return 1/inverse + + + \ No newline at end of file diff --git a/obitools/svg.py b/obitools/svg.py new file mode 100644 index 0000000..c42e3ef --- /dev/null +++ b/obitools/svg.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +"""\ +SVG.py - Construct/display SVG scenes. + +The following code is a lightweight wrapper around SVG files. The metaphor +is to construct a scene, add objects to it, and then write it to a file +to display it. + +This program uses ImageMagick to display the SVG files. ImageMagick also +does a remarkable job of converting SVG files into other formats. +""" + +import os +display_prog = 'display' # Command to execute to display images. + +class Scene: + def __init__(self,name="svg",height=400,width=400): + self.name = name + self.items = [] + self.height = height + self.width = width + return + + def add(self,item): self.items.append(item) + + def strarray(self): + var = ["\n", + "\n" % (self.height,self.width), + " \n"] + for item in self.items: var += item.strarray() + var += [" \n\n"] + return var + + def write_svg(self,filename=None): + if filename: + self.svgname = filename + else: + self.svgname = self.name + ".svg" + file = open(self.svgname,'w') + file.writelines(self.strarray()) + file.close() + return + + def display(self,prog=display_prog): + os.system("%s %s" % (prog,self.svgname)) + return + + +class Line: + def __init__(self,start,end): + self.start = start #xy tuple + self.end = end #xy tuple + return + + def strarray(self): + return [" \n" %\ + (self.start[0],self.start[1],self.end[0],self.end[1])] + + +class Circle: + def __init__(self,center,radius,color): + self.center = center #xy tuple + self.radius = radius #xy tuple + self.color = color #rgb tuple in range(0,256) + return + + def strarray(self): + return [" \n" % colorstr(self.color)] + +class Rectangle: + def __init__(self,origin,height,width,color): + self.origin = origin + self.height = height + self.width = width + self.color = color + return + + def strarray(self): + return [" \n" %\ + (self.width,colorstr(self.color))] + +class Text: + def __init__(self,origin,text,size=24): + self.origin = origin + self.text = text + self.size = size + return + + def strarray(self): + return [" \n" %\ + (self.origin[0],self.origin[1],self.size), + " %s\n" % self.text, + " \n"] + + +def colorstr(rgb): return "#%x%x%x" % (rgb[0]/16,rgb[1]/16,rgb[2]/16) + +def test(): + scene = Scene('test') + scene.add(Rectangle((100,100),200,200,(0,255,255))) + scene.add(Line((200,200),(200,300))) + scene.add(Line((200,200),(300,200))) + scene.add(Line((200,200),(100,200))) + scene.add(Line((200,200),(200,100))) + scene.add(Circle((200,200),30,(0,0,255))) + scene.add(Circle((200,300),30,(0,255,0))) + scene.add(Circle((300,200),30,(255,0,0))) + scene.add(Circle((100,200),30,(255,255,0))) + scene.add(Circle((200,100),30,(255,0,255))) + scene.add(Text((50,50),"Testing SVG")) + scene.write_svg() + scene.display() + return + +if __name__ == '__main__': test() diff --git a/obitools/table/__init__.py b/obitools/table/__init__.py new file mode 100644 index 0000000..41e00bd --- /dev/null +++ b/obitools/table/__init__.py @@ -0,0 +1,633 @@ +''' + +''' + +from itertools import imap,count,chain + +from itertools import imap,count,chain + +class Table(list): + """ + Tables are list of rows of the same model + """ + def __init__(self, headers=None, + types=None, + colcount=None, + rowFactory=None, + subrowFactory=None): + ''' + + @param headers: the list of column header. + + if this parametter is C{None}, C{colcount} + parametter must be set. + + @type headers: C{list}, C{tuple} or and iterable object + + @param types: the list of data type associated to each column. + + If this parametter is specified its length must be + equal to the C{headers} length or to C{colcount}. + + @type types: C{list}, C{tuple} or and iterable object + + @param colcount: number of column in the created table. + + If C{headers} parametter is not C{None} this + parametter is ignored + + @type colcount: int + ''' + + assert headers is not None or colcount is not None,\ + 'headers or colcount parametter must be not None value' + + if headers is None: + headers = tuple('Col_%d' % x for x in xrange(colcount)) + + self.headers = headers + self.types = types + self.colcount= len(self.headers) + + if rowFactory is None: + self.rowFactory=TableRow + else: + self.rowFactory=rowFactory + + if subrowFactory is None: + self.subrowFactory=TableRow + else: + self.subrowFactory=rowFactory + + + self.likedTo=set() + + + + def isCompatible(self,data): + assert isinstance(data,(Table,TableRow)) + return (self.colcount == data.colcount and + (id(self.types)==id(data.types) or + self.types==data.types + ) + ) + + def __setitem__ (self,key,value): + ''' + + @param key: + @type key: C{int}, C{slice} or C{str} + @param value: + @type value: + ''' + + if isintance(key,int): + if not isinstance(value, TableRow): + value = self.rowFactory(self,value) + else: + assert self.isCompatible(value) + list.__setitem__(self,key,value.row) + + elif isinstance(key,slice): + indices = xrange(key.indices(len(self))) + for i,d in imap(None,indices,value): + self[i]=d + + else: + raise TypeError, "Key must be an int or slice value" + + def __getitem__(self,key): + ''' + this function has different comportements depending + of the data type of C{key} and the table used. + + @param key: description of the table part to return + @type key: C{int} or C{slice} + + @return: return a TableRow (if key is C{int}) + or a subpart of the table (if key is C{slice}). + ''' + if isinstance(key,int): + return self.rowFactory(self, + list.__getitem__(self,key)) + + if isinstance(key,slice): + newtable=Table(self.headers,self.types) + indices = xrange(key.indices(len(self))) + for i in indices: + list.append(newtable,list.__getitem__(self,i)) + self.likedTo.add(newtable) + return newtable + + raise TypeError + + + def __getslice__(self,x,y): + return self.__getitem__(slice(x,y)) + + def __iter__(self): + return TableIterator(self) + + def __hash__(self): + return id(self) + + def __add__(self,itable): + return concatTables(self,itable) + + def _setTypes(self,types): + if types is not None and not isinstance(type,tuple): + types = tuple(x for x in types) + + assert types is None or len(types)==len(self._headers) + + self._types = types + + if types is not None: + for row in self: + row.castRow() + + def _getTypes(self): + return self._types + + types = property(_getTypes,_setTypes) + + def _getHeaders(self): + return self._headers + + def _setHeaders(self,headers): + if not isinstance(headers, tuple): + headers = tuple(x for x in headers) + + self._hindex = dict((k,i) for i,k in imap(None,count(),headers)) + self._headers=headers + self.colcount=len(headers) + + headers=property(_getHeaders,_setHeaders) + + def append(self,value): + if not isinstance(value, TableRow): + value = self.rowFactory(self,value) + else: + assert self.isCompatible(value) + list.append(self,value.row) + + + +class _Row(list): + def __init__(self,data,size): + if data is None: + list.__init__(self,(None for x in xrange(size))) + else: + list.__init__(self,data) + assert len(self)==size, \ + "Size of data is not correct (%d instead of %d)" % (len(self),size) + + def append(self,value): + raise NotImplementedError, \ + "Rows cannot change of size" + + def pop(self,key=None): + raise NotImplementedError, \ + "Rows cannot change of size" + + def extend(self,values): + raise NotImplementedError, \ + "Rows cannot change of size" + + + + +class TableRow(object): + ''' + + ''' + def __init__(self, table, + data=None, + ): + + self.table = table + + if isinstance(data,_Row): + self.row=row + else: + data = self._castRow(data) + self.row=_Row(data,self._colcount) + + def getType(self): + return self.table.types + + def getHeaders(self): + return self.table.headers + + def getHIndex(self): + return self.table._hindex + + def getColCount(self): + return self.table.colcount + + types = property(getType,None,None, + "List of types associated to this row") + headers= property(getHeaders,None,None, + "List of headers associated to this row") + + _hindex= property(getHIndex,None,None) + _colcount = property(getColCount,None,None) + + def _castValue(t,x): + ''' + Cast a value to a specified type, with exception of + C{None} values that are returned without cast. + + @param t: the destination type + @type t: C{type} + @param x: the value to cast + + @return: the casted value or C{None} + + ''' + if x is None or t is None: + return x + else: + return t(x) + + _castValue=staticmethod(_castValue) + + def _castRow(self,data): + + if not isinstance(data, (list,dict)): + data=[x for x in data] + + if isinstance(data,list): + assert len(data)==self._colcount, \ + 'values has not good length' + if self.types is not None: + data=[TableRow._castValue(t, x) + for t,x in imap(None,self.types,data)] + + elif isinstance(data,dict): + lvalue = [None] * len(self.header) + + for k,v in data.items(): + try: + hindex = self._hindex[k] + if self.types is not None: + lvalue[hindex]=TableRow._castValue(self.types[hindex], v) + else: + lvalue[hindex]=v + except KeyError: + info('%s is not a table column' % k) + + data=lvalue + else: + raise TypeError + + return data + + def __getitem__(self,key): + ''' + + @param key: + @type key: + ''' + + if isinstance(key,(int,slice)): + return self.row[key] + + if isinstance(key,str): + i = self._hindex[key] + return self.row[i] + + raise TypeError, "Key must be an int, slice or str value" + + def __setitem__(self,key,value): + ''' + + @param key: + @type key: + @param value: + @type value: + ''' + + if isinstance(key,str): + key = self._hindex[key] + + elif isinstance(key,int): + if self.types is not None: + value = TableRow._castValue(self.types[key], value) + self.row[key]=value + + elif isinstance(key,slice): + indices = xrange(key.indices(len(self.row))) + for i,v in imap(None,indices,value): + self[i]=v + else: + raise TypeError, "Key must be an int, slice or str value" + + + + def __iter__(self): + ''' + + ''' + return iter(self.row) + + def append(self,value): + raise NotImplementedError, \ + "Rows cannot change of size" + + def pop(self,key=None): + raise NotImplementedError, \ + "Rows cannot change of size" + + def extend(self,values): + raise NotImplementedError, \ + "Rows cannot change of size" + + def __len__(self): + return self._colcount + + def __repr__(self): + return repr(self.row) + + def __str__(self): + return str(self.row) + + def castRow(self): + self.row = _Row(self._castRow(self.row),len(self.row)) + + +class iTableIterator(object): + + def _getHeaders(self): + raise NotImplemented + + def _getTypes(self): + raise NotImplemented + + def _getRowFactory(self): + raise NotImplemented + + def _getSubrowFactory(self): + raise NotImplemented + + def _getColcount(self): + return len(self._getTypes()) + + def __iter__(self): + return self + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + colcount = property(_getColcount,None,None) + + def columnIndex(self,name): + if isinstance(name,str): + return self._reference.headers.index(name) + elif isinstance(name,int): + lh = len(self._reference.headers) + if name < lh and name >=0: + return name + elif name < 0 and name >= -lh: + return lh - name + raise IndexError + raise TypeError + + def next(self): + raise NotImplemented + + +class TableIterator(iTableIterator): + + def __init__(self,table): + if not isinstance(table,Table): + raise TypeError + + self._reftable=table + self._i=0 + + def _getHeaders(self): + return self._reftable.headers + + def _getTypes(self): + return self._reftable.types + + def _getRowFactory(self): + return self._reftable.rowFactory + + def _getSubrowFactory(self): + return self._reftable.subrowFactory + + def columnIndex(self,name): + if isinstance(name,str): + return self._reftable._hindex[name] + elif isinstance(name,int): + lh = len(self._reftable._headers) + if name < lh and name >=0: + return name + elif name < 0 and name >= -lh: + return lh - name + raise IndexError + raise TypeError + + + def rewind(self): + i=0 + + def next(self): + if self._i < len(self._reftable): + rep=self._reftable[self._i] + self._i+=1 + return rep + else: + raise StopIteration + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + + +class ProjectionIterator(iTableIterator): + + def __init__(self,tableiterator,*cols): + self._reference = iter(tableiterator) + + assert isinstance(self._reference, iTableIterator) + + self._selected = tuple(self._reference.columnIndex(x) + for x in cols) + self._headers = tuple(self._reference.headers[x] + for x in self._selected) + + if self._reference.types is not None: + self._types= tuple(self._reference.types[x] + for x in self._selected) + else: + self._types=None + + def _getRowFactory(self): + return self._reference.subrowFactory + + def _getSubrowFactory(self): + return self._reference.subrowFactory + + def _getHeaders(self): + return self._headers + + def _getTypes(self): + return self._types + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + + def next(self): + value = self._reference.next() + value = (value[x] for x in self._selected) + return self.rowFactory(self,value) + +class SelectionIterator(iTableIterator): + def __init__(self,tableiterator,**conditions): + self._reference = iter(tableiterator) + + assert isinstance(self._reference, iTableIterator) + + self._conditions=dict((self._reference.columnIndex(i),c) + for i,c in conditions.iteritems()) + + def _checkCondition(self,row): + return reduce(lambda x,y : x and y, + (bool(self._conditions[i](row[i])) + for i in self._conditions), + True) + + def _getRowFactory(self): + return self._reference.rowFactory + + def _getSubrowFactory(self): + return self._reference.subrowFactory + + def _getHeaders(self): + return self._reference.headers + + def _getTypes(self): + return self._reference.types + + def next(self): + row = self._reference.next() + while not self._checkCondition(row): + row = self._reference.next() + return row + + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + + +class UnionIterator(iTableIterator): + def __init__(self,*itables): + self._itables=[iter(x) for x in itables] + self._types = self._itables[0].types + self._headers = self._itables[0].headers + + assert reduce(lambda x,y: x and y, + ( isinstance(z,iTableIterator) + and len(z.headers)==len(self._headers) + for z in self._itables), + True) + + self._iterator = chain(*self._itables) + + def _getRowFactory(self): + return self._itables[0].rowFactory + + def _getSubrowFactory(self): + return self._itables[0].subrowFactory + + def _getHeaders(self): + return self._headers + + def _getTypes(self): + return self._types + + def next(self): + value = self._iterator.next() + return self.rowFactory(self,value.row) + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + rowFactory = property(_getRowFactory,None,None) + subrowFactory = property(_getSubrowFactory,None,None) + + + +def tableFactory(tableiterator): + tableiterator = iter(tableiterator) + assert isinstance(tableiterator, iTableIterator) + + newtable = Table(tableiterator.headers, + tableiterator.types, + tableiterator.rowFactory, + tableiterator.subrowFactory) + + for r in tableiterator: + newtable.append(r) + + return newtable + +def projectTable(tableiterator,*cols): + return tableFactory(ProjectionIterator(tableiterator,*cols)) + +def subTable(tableiterator,**conditions): + return tableFactory(SelectionIterator(tableiterator,**conditions)) + +def concatTables(*itables): + ''' + Concatene severals tables. + + concatenation is done using the L{UnionIterator} + + @type itables: iTableIterator or Table + + @return: a new Table + @rtype: c{Table} + + @see: L{UnionIterator} + ''' + return tableFactory(UnionIterator(*itables)) + +class TableIteratorAsDict(object): + + def __init__(self,tableiterator): + self._reference = iter(tableiterator) + + assert isinstance(self._reference, iTableIterator) + + self._headers = self._reference.headers + self._types = self._reference.types + if self._types is not None: + self._types = dict((n,t) + for n,t in imap(None,self._headers,self._types)) + + def __iter__(self): + return self + + def next(self): + value = self._reference.next() + return dict((n,t) + for n,t in imap(None,self._headers,value)) + + def _getHeaders(self): + return self._headers + + def _getTypes(self): + return self._types + + headers = property(_getHeaders,None,None) + types = property(_getTypes,None,None) + \ No newline at end of file diff --git a/obitools/table/csv.py b/obitools/table/csv.py new file mode 100644 index 0000000..1d9a73d --- /dev/null +++ b/obitools/table/csv.py @@ -0,0 +1,52 @@ +""" +obitools.table.csv module provides an iterator adapter +allowing to parse csv (comma separatted value) file +""" + +import re + +def csvIterator(lineIterator,sep=','): + ''' + Allows easy parsing of a csv file. This function + convert an iterator on line over a csv text file + in an iterator on data list. Each list corresponds + to all values present n one line. + + @param lineIterator: iterator on text lines + @type lineIterator: iterator + @param sep: string of one letter used as separator + blank charactere or " is not allowed as + separator + @type sep: string + @return: an iterator on data list + @rtype: iterator + ''' + assert len(sep)==1 and not sep.isspace() and sep!='"' + valueMatcher=re.compile('\s*((")(([^"]|"")*)"|([^%s]*?))\s*(%s|$)' % (sep,sep)) + def iterator(): + for l in lineIterator: + yield _csvParse(l,valueMatcher) + return iterator() + + +def _csvParse(line,valueMatcher): + data=[] + i = iter(valueMatcher.findall(line)) + m = i.next() + if m[0]: + while m[-1]!='': + if m[1]=='"': + data.append(m[2].replace('""','"')) + else: + data.append(m[0]) + m=i.next() + if m[1]=='"': + data.append(m[2].replace('""','"')) + else: + data.append(m[0]) + return data + + + + + \ No newline at end of file diff --git a/obitools/tagmatcher/__init__.py b/obitools/tagmatcher/__init__.py new file mode 100644 index 0000000..880ead0 --- /dev/null +++ b/obitools/tagmatcher/__init__.py @@ -0,0 +1,35 @@ +from obitools import NucSequence +from obitools.location import locationGenerator,extractExternalRefs + + + +class TagMatcherSequence(NucSequence): + ''' + Class used to represent a nucleic sequence issued mapped + on a genome by the tagMatcher software. + ''' + + def __init__(self,seq,cd,locs,dm,rm): + NucSequence.__init__(self, seq, seq) + self['locations']=locs + self['conditions']=cd + self['dm']=dm + self['rm']=rm + self['tm']=dm+rm + + def eminEmaxFilter(self,emin=None,emax=None): + result = [x for x in self['locations'] + if (emin is None or x['error'] >=emin) + and (emax is None or x['error'] <=emax)] + self['locations']=result + dm=0 + rm=0 + for x in result: + if x.isDirect(): + dm+=1 + else: + rm+=1 + self['dm']=dm + self['rm']=rm + self['tm']=dm+rm + return self diff --git a/obitools/tagmatcher/options.py b/obitools/tagmatcher/options.py new file mode 100644 index 0000000..45673ce --- /dev/null +++ b/obitools/tagmatcher/options.py @@ -0,0 +1,14 @@ +def addTagMatcherErrorOptions(optionManager): + optionManager.add_option('-E','--emax', + action='store', + metavar="<##>", + type="int",dest="emax", + default=None, + help="keep match with no more than emax errors") + + optionManager.add_option('-e','--emin', + action='store', + metavar="<##>", + type="int",dest="emin", + default=0, + help="keep match with at least emin errors") diff --git a/obitools/tagmatcher/parser.py b/obitools/tagmatcher/parser.py new file mode 100644 index 0000000..a843e66 --- /dev/null +++ b/obitools/tagmatcher/parser.py @@ -0,0 +1,89 @@ +import re +import sys + +from obitools import tagmatcher +from obitools.seqdb import nucEntryIterator +from obitools.location.feature import Feature +from obitools.location import locationGenerator + +_seqMatcher = re.compile('(?<=TG )[acgtrymkwsbdhvnACGTRYMKWSBDHVN]+') +_cdMatcher = re.compile('(?<=CD ) *([^:]+?) +: +([0-9]+)') +_loMatcher = re.compile('(?<=LO ) *([ACGTRYMKWSBDHVN]+) +([^ ]+) +([^ ]+) +\(([0-9]+)\)') +_dmMatcher = re.compile('(?<=DM )[0-9]+') +_rmMatcher = re.compile('(?<=RM )[0-9]+') + + +def __tagmatcherparser(text): + try: + seq = _seqMatcher.search(text).group() + cd = dict((x[0],int(x[1])) for x in _cdMatcher.findall(text)) + locs = [] + + for (match,ac,loc,err) in _loMatcher.findall(text): + feat = Feature('location', locationGenerator(loc)) + feat['error']=int(err) + feat['match']=match + feat['contig']=ac + locs.append(feat) + + dm = int(_dmMatcher.search(text).group()) + rm = int(_rmMatcher.search(text).group()) + + except AttributeError,e: + print >>sys.stderr,'=======================================================' + print >>sys.stderr,text + print >>sys.stderr,'=======================================================' + raise e + + return (seq,cd,locs,dm,rm) + +def tagMatcherParser(text): + return tagmatcher.TagMatcherSequence(*__tagmatcherparser(text)) + + +class TagMatcherIterator(object): + _cdheadparser = re.compile('condition [0-9]+ : (.+)') + + def __init__(self,file): + self._ni = nucEntryIterator(file) + self.header=self._ni.next() + self.conditions=TagMatcherIterator._cdheadparser.findall(self.header) + + def next(self): + return tagMatcherParser(self._ni.next()) + + def __iter__(self): + return self + +def formatTagMatcher(tmseq,reader=None): + if isinstance(tmseq, TagMatcherIterator): + return tmseq.header + + assert isinstance(tmseq,tagmatcher.TagMatcherSequence),'Only TagMatcherSequence can be used' + lo = '\n'.join(['LO %s %s %s (%d)' % (l['match'],l['contig'],l.locStr(),l['error']) + for l in tmseq['locations']]) + if reader is not None: + cd = '\n'.join(['CD %s : %d' % (x,tmseq['conditions'][x]) + for x in reader.conditions]) + else: + cd = '\n'.join(['CD %s : %d' % (x,tmseq['conditions'][x]) + for x in tmseq['conditions']]) + + tg = 'TG %s' % str(tmseq) + + e=[tg] + if cd: + e.append(cd) + if lo: + e.append(lo) + + tm = 'TM %d' % tmseq['tm'] + dm = 'DM %d' % tmseq['dm'] + rm = 'RM %d' % tmseq['rm'] + + e.extend((tm,dm,rm,'//')) + + return '\n'.join(e) + + + diff --git a/obitools/thermo/__init__.py b/obitools/thermo/__init__.py new file mode 100644 index 0000000..492dbb9 --- /dev/null +++ b/obitools/thermo/__init__.py @@ -0,0 +1,597 @@ +from math import log +from array import array +from copy import deepcopy + +bpencoder={'A':1,'C':2,'G':3,'T':4, + 'a':1,'c':2,'g':3,'t':4, + '-':0 + } + +rvencoder={'A':4,'C':3,'G':2,'T':1, + 'a':4,'c':3,'g':2,'t':1, + '-':0 + } + +R = 1.987 +SALT_METHOD_SANTALUCIA = 1 +SALT_METHOD_OWCZARZY = 2 +DEF_CONC_PRIMERS = 8.e-7 +DEF_CONC_SEQUENCES = 0. +DEF_SALT = 0.05 +forbidden_entropy = 0. +forbidden_enthalpy = 1.e18 + +__dH = [[[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]] + ] +__dS = [[[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]], + [[[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]], + [[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.],[0.,0.,0.,0.,0.,0.]]] + ] + +def initParams(c1, c2, kp, sm,nparm={}): + global forbidden_entropy + global dH,dS + + dH=deepcopy(__dH) + dS=deepcopy(__dS) + + nparm['Ct1'] = c1; + nparm['Ct2'] = c2; + nparm['kplus'] = kp; + maxCT = 1; + + if(nparm['Ct2'] > nparm['Ct1']): + maxCT = 2 + + if(nparm['Ct1'] == nparm['Ct2']): + ctFactor = nparm['Ct1']/2 + elif (maxCT == 1): + ctFactor = nparm['Ct1']-nparm['Ct2']/2 + else: + ctFactor = nparm['Ct2']-nparm['Ct1']/2 + + nparm['rlogc'] = R * log(ctFactor) + forbidden_entropy = nparm['rlogc'] + nparm['kfac'] = 0.368 * log(nparm['kplus']) + nparm['saltMethod'] = sm + + + # Set all X-/Y-, -X/Y- and X-/-Y so, that TM will be VERY small! + for x in xrange(1,5): + for y in xrange(1,5): + dH[0][x][y][0]=forbidden_enthalpy; + dS[0][x][y][0]=forbidden_entropy; + dH[x][0][0][y]=forbidden_enthalpy; + dS[x][0][0][y]=forbidden_entropy; + dH[x][0][y][0]=forbidden_enthalpy; + dS[x][0][y][0]=forbidden_entropy; + # forbid X-/Y$ and X$/Y- etc., i.e. terminal must not be paired with gap! + dH[x][5][y][0]=forbidden_enthalpy; + dS[x][5][y][0]=forbidden_entropy; + dH[x][0][y][5]=forbidden_enthalpy; + dS[x][0][y][5]=forbidden_entropy; + dH[5][x][0][y]=forbidden_enthalpy; + dS[5][x][0][y]=forbidden_entropy; + dH[0][x][5][y]=forbidden_enthalpy; + dS[0][x][5][y]=forbidden_entropy; + + #forbid X$/-Y etc. + dH[x][5][0][y]=forbidden_enthalpy; + dS[x][5][0][y]=forbidden_entropy; + dH[x][0][5][y]=forbidden_enthalpy; + dS[x][0][5][y]=forbidden_entropy; + dH[5][x][y][0]=forbidden_enthalpy; + dS[5][x][y][0]=forbidden_entropy; + dH[0][x][y][5]=forbidden_enthalpy; + dS[0][x][y][5]=forbidden_entropy; + + + + #also, forbid x-/-- and --/x-, i.e. no two inner gaps paired + dH[x][0][0][0]=forbidden_enthalpy; + dS[x][0][0][0]=forbidden_entropy; + dH[0][0][x][0]=forbidden_enthalpy; + dS[0][0][x][0]=forbidden_entropy; + # x-/-$ + dH[x][0][0][5]=forbidden_enthalpy; + dS[x][0][0][5]=forbidden_entropy; + dH[5][0][0][x]=forbidden_enthalpy; + dS[5][0][0][x]=forbidden_entropy; + dH[0][5][x][0]=forbidden_enthalpy; + dS[x][0][0][5]=forbidden_entropy; + dH[0][x][5][0]=forbidden_enthalpy; + dS[0][x][5][0]=forbidden_entropy; + + # forbid --/-- + dH[0][0][0][0]=forbidden_enthalpy; + dS[0][0][0][0]=forbidden_entropy; + + dH[5][0][0][0]=forbidden_enthalpy; + dS[5][0][0][0]=forbidden_entropy; + dH[0][0][5][0]=forbidden_enthalpy; + dS[0][0][5][0]=forbidden_entropy; + dH[0][5][5][0]=forbidden_enthalpy; + dS[0][5][5][0]=forbidden_entropy; + + # Interior loops (double Mismatches) + iloop_entropy=-0.97 + iloop_enthalpy=0.0 + + for x in xrange(1,5): + for y in xrange(1,5): + for a in xrange(1,5): + for b in xrange(1,5): + # AT and CG pair, and as A=1, C=2, G=3, T=4 this means + # we have Watson-Crick pairs if (x+a==5) and (y+b)==5. + if ( not ((x+a==5) or (y+b==5))): + # No watson-crick-pair, i.e. double mismatch! + # set enthalpy/entropy to loop expansion! + dH[x][y][a][b] = iloop_enthalpy; + dS[x][y][a][b] = iloop_entropy; + + + # xy/-- and --/xy (Bulge Loops of size > 1) + bloop_entropy=-1.3 + bloop_enthalpy=0.0 + + for x in xrange(1,5): + for y in xrange(1,5): + dH[x][y][0][0] = bloop_enthalpy; + dS[x][y][0][0] = bloop_entropy; + dH[0][0][x][y] = bloop_enthalpy; + dS[0][0][x][y] = bloop_entropy; + + + # x-/ya abd xa/y- as well as -x/ay and ax/-y + # bulge opening and closing parameters with + # adjacent matches / mismatches + # obulge_mism and cbulge_mism chosen so high to avoid + # AAAAAAAAA + # T--G----T + # being better than + # AAAAAAAAA + # TG------T + obulge_match_H =-2.66e3 + obulge_match_S =-14.22 + cbulge_match_H =-2.66e3 + cbulge_match_S =-14.22 + obulge_mism_H = 0.0 + obulge_mism_S = -6.45 + cbulge_mism_H = 0.0 + cbulge_mism_S =-6.45 + + for x in xrange(1,5): + for y in xrange(1,5): + for a in xrange(1,5): + if (x+y==5): # other base pair matches! + + dH[x][0][y][a]=obulge_match_H; # bulge opening + dS[x][0][y][a]=obulge_match_S; + dH[x][a][y][0]=obulge_match_H; + dS[x][a][y][0]=obulge_match_S; + dH[0][x][a][y]=cbulge_match_H; # bulge closing + dS[0][x][a][y]=cbulge_match_S; + dH[a][x][0][y]=cbulge_match_H; + dS[a][x][0][y]=cbulge_match_S; + else: + # mismatch in other base pair! + dH[x][0][y][a]=obulge_mism_H; # bulge opening + dS[x][0][y][a]=obulge_mism_S; + dH[x][a][y][0]=obulge_mism_H; + dS[x][a][y][0]=obulge_mism_S; + dH[0][x][a][y]=cbulge_mism_H; # bulge closing + dS[0][x][a][y]=cbulge_mism_S; + dH[a][x][0][y]=cbulge_mism_H; + dS[a][x][0][y]=cbulge_mism_S; + + + + # Watson-Crick pairs (note that only ten are unique, as obviously + # 5'-AG-3'/3'-TC-5' = 5'-CT-3'/3'-GA-5' etc. + dH[1][1][4][4]=-7.6e3; dS[1][1][4][4]=-21.3 # AA/TT 04 + dH[1][2][4][3]=-8.4e3; dS[1][2][4][3]=-22.4 # AC/TG adapted GT/CA + dH[1][3][4][2]=-7.8e3; dS[1][3][4][2]=-21.0 # AG/TC adapted CT/GA + dH[1][4][4][1]=-7.2e3; dS[1][4][4][1]=-20.4 # AT/TA 04 + dH[2][1][3][4]=-8.5e3; dS[2][1][3][4]=-22.7 # CA/GT 04 + dH[2][2][3][3]=-8.0e3; dS[2][2][3][3]=-19.9 # CC/GG adapted GG/CC + dH[2][3][3][2]=-10.6e3; dS[2][3][3][2]=-27.2 # CG/GC 04 + dH[2][4][3][1]=-7.8e3; dS[2][4][3][1]=-21.0 # CT/GA 04 + dH[3][1][2][4]=-8.2e3; dS[3][1][2][4]=-22.2 # GA/CT 04 + dH[3][2][2][3]=-9.8e3; dS[3][2][2][3]=-24.4 # GC/CG 04 + dH[3][3][2][2]=-8.0e3; dS[3][3][2][2]=-19.9 # GG/CC 04 + dH[3][4][2][1]=-8.4e3; dS[3][4][2][1]=-22.4 # GT/CA 04 + dH[4][1][1][4]=-7.2e3; dS[4][1][1][4]=-21.3 # TA/AT 04 + dH[4][2][1][3]=-8.2e3; dS[4][2][1][3]=-22.2 # TC/AG adapted GA/CT + dH[4][3][1][2]=-8.5e3; dS[4][3][1][2]=-22.7 # TG/AC adapted CA/GT + dH[4][4][1][1]=-7.6e3; dS[4][4][1][1]=-21.3 # TT/AA adapted AA/TT + + # A-C Mismatches (Values for pH 7.0) + dH[1][1][2][4]=7.6e3; dS[1][1][2][4]=20.2 # AA/CT + dH[1][1][4][2]=2.3e3; dS[1][1][4][2]=4.6 # AA/TC + dH[1][2][2][3]=-0.7e3; dS[1][2][2][3]=-3.8 # AC/CG + dH[1][2][4][1]=5.3e3; dS[1][2][4][1]=14.6 # AC/TA + dH[1][3][2][2]=0.6e3; dS[1][3][2][2]=-0.6 # AG/CC + dH[1][4][2][1]=5.3e3; dS[1][4][2][1]=14.6 # AT/CA + dH[2][1][1][4]=3.4e3; dS[2][1][1][4]=8.0 # CA/AT + dH[2][1][3][2]=1.9e3; dS[2][1][3][2]=3.7 # CA/GC + dH[2][2][1][3]=5.2e3; dS[2][2][1][3]=14.2 # CC/AG + dH[2][2][3][1]=0.6e3; dS[2][2][3][1]=-0.6 # CC/GA + dH[2][3][1][2]=1.9e3; dS[2][3][1][2]=3.7 # CG/AC + dH[2][4][1][1]=2.3e3; dS[2][4][1][1]=4.6 # CT/AA + dH[3][1][2][2]=5.2e3; dS[3][1][2][2]=14.2 # GA/CC + dH[3][2][2][1]=-0.7e3; dS[3][2][2][1]=-3.8 # GC/CA + dH[4][1][1][2]=3.4e3; dS[4][1][1][2]=8.0 # TA/AC + dH[4][2][1][1]=7.6e3; dS[4][2][1][1]=20.2 # TC/AA + + # C-T Mismatches + dH[1][2][4][4]=0.7e3; dS[1][2][4][4]=0.2 # AC/TT + dH[1][4][4][2]=-1.2e3; dS[1][4][4][2]=-6.2 # AT/TC + dH[2][1][4][4]=1.0e3; dS[2][1][4][4]=0.7 # CA/TT + dH[2][2][3][4]=-0.8e3; dS[2][2][3][4]=-4.5 # CC/GT + dH[2][2][4][3]=5.2e3; dS[2][2][4][3]=13.5 # CC/TG + dH[2][3][4][2]=-1.5e3; dS[2][3][4][2]=-6.1 # CG/TC + dH[2][4][3][2]=-1.5e3; dS[2][4][3][2]=-6.1 # CT/GC + dH[2][4][4][1]=-1.2e3; dS[2][4][4][1]=-6.2 # CT/TA + dH[3][2][2][4]=2.3e3; dS[3][2][2][4]=5.4 # GC/CT + dH[3][4][2][2]=5.2e3; dS[3][4][2][2]=13.5 # GT/CC + dH[4][1][2][4]=1.2e3; dS[4][1][2][4]=0.7 # TA/CT + dH[4][2][2][3]=2.3e3; dS[4][2][2][3]=5.4 # TC/CG + dH[4][2][1][4]=1.2e3; dS[4][2][1][4]=0.7 # TC/AT + dH[4][3][2][2]=-0.8e3; dS[4][3][2][2]=-4.5 # TG/CC + dH[4][4][2][1]=0.7e3; dS[4][4][2][1]=0.2 # TT/CA + dH[4][4][1][2]=1.0e3; dS[4][4][1][2]=0.7 # TT/AC + + # G-A Mismatches + dH[1][1][3][4]=3.0e3; dS[1][1][3][4]=7.4 # AA/GT + dH[1][1][4][3]=-0.6e3; dS[1][1][4][3]=-2.3 # AA/TG + dH[1][2][3][3]=0.5e3; dS[1][2][3][3]=3.2 # AC/GG + dH[1][3][3][2]=-4.0e3; dS[1][3][3][2]=-13.2 # AG/GC + dH[1][3][4][1]=-0.7e3; dS[1][3][4][1]=-2.3 # AG/TA + dH[1][4][3][1]=-0.7e3; dS[1][4][3][1]=-2.3 # AT/GA + dH[2][1][3][3]=-0.7e3; dS[2][1][3][3]=-2.3 # CA/GG + dH[2][3][3][1]=-4.0e3; dS[2][3][3][1]=-13.2 # CG/GA + dH[3][1][1][4]=0.7e3; dS[3][1][1][4]=0.7 # GA/AT + dH[3][1][2][3]=-0.6e3; dS[3][1][2][3]=-1.0 # GA/CG + dH[3][2][1][3]=-0.6e3; dS[3][2][1][3]=-1.0 # GC/AG + dH[3][3][1][2]=-0.7e3; dS[3][3][1][2]=-2.3 # GG/AC + dH[3][3][2][1]=0.5e3; dS[3][3][2][1]=3.2 # GG/CA + dH[3][4][1][1]=-0.6e3; dS[3][4][1][1]=-2.3 # GT/AA + dH[4][1][1][3]=0.7e3; dS[4][1][1][3]=0.7 # TA/AG + dH[4][3][1][1]=3.0e3; dS[4][3][1][1]=7.4 # TG/AA + + # G-T Mismatches + dH[1][3][4][4]=1.0e3; dS[1][3][4][4]=0.9 # AG/TT + dH[1][4][4][3]=-2.5e3; dS[1][4][4][3]=-8.3 # AT/TG + dH[2][3][3][4]=-4.1e3; dS[2][3][3][4]=-11.7 # CG/GT + dH[2][4][3][3]=-2.8e3; dS[2][4][3][3]=-8.0 # CT/GG + dH[3][1][4][4]=-1.3e3; dS[3][1][4][4]=-5.3 # GA/TT + dH[3][2][4][3]=-4.4e3; dS[3][2][4][3]=-12.3 # GC/TG + dH[3][3][2][4]=3.3e3; dS[3][3][2][4]=10.4 # GG/CT + dH[3][3][4][2]=-2.8e3; dS[3][3][4][2]=-8.0 # GG/TC +# dH[3][3][4][4]=5.8e3; dS[3][3][4][4]=16.3 # GG/TT + dH[3][4][2][3]=-4.4e3; dS[3][4][2][3]=-12.3 # GT/CG + dH[3][4][4][1]=-2.5e3; dS[3][4][4][1]=-8.3 # GT/TA +# dH[3][4][4][3]=4.1e3; dS[3][4][4][3]=9.5 # GT/TG + dH[4][1][3][4]=-0.1e3; dS[4][1][3][4]=-1.7 # TA/GT + dH[4][2][3][3]=3.3e3; dS[4][2][3][3]=10.4 # TC/GG + dH[4][3][1][4]=-0.1e3; dS[4][3][1][4]=-1.7 # TG/AT + dH[4][3][3][2]=-4.1e3; dS[4][3][3][2]=-11.7 # TG/GC +# dH[4][3][3][4]=-1.4e3; dS[4][3][3][4]=-6.2 # TG/GT + dH[4][4][1][3]=-1.3e3; dS[4][4][1][3]=-5.3 # TT/AG + dH[4][4][3][1]=1.0e3; dS[4][4][3][1]=0.9 # TT/GA +# dH[4][4][3][3]=5.8e3; dS[4][4][3][3]=16.3 # TT/GG + + # A-A Mismatches + dH[1][1][1][4]=4.7e3; dS[1][1][1][4]=12.9 # AA/AT + dH[1][1][4][1]=1.2e3; dS[1][1][4][1]=1.7 # AA/TA + dH[1][2][1][3]=-2.9e3; dS[1][2][1][3]=-9.8 # AC/AG + dH[1][3][1][2]=-0.9e3; dS[1][3][1][2]=-4.2 # AG/AC + dH[1][4][1][1]=1.2e3; dS[1][4][1][1]=1.7 # AT/AA + dH[2][1][3][1]=-0.9e3; dS[2][1][3][1]=-4.2 # CA/GA + dH[3][1][2][1]=-2.9e3; dS[3][1][2][1]=-9.8 # GA/CA + dH[4][1][1][1]=4.7e3; dS[4][1][1][1]=12.9 # TA/AA + + # C-C Mismatches + dH[1][2][4][2]=0.0e3; dS[1][2][4][2]=-4.4 # AC/TC + dH[2][1][2][4]=6.1e3; dS[2][1][2][4]=16.4 # CA/CT + dH[2][2][2][3]=3.6e3; dS[2][2][2][3]=8.9 # CC/CG + dH[2][2][3][2]=-1.5e3; dS[2][2][3][2]=-7.2 # CC/GC + dH[2][3][2][2]=-1.5e3; dS[2][3][2][2]=-7.2 # CG/CC + dH[2][4][2][1]=0.0e3; dS[2][4][2][1]=-4.4 # CT/CA + dH[3][2][2][2]=3.6e3; dS[3][2][2][2]=8.9 # GC/CC + dH[4][2][1][2]=6.1e3; dS[4][2][1][2]=16.4 # TC/AC + + # G-G Mismatches + dH[1][3][4][3]=-3.1e3; dS[1][3][4][3]=-9.5 # AG/TG + dH[2][3][3][3]=-4.9e3; dS[2][3][3][3]=-15.3 # CG/GG + dH[3][1][3][4]=1.6e3; dS[3][1][3][4]=3.6 # GA/GT + dH[3][2][3][3]=-6.0e3; dS[3][2][3][3]=-15.8 # GC/GG + dH[3][3][2][3]=-6.0e3; dS[3][3][2][3]=-15.8 # GG/CG + dH[3][3][3][2]=-4.9e3; dS[3][3][3][2]=-15.3 # GG/GC + dH[3][4][3][1]=-3.1e3; dS[3][4][3][1]=-9.5 # GT/GA + dH[4][3][1][3]=1.6e3; dS[4][3][1][3]=3.6 # TG/AG + + # T-T Mismatches + dH[1][4][4][4]=-2.7e3; dS[1][4][4][4]=-10.8 # AT/TT + dH[2][4][3][4]=-5.0e3; dS[2][4][3][4]=-15.8 # CT/GT + dH[3][4][2][4]=-2.2e3; dS[3][4][2][4]=-8.4 # GT/CT + dH[4][1][4][4]=0.2e3; dS[4][1][4][4]=-1.5 # TA/TT + dH[4][2][4][3]=-2.2e3; dS[4][2][4][3]=-8.4 # TC/TG + dH[4][3][4][2]=-5.0e3; dS[4][3][4][2]=-15.8 # TG/TC + dH[4][4][1][4]=0.2e3; dS[4][4][1][4]=-1.5 # TT/AT + dH[4][4][4][1]=-2.7e3; dS[4][4][4][1]=-10.8 # TT/TA + + # Dangling Eds + dH[5][1][1][4]=-0.7e3; dS[5][1][1][4]=-0.8 # $A/AT + dH[5][1][2][4]=4.4e3; dS[5][1][2][4]=14.9 # $A/CT + dH[5][1][3][4]=-1.6e3; dS[5][1][3][4]=-3.6 # $A/GT + dH[5][1][4][4]=2.9e3; dS[5][1][4][4]=10.4 # $A/TT + dH[5][2][1][3]=-2.1e3; dS[5][2][1][3]=-3.9 # $C/AG + dH[5][2][2][3]=-0.2e3; dS[5][2][2][3]=-0.1 # $C/CG + dH[5][2][3][3]=-3.9e3; dS[5][2][3][3]=-11.2 # $C/GG + dH[5][2][4][3]=-4.4e3; dS[5][2][4][3]=-13.1 # $C/TG + dH[5][3][1][2]=-5.9e3; dS[5][3][1][2]=-16.5 # $G/AC + dH[5][3][2][2]=-2.6e3; dS[5][3][2][2]=-7.4 # $G/CC + dH[5][3][3][2]=-3.2e3; dS[5][3][3][2]=-10.4 # $G/GC + dH[5][3][4][2]=-5.2e3; dS[5][3][4][2]=-15.0 # $G/TC + dH[5][4][1][1]=-0.5e3; dS[5][4][1][1]=-1.1 # $T/AA + dH[5][4][2][1]=4.7e3; dS[5][4][2][1]=14.2 # $T/CA + dH[5][4][3][1]=-4.1e3; dS[5][4][3][1]=-13.1 # $T/GA + dH[5][4][4][1]=-3.8e3; dS[5][4][4][1]=-12.6 # $T/TA + dH[1][5][4][1]=-2.9e3; dS[1][5][4][1]=-7.6 # A$/TA + dH[1][5][4][2]=-4.1e3; dS[1][5][4][2]=-13.0 # A$/TC + dH[1][5][4][3]=-4.2e3; dS[1][5][4][3]=-15.0 # A$/TG + dH[1][5][4][4]=-0.2e3; dS[1][5][4][4]=-0.5 # A$/TT + dH[1][1][5][4]=0.2e3; dS[1][1][5][4]=2.3 # AA/$T + dH[1][1][4][5]=-0.5e3; dS[1][1][4][5]=-1.1 # AA/T$ + dH[1][2][5][3]=-6.3e3; dS[1][2][5][3]=-17.1 # AC/$G + dH[1][2][4][5]=4.7e3; dS[1][2][4][5]=14.2 # AC/T$ + dH[1][3][5][2]=-3.7e3; dS[1][3][5][2]=-10.0 # AG/$C + dH[1][3][4][5]=-4.1e3; dS[1][3][4][5]=-13.1 # AG/T$ + dH[1][4][5][1]=-2.9e3; dS[1][4][5][1]=-7.6 # AT/$A + dH[1][4][4][5]=-3.8e3; dS[1][4][4][5]=-12.6 # AT/T$ + dH[2][5][3][1]=-3.7e3; dS[2][5][3][1]=-10.0 # C$/GA + dH[2][5][3][2]=-4.0e3; dS[2][5][3][2]=-11.9 # C$/GC + dH[2][5][3][3]=-3.9e3; dS[2][5][3][3]=-10.9 # C$/GG + dH[2][5][3][4]=-4.9e3; dS[2][5][3][4]=-13.8 # C$/GT + dH[2][1][5][4]=0.6e3; dS[2][1][5][4]=3.3 # CA/$T + dH[2][1][3][5]=-5.9e3; dS[2][1][3][5]=-16.5 # CA/G$ + dH[2][2][5][3]=-4.4e3; dS[2][2][5][3]=-12.6 # CC/$G + dH[2][2][3][5]=-2.6e3; dS[2][2][3][5]=-7.4 # CC/G$ + dH[2][3][5][2]=-4.0e3; dS[2][3][5][2]=-11.9 # CG/$C + dH[2][3][3][5]=-3.2e3; dS[2][3][3][5]=-10.4 # CG/G$ + dH[2][4][5][1]=-4.1e3; dS[2][4][5][1]=-13.0 # CT/$A + dH[2][4][3][5]=-5.2e3; dS[2][4][3][5]=-15.0 # CT/G$ + dH[3][5][2][1]=-6.3e3; dS[3][5][2][1]=-17.1 # G$/CA + dH[3][5][2][2]=-4.4e3; dS[3][5][2][2]=-12.6 # G$/CC + dH[3][5][2][3]=-5.1e3; dS[3][5][2][3]=-14.0 # G$/CG + dH[3][5][2][4]=-4.0e3; dS[3][5][2][4]=-10.9 # G$/CT + dH[3][1][5][4]=-1.1e3; dS[3][1][5][4]=-1.6 # GA/$T + dH[3][1][2][5]=-2.1e3; dS[3][1][2][5]=-3.9 # GA/C$ + dH[3][2][5][3]=-5.1e3; dS[3][2][5][3]=-14.0 # GC/$G + dH[3][2][2][5]=-0.2e3; dS[3][2][2][5]=-0.1 # GC/C$ + dH[3][3][5][2]=-3.9e3; dS[3][3][5][2]=-10.9 # GG/$C + dH[3][3][2][5]=-3.9e3; dS[3][3][2][5]=-11.2 # GG/C$ + dH[3][4][5][1]=-4.2e3; dS[3][4][5][1]=-15.0 # GT/$A + dH[3][4][2][5]=-4.4e3; dS[3][4][2][5]=-13.1 # GT/C$ + dH[4][5][1][1]=0.2e3; dS[4][5][1][1]=2.3 # T$/AA + dH[4][5][1][2]=0.6e3; dS[4][5][1][2]=3.3 # T$/AC + dH[4][5][1][3]=-1.1e3; dS[4][5][1][3]=-1.6 # T$/AG + dH[4][5][1][4]=-6.9e3; dS[4][5][1][4]=-20.0 # T$/AT + dH[4][1][5][4]=-6.9e3; dS[4][1][5][4]=-20.0 # TA/$T + dH[4][1][1][5]=-0.7e3; dS[4][1][1][5]=-0.7 # TA/A$ + dH[4][2][5][3]=-4.0e3; dS[4][2][5][3]=-10.9 # TC/$G + dH[4][2][1][5]=4.4e3; dS[4][2][1][5]=14.9 # TC/A$ + dH[4][3][5][2]=-4.9e3; dS[4][3][5][2]=-13.8 # TG/$C + dH[4][3][1][5]=-1.6e3; dS[4][3][1][5]=-3.6 # TG/A$ + dH[4][4][5][1]=-0.2e3; dS[4][4][5][1]=-0.5 # TT/$A + dH[4][4][1][5]=2.9e3; dS[4][4][1][5]=10.4 # TT/A$ + + + nparm['dH']=dH + nparm['dS']=dS + + return nparm + + +defaultParm=initParams(DEF_CONC_PRIMERS,DEF_CONC_SEQUENCES,DEF_SALT, SALT_METHOD_SANTALUCIA) + +def seqencoder(seq): + return [bpencoder[x] for x in seq] + +def getInitialEntropy(nparm=defaultParm): + return -5.9+nparm['rlogc'] + +def getEnthalpy(x0, x1, y0, y1,nparm=defaultParm): + return nparm['dH'][x0][x1][y0][y1] + +def GetEntropy(x0, x1, y0, y1,nparm=defaultParm): + + nx0=x0 + nx1=x1 + ny0=y0 + ny1=y1 + dH=nparm['dH'] + dS=nparm['dS'] + answer = dS[nx0][nx1][ny0][ny1] + + if (nparm['saltMethod'] == SALT_METHOD_SANTALUCIA): + if(nx0!=5 and 1<= nx1 and nx1<=4): + answer += 0.5*nparm['kfac'] + + if(ny1!=5 and 1<= ny0 and ny0<=4): + answer += 0.5*nparm['kfac'] + + if (nparm['saltMethod'] == SALT_METHOD_OWCZARZY): + logk = log(nparm['kplus']); + answer += dH[nx0][nx1][ny0][ny1]*((4.29 * nparm['gcContent']-3.95)* 1e-5 * logk + 0.0000094*logk**2); + + return answer; + +def CalcTM(entropy,enthalpy): + tm = 0 + if (enthalpy>=forbidden_enthalpy) : + return 0; + + if (entropy<0) : + tm = enthalpy/entropy + if (tm<0): + return 0; + + return tm; + + + + +def countGCContent(seq): + count = 0; + for k in seq : + if k in 'cgGC': + count+=1; + return count; + + +#def cleanSeq (inseq,outseq,length): +# +# seqlen = len(inseq) +# if (len != 0) +# seqlen = length; +# +# j=0 +# for i in xrange(seqlen): +# { +# switch (inseq[i]) +# { +# case 'a': +# case '\0': +# case 'A': +# outseq[j++] = 'A'; break; +# case 'c': +# case '\1': +# case 'C': +# outseq[j++] = 'C'; break; +# case 'g': +# case '\2': +# case 'G': +# outseq[j++] = 'G'; break; +# case 't': +# case '\3': +# case 'T': +# outseq[j++] = 'T'; break; +# } +# } +# outseq[j] = '\0'; +#} + +def calcSelfTM(seq,nparm=defaultParm): + dH=nparm['dH'] + dS=nparm['dS'] + length=len(seq) + + thedH = 0; + thedS = -5.9+nparm['rlogc'] + for i in xrange(1,length): + c1 = rvencoder[seq[i-1]]; + c2 = rvencoder[seq[i]]; + c3 = bpencoder[seq[i-1]]; + c4 = bpencoder[seq[i]]; + + thedH += dH[c3][c4][c1][c2]; + thedS += GetEntropy(c3, c4, c1, c2, nparm) + + mtemp = CalcTM(thedS,thedH); +# print thedH,thedS,nparm['rlogc'] + return mtemp-273.15; + + +def calcTMTwoSeq(seq1,seq2,nparm=defaultParm): + + thedH = 0; + thedS = -5.9+nparm['rlogc'] + dH=nparm['dH'] + dS=nparm['dS'] + length=len(seq1) + + for i in xrange(1,length): + c1 = rvencoder[seq2[i-1]] + c2 = rvencoder[seq2[i]] + c3 = bpencoder[seq1[i-1]] + c4 = bpencoder[seq1[i]] + + thedH += dH[c3][c4][c1][c2] + thedS += GetEntropy(c3, c4, c1, c2, nparm) + + mtemp = CalcTM(thedS,thedH); +# print thedH,thedS,nparm['rlogc'] + + return mtemp-273.15; + + diff --git a/obitools/tools/__init__.py b/obitools/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/obitools/tools/_solexapairend.so b/obitools/tools/_solexapairend.so new file mode 100755 index 0000000000000000000000000000000000000000..2d9e0751dea91c7af613fb7f472165312543e4bb GIT binary patch literal 130384 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 z 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 Kw)oV*WcuG|%E$8n literal 0 HcmV?d00001 diff --git a/obitools/tools/solexapairend.py b/obitools/tools/solexapairend.py new file mode 100644 index 0000000..609f533 --- /dev/null +++ b/obitools/tools/solexapairend.py @@ -0,0 +1,51 @@ +''' +Created on 17 mai 2010 + +@author: coissac +''' + +from obitools.alignment import columnIterator + + +def iterOnAligment(ali): + pos0=0 + pos1=len(ali[1].wrapped)-1 + begin0=False + end0=False + begin1=False + end1=False + for nuc0,nuc1 in columnIterator(ali): + if nuc0=='-': + if begin0: + if not end0: + score0 = ( ali[0].wrapped.quality[pos0-1] + +ali[0].wrapped.quality[pos0] + )/2 + else: + score0 = 1. + else: + score0 = 0. + else: + begin0=True + score0 = ali[0].wrapped.quality[pos0] + pos0+=1 + end0= pos0==len(ali[0].wrapped) + + if nuc1=='-': + if begin1: + if not end1: + score1 = ( ali[1].wrapped.wrapped.quality[pos1] + +ali[1].wrapped.wrapped.quality[pos1+1] + )/2 + else: + score1 = 0. + else: + score1 = 1. + else: + begin1=True + score1 = ali[1].wrapped.wrapped.quality[pos1] + pos1-=1 + end1=pos1<0 + + result = (nuc0,score0,nuc1,score1) + yield result diff --git a/obitools/tree/__init__.py b/obitools/tree/__init__.py new file mode 100644 index 0000000..facb5ff --- /dev/null +++ b/obitools/tree/__init__.py @@ -0,0 +1,116 @@ +import re + + +class Tree(set): + def registerNode(self,node): + assert isinstance(node, TreeNode) + self.add(node) + + def childNodeIterator(self,node): + assert isinstance(node, TreeNode) + return (x for x in self if x._parent==node) + + def subTreeSize(self,node): + n=1 + for subnode in self.childNodeIterator(node): + n+=self.subTreeSize(subnode) + return n + + def getRoot(self): + roots = [x for x in self if x._parent is None] + assert len(roots)==1,'Tree cannot have several root node' + return roots[0] + + def ancestorNodeIterator(self,node): + assert isinstance(node, TreeNode) + while node._parent is not None: + yield node + node=node._parent + yield node + + def terminalNodeIterator(self): + return (x for x in self if x._isterminal) + + def commonAncestor(self,node1,node2): + anc1 = set(x for x in self.ancestorNodeIterator(node1)) + rep = [x for x in self.ancestorNodeIterator(node2) + if x in anc1] + assert len(rep)>=1 + return rep[0] + + def getDist(self,node1,node2): + ca = self.commonAncestor(node1, node2) + dist = 0 + while node1 != ca: + dist+=node1._dist + node1=node1._parent + while node2 != ca: + dist+=node2._dist + node2=node2._parent + return dist + + def farestNodes(self): + dmax=0 + n1=None + n2=None + for node1 in self.terminalNodeIterator(): + for node2 in self.terminalNodeIterator(): + d = self.getDist(node1, node2) + if d > dmax: + dmax = d + n1=node1 + n2=node2 + return node1,node2,dmax + + def setRoot(self,node,dist): + assert node in self + assert node._parent and node._dist > dist + + newroot = TreeNode(self) + parent = node._parent + node._parent = newroot + compdist = node._dist - dist + node._dist=dist + node = parent + + while node: + parent = node._parent + if parent: + dist = node._dist + + node._parent = newroot + node._dist = compdist + + newroot = node + node = parent + + if node: + compdist=dist + + for child in self.childNodeIterator(newroot): + child._parent = newroot._parent + child._dist += newroot._dist + + self.remove(newroot) + + +class TreeNode(object): + def __init__(self,tree,name=None,dist=None,bootstrap=None,**info): + self._parent=None + self._name=name + self._dist=dist + self._bootstrap=bootstrap + self._info=info + tree.registerNode(self) + self._isterminal=True + + + def linkToParent(self,parent): + assert isinstance(parent, TreeNode) or parent is None + self._parent=parent + if parent is not None: + parent._isterminal=False + + + + diff --git a/obitools/tree/dot.py b/obitools/tree/dot.py new file mode 100644 index 0000000..a21c4a1 --- /dev/null +++ b/obitools/tree/dot.py @@ -0,0 +1,18 @@ + +from obitools.utils import universalOpen +from obitools.tree import Tree,TreeNode + +def nodeWriter(tree,node,nodes): + data=[] + if node._parent: + data.append('%d -> %d ' % (nodes[node],nodes[node._parent])) + return "\n".join(data) + + +def treeWriter(tree): + nodes=dict(map(None,tree,xrange(len(tree)))) + code=[] + for node in tree: + code.append(nodeWriter(tree,node,nodes)) + code = "\n".join(code) + return 'digraph tree { node [shape=point]\n%s\n};' % code \ No newline at end of file diff --git a/obitools/tree/layout.py b/obitools/tree/layout.py new file mode 100644 index 0000000..a39ba77 --- /dev/null +++ b/obitools/tree/layout.py @@ -0,0 +1,103 @@ + +class NodeLayout(dict): + ''' + Layout data associated to a tree node. + ''' + pass + +class TreeLayout(dict): + ''' + Description of a phylogenetic tree layout + + @see: + ''' + def addNode(self,node): + self[node]=NodeLayout() + + def setAttribute(self,node,key,value): + self[node][key]=value + + def hasAttribute(self,node,key): + return key in self[node] + + def getAttribute(self,node,key,default=None): + return self[node].get(key,default) + + def setNodesColor(self,color,predicate=True): + ''' + + @param color: + @type color: + @param predicat: + @type predicat: + ''' + for node in self: + if callable(predicat): + change = predicat(node) + else: + change = predicat + + if change: + if callable(color): + c = color(node) + else: + c = color + self.setAttribute(node, 'color', color) + + def setCircular(self,iscircularpredicat): + for node in self: + if callable(iscircularpredicat): + change = iscircularpredicat(node) + else: + change = iscircularpredicat + + if change: + self.setAttribute(node, 'shape', 'circle') + else: + self.setAttribute(node, 'shape', 'square') + + def setRadius(self,radius,predicate=True): + for node in self: + if callable(predicat): + change = predicat(node) + else: + change = predicat + + if change: + if callable(radius): + r = radius(node) + else: + r = radius + self.setAttribute(node, 'radius', r) + +def predicatGeneratorIsInfoEqual(info,value): + def isInfoEqual(node): + data = node._info + return data is not None and info in data and data[info]==value + + return isInfoEqual + +def isTerminalNode(node): + return node._isterminal + +def constantColorGenerator(color): + def colorMaker(node): + return color + + return colorMaker + +def constantColorGenerator(color): + def colorMaker(node): + return color + + return colorMaker + +def notPredicatGenerator(predicate): + def notpred(x): + return not predicat(x) + return notpred + + + + + \ No newline at end of file diff --git a/obitools/tree/newick.py b/obitools/tree/newick.py new file mode 100644 index 0000000..c69d0d3 --- /dev/null +++ b/obitools/tree/newick.py @@ -0,0 +1,117 @@ +import re +import sys + +from obitools.utils import universalOpen +from obitools.tree import Tree,TreeNode + +def subNodeIterator(data): + level=0 + start = 1 + if data[0]=='(': + for i in xrange(1,len(data)): + c=data[i] + if c=='(': + level+=1 + elif c==')': + level-=1 + if c==',' and not level: + yield data[start:i] + start = i+1 + yield data[start:i] + else: + yield data + + +_nodeParser=re.compile('\s*(?P\(.*\))?(?P[^ :]+)? *(?P[0-9.]+)?(:(?P-?[0-9.]+))?') + +def nodeParser(data): + parsedNode = _nodeParser.match(data).groupdict(0) + if not parsedNode['name']: + parsedNode['name']=None + + if not parsedNode['bootstrap']: + parsedNode['bootstrap']=None + else: + parsedNode['bootstrap']=float(parsedNode['bootstrap']) + + if not parsedNode['distance']: + parsedNode['distance']=None + else: + parsedNode['distance']=float(parsedNode['distance']) + + if not parsedNode['subnodes']: + parsedNode['subnodes']=None + + return parsedNode + +_cleanTreeData=re.compile('\s+') + +def treeParser(data,tree=None,parent=None): + if tree is None: + tree = Tree() + data = _cleanTreeData.sub(' ',data).strip() + + parsedNode = nodeParser(data) + node = TreeNode(tree, + parsedNode['name'], + parsedNode['distance'], + parsedNode['bootstrap']) + + node.linkToParent(parent) + + if parsedNode['subnodes']: + for subnode in subNodeIterator(parsedNode['subnodes']): + treeParser(subnode,tree,node) + return tree + +_treecomment=re.compile('\[.*\]') + +def treeIterator(file): + file = universalOpen(file) + data = file.read() + + comment = _treecomment.findall(data) + data=_treecomment.sub('',data).strip() + + if comment: + comment=comment[0] + else: + comment=None + for tree in data.split(';'): + t = treeParser(tree) + if comment: + t.comment=comment + yield t + +def nodeWriter(tree,node,deep=0): + name = node._name + if name is None: + name='' + + distance=node._dist + if distance is None: + distance='' + else: + distance = ':%6.5f' % distance + + bootstrap=node._bootstrap + if bootstrap is None: + bootstrap='' + else: + bootstrap=' %d' % int(bootstrap) + + nodeseparator = ',\n' + ' ' * (deep+1) + + subnodes = nodeseparator.join([nodeWriter(tree, x, deep+1) + for x in tree.childNodeIterator(node)]) + if subnodes: + subnodes='(\n' + ' ' * (deep+1) + subnodes + '\n' + ' ' * deep + ')' + + return '%s%s%s%s' % (subnodes,name,bootstrap,distance) + +def treeWriter(tree,startnode=None): + if startnode is not None: + root=startnode + else: + root = tree.getRoot() + return nodeWriter(tree,root)+';' diff --git a/obitools/tree/svg.py b/obitools/tree/svg.py new file mode 100644 index 0000000..ff51a8c --- /dev/null +++ b/obitools/tree/svg.py @@ -0,0 +1,70 @@ +import math + +from obitools.svg import Scene,Circle,Line,Rectangle,Text +from obitools.tree import Tree + +def displayTreeLayout(layout,width=400,height=400,radius=3,scale=1.0): + ''' + Convert a tree layout object in an svg file. + + @param layout: the tree layout object + @type layout: obitools.tree.layout.TreeLayout + @param width: svg document width + @type width: int + @param height: svg document height + @type height: int + @param radius: default radius of node in svg unit (default 3) + @type radius: int + @param scale: scale factor applied to the svg coordinates (default 1.0) + @type scale: float + + @return: str containing svg code + ''' + xmin = min(layout.getAttribute(n,'x') for n in layout) + xmax = max(layout.getAttribute(n,'x') for n in layout) + ymin = min(layout.getAttribute(n,'y') for n in layout) + ymax = max(layout.getAttribute(n,'y') for n in layout) + + dx = xmax - xmin + dy = ymax - ymin + + xscale = width * 0.95 / dx * scale + yscale = height * 0.95 / dy * scale + + def X(x): + return (x - xmin ) * xscale + width * 0.025 + + def Y(y): + return (y - ymin ) * yscale + height * 0.025 + + scene = Scene('unrooted', height, width) + + for n in layout: + if n._parent is not None: + parent = n._parent + xf = layout.getAttribute(n,'x') + yf = layout.getAttribute(n,'y') + xp = layout.getAttribute(parent,'x') + yp = layout.getAttribute(parent,'y') + scene.add(Line((X(xf),Y(yf)),(X(xp),Y(yp)))) + + for n in layout: + xf = layout.getAttribute(n,'x') + yf = layout.getAttribute(n,'y') + cf = layout.getAttribute(n,'color') + sf = layout.getAttribute(n,'shape') + if layout.hasAttribute(n,'radius'): + rf=layout.getAttribute(n,'radius') + else: + rf=radius + + if sf=='circle': + scene.add(Circle((X(xf),Y(yf)),rf,cf)) + else: + scene.add(Rectangle((X(xf)-rf,Y(yf)-rf),2*rf,2*rf,cf)) + + + return ''.join(scene.strarray()) + + + \ No newline at end of file diff --git a/obitools/tree/unrooted.py b/obitools/tree/unrooted.py new file mode 100644 index 0000000..9a9f3e6 --- /dev/null +++ b/obitools/tree/unrooted.py @@ -0,0 +1,33 @@ +from obitools.tree.layout import TreeLayout +import math + +def subtreeLayout(tree,node,layout,start,end,x,y,default): + nbotu = tree.subTreeSize(node) + delta = (end-start)/(nbotu+1) + + layout.addNode(node) + layout.setAttribute(node,'x',x) + layout.setAttribute(node,'y',y) + layout.setAttribute(node,'color',(255,0,0)) + layout.setAttribute(node,'shape','circle') + + for subnode in tree.childNodeIterator(node): + snbotu = tree.subTreeSize(subnode) + end = start + snbotu * delta + med = start + snbotu * delta /2 + r = subnode._dist + if r is None or r <=0: + r=default + subx=math.cos(med) * r + x + suby=math.sin(med) * r + y + subtreeLayout(tree, subnode, layout, start, end, subx, suby, default) + start=end + + return layout + +def treeLayout(tree): + layout = TreeLayout() + root = tree.getRoot() + dmin = min(n._dist for n in tree if n._dist is not None and n._dist > 0) + return subtreeLayout(tree,root,layout,0,2*math.pi,0,0,dmin / 100) + \ No newline at end of file diff --git a/obitools/unit/__init__.py b/obitools/unit/__init__.py new file mode 100644 index 0000000..d02c812 --- /dev/null +++ b/obitools/unit/__init__.py @@ -0,0 +1,8 @@ +import unittest + +from obitools import tests_group as obitools_tests_group + +tests_group=obitools_tests_group + + + diff --git a/obitools/unit/obitools/__init__.py b/obitools/unit/obitools/__init__.py new file mode 100644 index 0000000..ab1bcec --- /dev/null +++ b/obitools/unit/obitools/__init__.py @@ -0,0 +1,89 @@ +import unittest + +import obitools + +class BioseqTest(unittest.TestCase): + + sequenceId = 'id1' + sequenceDefinition = 'sequence definition' + sequenceQualifier = {'extra':3} + + def setUp(self): + self.bioseq = self.bioseqClass(self.sequenceId, + self.sequenceString, + self.sequenceDefinition, + **self.sequenceQualifier) + + title = self.__doc__.strip() + underline = "=" * len(title) + + #print "%s\n%s" % (title,underline) + + def tearDown(self): + pass + #print "\n" + + def testIdAttribute(self): + ''' + test if id attribute exists + ''' + self.failUnless(hasattr(self.bioseq, 'id'), 'id missing attribute') + + def testIdValue(self): + ''' + test if id attribute value is 'id1' + ''' + self.failUnlessEqual(self.bioseq.id, 'id1', + 'identifier is created with good value') + + def testDefinitionAttribute(self): + ''' + test if definition attribute exists + ''' + self.failUnless(hasattr(self.bioseq, 'definition'), 'definition missing attribute') + + def testSequenceIsLowerCase(self): + ''' + test if sequence is stored as lower case letter + ''' + self.failUnlessEqual(str(self.bioseq), + str(self.bioseq).lower(), + "Sequence is not stored as lower case string") + + def testSequenceQualifier(self): + ''' + test if the extra qualifier is present and its value is three. + ''' + self.failUnlessEqual(self.bioseq['extra'], + 3, + "Sequence qualifier cannot be successfully retrieve") + + def testCreateSequenceQualifier(self): + self.bioseq['testqualifier']='ok' + self.failUnlessEqual(self.bioseq['testqualifier'], + 'ok', + "Sequence qualifier cannot be successfully created") + + + +class NucBioseqTest(BioseqTest): + ''' + Test obitools.NucSequence class + ''' + + bioseqClass = obitools.NucSequence + sequenceString = 'AACGT' * 5 + + +class AABioseqTest(BioseqTest): + ''' + Test obitools.AASequence class + ''' + + bioseqClass = obitools.AASequence + sequenceString = 'MLKCVT' * 5 + + + + +tests_group = [NucBioseqTest,AABioseqTest] \ No newline at end of file diff --git a/obitools/utils/__init__.py b/obitools/utils/__init__.py new file mode 100644 index 0000000..fd7076f --- /dev/null +++ b/obitools/utils/__init__.py @@ -0,0 +1,324 @@ +import sys + +import time +import re +import shelve + +from threading import Lock +from logging import warning +import urllib2 + +from obitools.gzip import GzipFile +from obitools.zipfile import ZipFile +import os.path + + +class FileFormatError(Exception): + pass + + + +def universalOpen(file,*options): + ''' + Open a file gziped or not. + + If file is a C{str} instance, file is + concidered as a file name. In this case + the C{.gz} suffixe is tested to eventually + open it a a gziped file. + + If file is an other kind of object, it is assumed + that this object follow the C{file} interface + and it is return as is. + + @param file: the file to open + @type file: C{str} or a file like object + + @return: an iterator on text lines. + ''' + if isinstance(file,str): + if urllib2.urlparse.urlparse(file)[0]=='': + rep = open(file,*options) + else: + rep = urllib2.urlopen(file,timeout=15) + + if file[-3:] == '.gz': + rep = GzipFile(fileobj=rep) + if file[-4:] == '.zip': + zip = ZipFile(file=rep) + data = zip.infolist() + assert len(data)==1,'Only zipped file containning a single file can be open' + name = data[0].filename + rep = zip.open(name) + else: + rep = file + return rep + +def universalTell(file): + ''' + Return the position in the file even if + it is a gziped one. + + @param file: the file to check + @type file: a C{file} like instance + + @return: position in the file + @rtype: C{int} + ''' + if isinstance(file, GzipFile): + file=file.myfileobj + return file.tell() + +def fileSize(file): + ''' + Return the file size even if it is a + gziped one. + + @param file: the file to check + @type file: a C{file} like instance + + @return: the size of the file + @rtype: C{int} + ''' + if isinstance(file, GzipFile): + file=file.myfileobj + pos = file.tell() + file.seek(0,2) + length = file.tell() + file.seek(pos,0) + return length + +def progressBar(pos,maxi,reset=False,head='',delta=[],step=[1,0,0]): + if reset: + del delta[:] + if not delta: + delta.append(time.time()) + delta.append(time.time()) + assert maxi>0 + + step[1]+=1 + if step[1] % step[0] == 0: + step[1]=1 + newtime = time.time() + d = newtime-delta[1] + if d < 0.2: + step[0]*=2 + elif d > 0.4 and step[0]>1: + step[0]/=2 + + delta[1]=newtime + elapsed = delta[1]-delta[0] + + if callable(pos): + pos=pos() + percent = float(pos)/maxi * 100 + remain = time.gmtime(elapsed / percent * (100-percent)) + days = remain.tm_yday - 1 + hour = remain.tm_hour + minu = remain.tm_min + sec = remain.tm_sec + if days: + remain = "%d days %02d:%02d:%02d" % (days,hour,minu,sec) + else: + remain = "%02d:%02d:%02d" % (hour,minu,sec) + bar = '#' * int(percent/2) + step[2]=(step[2]+1) % 4 + bar+= '|/-\\'[step[2]] + bar+= ' ' * (50 - int(percent/2)) + sys.stderr.write('\r%s %5.1f %% |%s] remain : %s' %(head,percent,bar,remain)) + else: + step[1]+=1 + +def endLessIterator(endedlist): + for x in endedlist: + yield x + while(1): + yield endedlist[-1] + + +def multiLineWrapper(lineiterator): + ''' + Aggregator of strings. + + @param lineiterator: a stream of strings from an opened OBO file. + @type lineiterator: a stream of strings. + + @return: an aggregated stanza. + @rtype: an iterotor on str + + @note: The aggregator aggregates strings from an opened OBO file. + When the length of a string is < 2, the current stanza is over. + ''' + + for line in lineiterator: + rep = [line] + while len(line)>=2 and line[-2]=='\\': + rep[-1]=rep[-1][0:-2] + try: + line = lineiterator.next() + except StopIteration: + raise FileFormatError + rep.append(line) + yield ''.join(rep) + + +def skipWhiteLineIterator(lineiterator): + ''' + Curator of stanza. + + @param lineiterator: a stream of strings from an opened OBO file. + @type lineiterator: a stream of strings. + + @return: a stream of strings without blank strings. + @rtype: a stream strings + + @note: The curator skip white lines of the current stanza. + ''' + + for line in lineiterator: + cleanline = line.strip() + if cleanline: + yield line + else: + print 'skipped' + + +class ColumnFile(object): + + def __init__(self,stream,sep=None,strip=True, + types=None,skip=None,head=None, + extra=None, + extraformat='([a-zA-Z]\w*) *= *([^;]+);'): + self._stream = universalOpen(stream) + self._delimiter=sep + self._strip=strip + self._extra=extra + self._extraformat = re.compile(extraformat) + + if types: + self._types=[x for x in types] + for i in xrange(len(self._types)): + if self._types[i] is bool: + self._types[i]=ColumnFile.str2bool + else: + self._types=None + + self._skip = skip + if skip is not None: + self._lskip= len(skip) + else: + self._lskip= 0 + self._head=head + + def str2bool(x): + return bool(eval(x.strip()[0].upper(),{'T':True,'V':True,'F':False})) + + str2bool = staticmethod(str2bool) + + + def __iter__(self): + return self + + def next(self): + + def cast(txt,type): + try: + v = type(txt) + except: + v=None + return v + ligne = self._stream.next() + if self._skip is not None: + while ligne[0:self._lskip]==self._skip: + ligne = self._stream.next() + if self._extra is not None: + try: + (ligne,extra) = ligne.rsplit(self._extra,1) + extra = dict(self._extraformat.findall(extra)) + except ValueError: + extra=None + else: + extra = None + data = ligne.split(self._delimiter) + if self._strip or self._types: + data = [x.strip() for x in data] + if self._types: + it = endLessIterator(self._types) + data = [cast(*x) for x in ((y,it.next()) for y in data)] + if self._head is not None: + data=dict(map(None, self._head,data)) + if extra is not None: + data['__extra__']=extra + else: + if extra is not None: + data.append(extra) + return data + + def tell(self): + return universalTell(self._stream) + + +class CachedDB(object): + + def __init__(self,cachefile,masterdb): + self._cache = shelve.open(cachefile,'c') + self._db = masterdb + self._lock=Lock() + + def _cacheSeq(self,seq): + self._lock.acquire() + self._cache[seq.id]=seq + self._lock.release() + return seq + + def __getitem__(self,ac): + if isinstance(ac,str): + self._lock.acquire() + if ac in self._cache: +# print >>sys.stderr,"Use cache for %s" % ac + data = self._cache[ac] + self._lock.release() + + else: + self._lock.release() + data = self._db[ac] + self._cacheSeq(data) + return data + else: + self._lock.acquire() + acs = [[x,self._cache.get(x,None)] for x in ac] + self._lock.release() + newacs = [ac for ac,cached in acs if cached is None] + if newacs: + newseqs = self._db[newacs] + else: + newseqs = iter([]) + for r in acs: + if r[1] is None: + r[1]=self._cacheSeq(newseqs.next()) +# else: +# print >>sys.stderr,"Use cache for %s" % r[0] + return (x[1] for x in acs) + + +def moduleInDevelopment(name): + Warning('This module %s is under development : use it with caution' % name) + + +def deprecatedScript(newscript): + current = sys.argv[0] + print >>sys.stderr," " + print >>sys.stderr," " + print >>sys.stderr," " + print >>sys.stderr,"#########################################################" + print >>sys.stderr,"# #" + print >>sys.stderr," W A R N I N G :" + print >>sys.stderr," %s is a deprecated script " % os.path.split(current)[1] + print >>sys.stderr," it will disappear in the next obitools version" + print >>sys.stderr," " + print >>sys.stderr," The new corresponding command is %s " % newscript + print >>sys.stderr,"# #" + print >>sys.stderr,"#########################################################" + print >>sys.stderr," " + print >>sys.stderr," " + print >>sys.stderr," " diff --git a/obitools/utils/__init__.pyc b/obitools/utils/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99512dc99cbbe2b0f84605a65166e36b2b6a308e GIT binary patch literal 12369 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 af$EFZ@#@s`FI9)C&sC>qzBco6wer8_Q0u|~ literal 0 HcmV?d00001 diff --git a/obitools/utils/bioseq.py b/obitools/utils/bioseq.py new file mode 100644 index 0000000..71337c7 --- /dev/null +++ b/obitools/utils/bioseq.py @@ -0,0 +1,232 @@ +def mergeTaxonomyClassification(uniqSeq,taxonomy): + for seq in uniqSeq: + if seq['merged_taxid']: + seq['taxid']=taxonomy.lastCommonTaxon(*seq['merged_taxid'].keys()) + tsp = taxonomy.getSpecies(seq['taxid']) + tgn = taxonomy.getGenus(seq['taxid']) + tfa = taxonomy.getFamily(seq['taxid']) + + if tsp is not None: + sp_sn = taxonomy.getScientificName(tsp) + else: + sp_sn="###" + tsp=-1 + + if tgn is not None: + gn_sn = taxonomy.getScientificName(tgn) + else: + gn_sn="###" + tgn=-1 + + if tfa is not None: + fa_sn = taxonomy.getScientificName(tfa) + else: + fa_sn="###" + tfa=-1 + + seq['species']=tsp + seq['genus']=tgn + seq['family']=tfa + + seq['species_sn']=sp_sn + seq['genus_sn']=gn_sn + seq['family_sn']=fa_sn + + seq['rank']=taxonomy.getRank(seq['taxid']) + seq['scientific_name']=fa_sn = taxonomy.getScientificName(seq['taxid']) + +def uniqSequence(seqIterator,taxonomy=None,mergedKey=None,mergeIds=False,categories=None): + uniques={} + uniqSeq=[] + + if categories is None: + categories=[] + + if mergedKey is not None: + mergedKey=set(mergedKey) + else: + mergedKey=set() + + if taxonomy is not None: + mergedKey.add('taxid') + + for seq in seqIterator: + s = tuple(seq[x] for x in categories) + (str(seq),) + if s in uniques: + s = uniques[s] + if 'count' in seq: + s['count']+=seq['count'] + else: + s['count']+=1 +# if taxonomy is not None and 'taxid' in seq: +# s['merged_taxid'][seq['taxid']]= + for key in mergedKey: + if key=='taxid' and mergeIds: + if 'taxid_dist' in seq: + s["taxid_dist"].update(seq["taxid_dist"]) + if 'taxid' in seq: + s["taxid_dist"][seq.id]=seq['taxid'] + + mkey = "merged_%s" % key + if key in seq: + s[mkey][seq[key]]=s[mkey].get(seq[key],0)+1 + if mkey in seq: + for skey in seq[mkey]: + if skey in s: + s[mkey][skey]=s[mkey].get(seq[skey],0)+seq[mkey][skey] + else: + s[mkey][skey]=seq[mkey][skey] + + for key in seq.iterkeys(): + # Merger proprement l'attribut merged s'il exist + if key in s and s[key]!=seq[key] and key!='count' and key[0:7]!='merged_' and key!='merged': + del(s[key]) + + + if mergeIds: + s['merged'].append(seq.id) + else: + uniques[s]=seq + for key in mergedKey: + if key=='taxid' and mergeIds: + if 'taxid_dist' not in seq: + seq["taxid_dist"]={} + if 'taxid' in seq: + seq["taxid_dist"][seq.id]=seq['taxid'] + mkey = "merged_%s" % key + if mkey not in seq: + seq[mkey]={} + if key in seq: + seq[mkey][seq[key]]=seq[mkey].get(seq[key],0)+1 + del(seq[key]) + + if 'count' not in seq: + seq['count']=1 + if mergeIds: + seq['merged']=[seq.id] + uniqSeq.append(seq) + + if taxonomy is not None: + mergeTaxonomyClassification(uniqSeq, taxonomy) + + + + return uniqSeq + +def uniqPrefixSequence(seqIterator,taxonomy=None,mergedKey=None,mergeIds=False,categories=None): + + if categories is None: + categories=[] + + def cmpseq(s1,s2): + return cmp(str(s1),str(s2)) + + if mergedKey is not None: + mergedKey=set(mergedKey) + else: + mergedKey=set() + + if taxonomy is not None: + mergedKey.add('taxid') + + sequences=list(seqIterator) + + if not sequences: + return [] + + sequences.sort(cmpseq) + + + old=sequences.pop() + uniqSeq=[old] + if 'count' not in old: + old['count']=1 + for key in mergedKey: + mkey = "merged_%s" % key + if mkey not in old: + old[mkey]={} + if key in old: + old[mkey][old[key]]=old[mkey].get(old[key],0)+1 + if mergeIds: + old['merged']=[old.id] + + + while(sequences): + seq=sequences.pop() + lseq=len(seq) + pold = str(old)[0:lseq] + if pold==str(seq): + + if 'count' in seq: + old['count']+=seq['count'] + else: + old['count']+=1 + + for key in mergedKey: + mkey = "merged_%s" % key + if key in seq: + old[mkey][seq[key]]=old[mkey].get(seq[key],0)+1 + if mkey in seq: + for skey in seq[mkey]: + if skey in old: + old[mkey][skey]=old[mkey].get(seq[skey],0)+seq[mkey][skey] + else: + old[mkey][skey]=seq[mkey][skey] + + for key in seq.iterkeys(): + if key in old and old[key]!=seq[key]: + del(old[key]) + + + if mergeIds: + old['merged'].append(seq.id) + else: + old=seq + + for key in mergedKey: + mkey = "merged_%s" % key + if mkey not in seq: + seq[mkey]={} + if key in seq: + seq[mkey][seq[key]]=seq[mkey].get(seq[key],0)+1 + del(seq[key]) + + if 'count' not in seq: + seq['count']=1 + if mergeIds: + seq['merged']=[seq.id] + uniqSeq.append(seq) + + if taxonomy is not None: + mergeTaxonomyClassification(uniqSeq, taxonomy) + + return uniqSeq + + + + +def _cmpOnKeyGenerator(key,reverse=False): + def compare(x,y): + try: + c1 = x[key] + except KeyError: + c1=None + + try: + c2 = y[key] + except KeyError: + c2=None + + if reverse: + s=c1 + c1=c2 + c2=s + return cmp(c1,c2) + + return compare + +def sortSequence(seqIterator,key,reverse=False): + seqs = list(seqIterator) + seqs.sort(_cmpOnKeyGenerator(key, reverse)) + return seqs + \ No newline at end of file diff --git a/obitools/utils/crc64.py b/obitools/utils/crc64.py new file mode 100644 index 0000000..537391e --- /dev/null +++ b/obitools/utils/crc64.py @@ -0,0 +1,53 @@ +# +# Code obtained from : +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/259177/index_txt +# + +# Initialisation +# 32 first bits of generator polynomial for CRC64 +# the 32 lower bits are assumed to be zero + +POLY64REVh = 0xd8000000L +CRCTableh = [0] * 256 +CRCTablel = [0] * 256 +isInitialized = False + +def CRC64(aString): + global isInitialized + crcl = 0 + crch = 0 + if (isInitialized is not True): + isInitialized = True + for i in xrange(256): + partl = i + parth = 0L + for j in xrange(8): + rflag = partl & 1L + partl >>= 1L + if (parth & 1): + partl |= (1L << 31L) + parth >>= 1L + if rflag: + parth ^= POLY64REVh + CRCTableh[i] = parth; + CRCTablel[i] = partl; + + for item in aString: + shr = 0L + shr = (crch & 0xFF) << 24 + temp1h = crch >> 8L + temp1l = (crcl >> 8L) | shr + tableindex = (crcl ^ ord(item)) & 0xFF + + crch = temp1h ^ CRCTableh[tableindex] + crcl = temp1l ^ CRCTablel[tableindex] + return (crch, crcl) + +def CRC64digest(aString): + return "%08X%08X" % (CRC64(aString)) + +if __name__ == '__main__': + assert CRC64("IHATEMATH") == (3822890454, 2600578513) + assert CRC64digest("IHATEMATH") == "E3DCADD69B01ADD1" + print 'CRC64: dumb test successful' + diff --git a/obitools/utils/iterator.py b/obitools/utils/iterator.py new file mode 100644 index 0000000..f53537f --- /dev/null +++ b/obitools/utils/iterator.py @@ -0,0 +1,8 @@ +from itertools import chain + +def uniqueChain(*args): + see = set() + for x in chain(*args): + if x not in see: + see.add(x) + yield x \ No newline at end of file diff --git a/obitools/utils/iterator.pyc b/obitools/utils/iterator.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88d415e44a517c90a1ca66e228c44c7d68fc0a70 GIT binary patch literal 565 zcmcgq%Sr<=6g`>xYPBE;-MMx)ol!yEh=|sOn+mNDD7ctTLXDl7v`Gdlw5z)GKl~3r z#b59P^d>F%2Zno3lAAO4oP?=-*K40&AMF&&5zu^2X?6)X1w#hp3_S+M1H1vf0K9^3 zxIzs1LnCCgva1B?Qz8rBt3o@M14?s6V1YR>W0)MlGE9MYp!K+iS%4`8!*CtC;TF%n zGABVgxM$-AC?3d6*DJL{*VsBR!ggp{Y!c2FC(c-+2WCkTBnM1FNAzHvsrb$aZQ@+$ zREku@EtQtJD6ENJlpdNszHV6ZcD*@i>$HC{I#lCe4HW=(s5hE3)QcbP}t+ zw4PX6Npxw2POQ?==+$nLR$0h#DK6dPc-J*&g;m)O+hTRLyib*C?nB?m->S1lr9a#M W?|p;xPjkh~Oq{tlH16-%4t@bu%!8`{ literal 0 HcmV?d00001 diff --git a/obitools/word/__init__.py b/obitools/word/__init__.py new file mode 100644 index 0000000..c1a4b6b --- /dev/null +++ b/obitools/word/__init__.py @@ -0,0 +1,72 @@ +from itertools import imap +from _binary import * + +def wordCount(liste): + count = {} + + for e in liste: + count[e]=count.get(e,0) + 1 + + return count + + +def wordIterator(sequence,lword,step=1,endIncluded=False,circular=False): + + assert not (endIncluded and circular), \ + "endIncluded and circular cannot not be set to True at the same time" + + L = len(sequence) + sequence = str(sequence) + if circular: + sequence += sequence[0:lword] + pmax=L + elif endIncluded: + pmax=L + else: + pmax = L - lword + 1 + + pos = xrange(0,pmax,step) + + for x in pos: + yield encodeWord(sequence[x:x+lword]) + + + +def wordSelector(words,accept=None,reject=None): + ''' + Filter over a DNA word iterator. + + @param words: an iterable object other a list of DNA words + @type words: an iterator + @param accept: a list of predicate. Each predicate is a function + accepting one str parametter and returning a boolean + value. + @type accept: list + @param reject: a list of predicat. Each predicat is a function + accepting one str parametter and returning a boolean + value. + @type reject: list + + @return: an iterator on DNA word (str) + @rtype: iterator + ''' + if accept is None: + accept=[] + if reject is None: + reject=[] + for w in words: +# print [bool(p(w)) for p in accept] + accepted = reduce(lambda x,y: bool(x) and bool(y), + (p(w) for p in accept), + True) +# print [(p.__name__,bool(p(w))) for p in reject] + rejected = reduce(lambda x,y:bool(x) or bool(y), + (p(w) for p in reject), + False) +# print decodeWord(w,5),accepted,rejected, + if accepted and not rejected: +# print " conserved" + yield w +# else: +# print + diff --git a/obitools/word/_binary.so b/obitools/word/_binary.so new file mode 100755 index 0000000000000000000000000000000000000000..1780762b80649036e0046e3954e7d532783df8eb GIT binary patch literal 150000 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 z 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 dmin + return predicate + +def predicateHomoPolymerLarger(count,size): + def predicate(w): + return homoMax(w, size) > count + return predicate + +def predicateHomoPolymerSmaller(count,size): + def predicate(w): + return homoMax(w, size) < count + return predicate + +def predicateGCUpperBond(count,size): + def predicate(w): + return countCG(w, size) > count + return predicate + +def predicateMatchPattern(pattern,size): + pattern=encodePattern(pattern) + def predicate(w): + return matchPattern(w, pattern) + return predicate + + + diff --git a/obitools/zipfile.py b/obitools/zipfile.py new file mode 100644 index 0000000..41e4bcb --- /dev/null +++ b/obitools/zipfile.py @@ -0,0 +1,1282 @@ +""" +Read and write ZIP files. +""" +import struct, os, time, sys, shutil +import binascii, cStringIO + +try: + import zlib # We may need its compression method + crc32 = zlib.crc32 +except ImportError: + zlib = None + crc32 = binascii.crc32 + +__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile", + "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ] + +class BadZipfile(Exception): + pass + + +class LargeZipFile(Exception): + """ + Raised when writing a zipfile, the zipfile requires ZIP64 extensions + and those extensions are disabled. + """ + +error = BadZipfile # The exception raised by this module + +ZIP64_LIMIT= (1 << 31) - 1 + +# constants for Zip file compression methods +ZIP_STORED = 0 +ZIP_DEFLATED = 8 +# Other ZIP compression methods not supported + +# Here are some struct module formats for reading headers +structEndArchive = "<4s4H2LH" # 9 items, end of archive, 22 bytes +stringEndArchive = "PK\005\006" # magic number for end of archive record +structCentralDir = "<4s4B4HLLL5HLL"# 19 items, central directory, 46 bytes +stringCentralDir = "PK\001\002" # magic number for central directory +structFileHeader = "<4s2B4HLLL2H" # 12 items, file header record, 30 bytes +stringFileHeader = "PK\003\004" # magic number for file header +structEndArchive64Locator = "<4sLQL" # 4 items, locate Zip64 header, 20 bytes +stringEndArchive64Locator = "PK\x06\x07" # magic token for locator header +structEndArchive64 = "<4sQHHLLQQQQ" # 10 items, end of archive (Zip64), 56 bytes +stringEndArchive64 = "PK\x06\x06" # magic token for Zip64 header + + +# indexes of entries in the central directory structure +_CD_SIGNATURE = 0 +_CD_CREATE_VERSION = 1 +_CD_CREATE_SYSTEM = 2 +_CD_EXTRACT_VERSION = 3 +_CD_EXTRACT_SYSTEM = 4 # is this meaningful? +_CD_FLAG_BITS = 5 +_CD_COMPRESS_TYPE = 6 +_CD_TIME = 7 +_CD_DATE = 8 +_CD_CRC = 9 +_CD_COMPRESSED_SIZE = 10 +_CD_UNCOMPRESSED_SIZE = 11 +_CD_FILENAME_LENGTH = 12 +_CD_EXTRA_FIELD_LENGTH = 13 +_CD_COMMENT_LENGTH = 14 +_CD_DISK_NUMBER_START = 15 +_CD_INTERNAL_FILE_ATTRIBUTES = 16 +_CD_EXTERNAL_FILE_ATTRIBUTES = 17 +_CD_LOCAL_HEADER_OFFSET = 18 + +# indexes of entries in the local file header structure +_FH_SIGNATURE = 0 +_FH_EXTRACT_VERSION = 1 +_FH_EXTRACT_SYSTEM = 2 # is this meaningful? +_FH_GENERAL_PURPOSE_FLAG_BITS = 3 +_FH_COMPRESSION_METHOD = 4 +_FH_LAST_MOD_TIME = 5 +_FH_LAST_MOD_DATE = 6 +_FH_CRC = 7 +_FH_COMPRESSED_SIZE = 8 +_FH_UNCOMPRESSED_SIZE = 9 +_FH_FILENAME_LENGTH = 10 +_FH_EXTRA_FIELD_LENGTH = 11 + +def is_zipfile(filename): + """Quickly see if file is a ZIP file by checking the magic number.""" + try: + fpin = open(filename, "rb") + endrec = _EndRecData(fpin) + fpin.close() + if endrec: + return True # file has correct magic number + except IOError: + pass + return False + +def _EndRecData64(fpin, offset, endrec): + """ + Read the ZIP64 end-of-archive records and use that to update endrec + """ + locatorSize = struct.calcsize(structEndArchive64Locator) + fpin.seek(offset - locatorSize, 2) + data = fpin.read(locatorSize) + sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data) + if sig != stringEndArchive64Locator: + return endrec + + if diskno != 0 or disks != 1: + raise BadZipfile("zipfiles that span multiple disks are not supported") + + # Assume no 'zip64 extensible data' + endArchiveSize = struct.calcsize(structEndArchive64) + fpin.seek(offset - locatorSize - endArchiveSize, 2) + data = fpin.read(endArchiveSize) + sig, sz, create_version, read_version, disk_num, disk_dir, \ + dircount, dircount2, dirsize, diroffset = \ + struct.unpack(structEndArchive64, data) + if sig != stringEndArchive64: + return endrec + + # Update the original endrec using data from the ZIP64 record + endrec[1] = disk_num + endrec[2] = disk_dir + endrec[3] = dircount + endrec[4] = dircount2 + endrec[5] = dirsize + endrec[6] = diroffset + return endrec + + +def _EndRecData(fpin): + """Return data from the "End of Central Directory" record, or None. + + The data is a list of the nine items in the ZIP "End of central dir" + record followed by a tenth item, the file seek offset of this record.""" + fpin.seek(-22, 2) # Assume no archive comment. + filesize = fpin.tell() + 22 # Get file size + data = fpin.read() + if data[0:4] == stringEndArchive and data[-2:] == "\000\000": + endrec = struct.unpack(structEndArchive, data) + endrec = list(endrec) + endrec.append("") # Append the archive comment + endrec.append(filesize - 22) # Append the record start offset + if endrec[-4] == 0xffffffff: + return _EndRecData64(fpin, -22, endrec) + return endrec + # Search the last END_BLOCK bytes of the file for the record signature. + # The comment is appended to the ZIP file and has a 16 bit length. + # So the comment may be up to 64K long. We limit the search for the + # signature to a few Kbytes at the end of the file for efficiency. + # also, the signature must not appear in the comment. + END_BLOCK = min(filesize, 1024 * 4) + fpin.seek(filesize - END_BLOCK, 0) + data = fpin.read() + start = data.rfind(stringEndArchive) + if start >= 0: # Correct signature string was found + endrec = struct.unpack(structEndArchive, data[start:start+22]) + endrec = list(endrec) + comment = data[start+22:] + if endrec[7] == len(comment): # Comment length checks out + # Append the archive comment and start offset + endrec.append(comment) + endrec.append(filesize - END_BLOCK + start) + if endrec[-4] == 0xffffffff: + return _EndRecData64(fpin, - END_BLOCK + start, endrec) + return endrec + return # Error, return None + + +class ZipInfo (object): + """Class with attributes describing each file in the ZIP archive.""" + + __slots__ = ( + 'orig_filename', + 'filename', + 'date_time', + 'compress_type', + 'comment', + 'extra', + 'create_system', + 'create_version', + 'extract_version', + 'reserved', + 'flag_bits', + 'volume', + 'internal_attr', + 'external_attr', + 'header_offset', + 'CRC', + 'compress_size', + 'file_size', + '_raw_time', + ) + + def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): + self.orig_filename = filename # Original file name in archive + + # Terminate the file name at the first null byte. Null bytes in file + # names are used as tricks by viruses in archives. + null_byte = filename.find(chr(0)) + if null_byte >= 0: + filename = filename[0:null_byte] + # This is used to ensure paths in generated ZIP files always use + # forward slashes as the directory separator, as required by the + # ZIP format specification. + if os.sep != "/" and os.sep in filename: + filename = filename.replace(os.sep, "/") + + self.filename = filename # Normalized file name + self.date_time = date_time # year, month, day, hour, min, sec + # Standard values: + self.compress_type = ZIP_STORED # Type of compression for the file + self.comment = "" # Comment for each file + self.extra = "" # ZIP extra data + if sys.platform == 'win32': + self.create_system = 0 # System which created ZIP archive + else: + # Assume everything else is unix-y + self.create_system = 3 # System which created ZIP archive + self.create_version = 20 # Version which created ZIP archive + self.extract_version = 20 # Version needed to extract archive + self.reserved = 0 # Must be zero + self.flag_bits = 0 # ZIP flag bits + self.volume = 0 # Volume number of file header + self.internal_attr = 0 # Internal attributes + self.external_attr = 0 # External file attributes + # Other attributes are set by class ZipFile: + # header_offset Byte offset to the file header + # CRC CRC-32 of the uncompressed file + # compress_size Size of the compressed file + # file_size Size of the uncompressed file + + def FileHeader(self): + """Return the per-file header as a string.""" + dt = self.date_time + dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] + dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) + if self.flag_bits & 0x08: + # Set these to zero because we write them after the file data + CRC = compress_size = file_size = 0 + else: + CRC = self.CRC + compress_size = self.compress_size + file_size = self.file_size + + extra = self.extra + + if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT: + # File is larger than what fits into a 4 byte integer, + # fall back to the ZIP64 extension + fmt = '= 24: + counts = unpack('> 1) & 0x7FFFFFFF) ^ poly + else: + crc = ((crc >> 1) & 0x7FFFFFFF) + table[i] = crc + return table + crctable = _GenerateCRCTable() + + def _crc32(self, ch, crc): + """Compute the CRC32 primitive on one byte.""" + return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff] + + def __init__(self, pwd): + self.key0 = 305419896 + self.key1 = 591751049 + self.key2 = 878082192 + for p in pwd: + self._UpdateKeys(p) + + def _UpdateKeys(self, c): + self.key0 = self._crc32(c, self.key0) + self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295 + self.key1 = (self.key1 * 134775813 + 1) & 4294967295 + self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2) + + def __call__(self, c): + """Decrypt a single character.""" + c = ord(c) + k = self.key2 | 2 + c = c ^ (((k * (k^1)) >> 8) & 255) + c = chr(c) + self._UpdateKeys(c) + return c + +class ZipExtFile: + """File-like object for reading an archive member. + Is returned by ZipFile.open(). + """ + + def __init__(self, fileobj, zipinfo, decrypt=None): + self.fileobj = fileobj + self.decrypter = decrypt + self.bytes_read = 0L + self.rawbuffer = '' + self.readbuffer = '' + self.linebuffer = '' + self.eof = False + self.univ_newlines = False + self.nlSeps = ("\n", ) + self.lastdiscard = '' + + self.compress_type = zipinfo.compress_type + self.compress_size = zipinfo.compress_size + + self.closed = False + self.mode = "r" + self.name = zipinfo.filename + + # read from compressed files in 64k blocks + self.compreadsize = 64*1024 + if self.compress_type == ZIP_DEFLATED: + self.dc = zlib.decompressobj(-15) + + def set_univ_newlines(self, univ_newlines): + self.univ_newlines = univ_newlines + + # pick line separator char(s) based on universal newlines flag + self.nlSeps = ("\n", ) + if self.univ_newlines: + self.nlSeps = ("\r\n", "\r", "\n") + + def __iter__(self): + return self + + def next(self): + nextline = self.readline() + if not nextline: + raise StopIteration() + + return nextline + + def close(self): + self.closed = True + + def _checkfornewline(self): + nl, nllen = -1, -1 + if self.linebuffer: + # ugly check for cases where half of an \r\n pair was + # read on the last pass, and the \r was discarded. In this + # case we just throw away the \n at the start of the buffer. + if (self.lastdiscard, self.linebuffer[0]) == ('\r','\n'): + self.linebuffer = self.linebuffer[1:] + + for sep in self.nlSeps: + nl = self.linebuffer.find(sep) + if nl >= 0: + nllen = len(sep) + return nl, nllen + + return nl, nllen + + def readline(self, size = -1): + """Read a line with approx. size. If size is negative, + read a whole line. + """ + if size < 0: + size = sys.maxint + elif size == 0: + return '' + + # check for a newline already in buffer + nl, nllen = self._checkfornewline() + + if nl >= 0: + # the next line was already in the buffer + nl = min(nl, size) + else: + # no line break in buffer - try to read more + size -= len(self.linebuffer) + while nl < 0 and size > 0: + buf = self.read(min(size, 100)) + if not buf: + break + self.linebuffer += buf + size -= len(buf) + + # check for a newline in buffer + nl, nllen = self._checkfornewline() + + # we either ran out of bytes in the file, or + # met the specified size limit without finding a newline, + # so return current buffer + if nl < 0: + s = self.linebuffer + self.linebuffer = '' + return s + + buf = self.linebuffer[:nl] + self.lastdiscard = self.linebuffer[nl:nl + nllen] + self.linebuffer = self.linebuffer[nl + nllen:] + + # line is always returned with \n as newline char (except possibly + # for a final incomplete line in the file, which is handled above). + return buf + "\n" + + def readlines(self, sizehint = -1): + """Return a list with all (following) lines. The sizehint parameter + is ignored in this implementation. + """ + result = [] + while True: + line = self.readline() + if not line: break + result.append(line) + return result + + def read(self, size = None): + # act like file() obj and return empty string if size is 0 + if size == 0: + return '' + + # determine read size + bytesToRead = self.compress_size - self.bytes_read + + # adjust read size for encrypted files since the first 12 bytes + # are for the encryption/password information + if self.decrypter is not None: + bytesToRead -= 12 + + if size is not None and size >= 0: + if self.compress_type == ZIP_STORED: + lr = len(self.readbuffer) + bytesToRead = min(bytesToRead, size - lr) + elif self.compress_type == ZIP_DEFLATED: + if len(self.readbuffer) > size: + # the user has requested fewer bytes than we've already + # pulled through the decompressor; don't read any more + bytesToRead = 0 + else: + # user will use up the buffer, so read some more + lr = len(self.rawbuffer) + bytesToRead = min(bytesToRead, self.compreadsize - lr) + + # avoid reading past end of file contents + if bytesToRead + self.bytes_read > self.compress_size: + bytesToRead = self.compress_size - self.bytes_read + + # try to read from file (if necessary) + if bytesToRead > 0: + bytes = self.fileobj.read(bytesToRead) + self.bytes_read += len(bytes) + self.rawbuffer += bytes + + # handle contents of raw buffer + if self.rawbuffer: + newdata = self.rawbuffer + self.rawbuffer = '' + + # decrypt new data if we were given an object to handle that + if newdata and self.decrypter is not None: + newdata = ''.join(map(self.decrypter, newdata)) + + # decompress newly read data if necessary + if newdata and self.compress_type == ZIP_DEFLATED: + newdata = self.dc.decompress(newdata) + self.rawbuffer = self.dc.unconsumed_tail + if self.eof and len(self.rawbuffer) == 0: + # we're out of raw bytes (both from the file and + # the local buffer); flush just to make sure the + # decompressor is done + newdata += self.dc.flush() + # prevent decompressor from being used again + self.dc = None + + self.readbuffer += newdata + + + # return what the user asked for + if size is None or len(self.readbuffer) <= size: + bytes = self.readbuffer + self.readbuffer = '' + else: + bytes = self.readbuffer[:size] + self.readbuffer = self.readbuffer[size:] + + return bytes + + +class ZipFile: + """ Class with methods to open, read, write, close, list zip files. + + z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True) + + @var file: Either the path to the file, or a file-like object. + If it is a path, the file will be opened and closed by ZipFile. + @var mode: The mode can be either read "r", write "w" or append "a". + @var compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib). + @var allowZip64: if True ZipFile will create files with ZIP64 extensions when + needed, otherwise it will raise an exception when this would + be necessary. + + """ + + fp = None # Set here since __del__ checks it + + def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False): + """Open the ZIP file with mode read "r", write "w" or append "a".""" + if mode not in ("r", "w", "a"): + raise RuntimeError('ZipFile() requires mode "r", "w", or "a"') + + if compression == ZIP_STORED: + pass + elif compression == ZIP_DEFLATED: + if not zlib: + raise RuntimeError,\ + "Compression requires the (missing) zlib module" + else: + raise RuntimeError, "That compression method is not supported" + + self._allowZip64 = allowZip64 + self._didModify = False + self.debug = 0 # Level of printing: 0 through 3 + self.NameToInfo = {} # Find file info given name + self.filelist = [] # List of ZipInfo instances for archive + self.compression = compression # Method of compression + self.mode = key = mode.replace('b', '')[0] + self.pwd = None + + # Check if we were passed a file-like object + if isinstance(file, basestring): + self._filePassed = 0 + self.filename = file + modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'} + try: + self.fp = open(file, modeDict[mode]) + except IOError: + if mode == 'a': + mode = key = 'w' + self.fp = open(file, modeDict[mode]) + else: + raise + else: + self._filePassed = 1 + self.fp = file + self.filename = getattr(file, 'name', None) + + if key == 'r': + self._GetContents() + elif key == 'w': + pass + elif key == 'a': + try: # See if file is a zip file + self._RealGetContents() + # seek to start of directory and overwrite + self.fp.seek(self.start_dir, 0) + except BadZipfile: # file is not a zip file, just append + self.fp.seek(0, 2) + else: + if not self._filePassed: + self.fp.close() + self.fp = None + raise RuntimeError, 'Mode must be "r", "w" or "a"' + + def _GetContents(self): + """Read the directory, making sure we close the file if the format + is bad.""" + try: + self._RealGetContents() + except BadZipfile: + if not self._filePassed: + self.fp.close() + self.fp = None + raise + + def _RealGetContents(self): + """Read in the table of contents for the ZIP file.""" + fp = self.fp + endrec = _EndRecData(fp) + if not endrec: + raise BadZipfile, "File is not a zip file" + if self.debug > 1: + print endrec + size_cd = endrec[5] # bytes in central directory + offset_cd = endrec[6] # offset of central directory + self.comment = endrec[8] # archive comment + # endrec[9] is the offset of the "End of Central Dir" record + if endrec[9] > ZIP64_LIMIT: + x = endrec[9] - size_cd - 56 - 20 + else: + x = endrec[9] - size_cd + # "concat" is zero, unless zip was concatenated to another file + concat = x - offset_cd + if self.debug > 2: + print "given, inferred, offset", offset_cd, x, concat + # self.start_dir: Position of start of central directory + self.start_dir = offset_cd + concat + fp.seek(self.start_dir, 0) + data = fp.read(size_cd) + fp = cStringIO.StringIO(data) + total = 0 + while total < size_cd: + centdir = fp.read(46) + total = total + 46 + if centdir[0:4] != stringCentralDir: + raise BadZipfile, "Bad magic number for central directory" + centdir = struct.unpack(structCentralDir, centdir) + if self.debug > 2: + print centdir + filename = fp.read(centdir[_CD_FILENAME_LENGTH]) + # Create ZipInfo instance to store file information + x = ZipInfo(filename) + x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) + x.comment = fp.read(centdir[_CD_COMMENT_LENGTH]) + total = (total + centdir[_CD_FILENAME_LENGTH] + + centdir[_CD_EXTRA_FIELD_LENGTH] + + centdir[_CD_COMMENT_LENGTH]) + x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET] + (x.create_version, x.create_system, x.extract_version, x.reserved, + x.flag_bits, x.compress_type, t, d, + x.CRC, x.compress_size, x.file_size) = centdir[1:12] + x.volume, x.internal_attr, x.external_attr = centdir[15:18] + # Convert date/time code to (year, month, day, hour, min, sec) + x._raw_time = t + x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F, + t>>11, (t>>5)&0x3F, (t&0x1F) * 2 ) + + x._decodeExtra() + x.header_offset = x.header_offset + concat + self.filelist.append(x) + self.NameToInfo[x.filename] = x + if self.debug > 2: + print "total", total + + + def namelist(self): + """Return a list of file names in the archive.""" + l = [] + for data in self.filelist: + l.append(data.filename) + return l + + def infolist(self): + """Return a list of class ZipInfo instances for files in the + archive.""" + return self.filelist + + def printdir(self): + """Print a table of contents for the zip file.""" + print "%-46s %19s %12s" % ("File Name", "Modified ", "Size") + for zinfo in self.filelist: + date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6] + print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size) + + def testzip(self): + """Read all the files and check the CRC.""" + for zinfo in self.filelist: + try: + self.read(zinfo.filename) # Check CRC-32 + except BadZipfile: + return zinfo.filename + + + def getinfo(self, name): + """Return the instance of ZipInfo given 'name'.""" + info = self.NameToInfo.get(name) + if info is None: + raise KeyError( + 'There is no item named %r in the archive' % name) + + return info + + def setpassword(self, pwd): + """Set default password for encrypted files.""" + self.pwd = pwd + + def read(self, name, pwd=None): + """Return file bytes (as a string) for name.""" + return self.open(name, "r", pwd).read() + + def open(self, name, mode="r", pwd=None): + """Return file-like object for 'name'.""" + if mode not in ("r", "U", "rU"): + raise RuntimeError, 'open() requires mode "r", "U", or "rU"' + if not self.fp: + raise RuntimeError, \ + "Attempt to read ZIP archive that was already closed" + + # Only open a new file for instances where we were not + # given a file object in the constructor + if self._filePassed: + zef_file = self.fp + else: + zef_file = open(self.filename, 'rb') + + # Get info object for name + zinfo = self.getinfo(name) + + filepos = zef_file.tell() + + zef_file.seek(zinfo.header_offset, 0) + + # Skip the file header: + fheader = zef_file.read(30) + if fheader[0:4] != stringFileHeader: + raise BadZipfile, "Bad magic number for file header" + + fheader = struct.unpack(structFileHeader, fheader) + fname = zef_file.read(fheader[_FH_FILENAME_LENGTH]) + if fheader[_FH_EXTRA_FIELD_LENGTH]: + zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH]) + + if fname != zinfo.orig_filename: + raise BadZipfile, \ + 'File name in directory "%s" and header "%s" differ.' % ( + zinfo.orig_filename, fname) + + # check for encrypted flag & handle password + is_encrypted = zinfo.flag_bits & 0x1 + zd = None + if is_encrypted: + if not pwd: + pwd = self.pwd + if not pwd: + raise RuntimeError, "File %s is encrypted, " \ + "password required for extraction" % name + + zd = _ZipDecrypter(pwd) + # The first 12 bytes in the cypher stream is an encryption header + # used to strengthen the algorithm. The first 11 bytes are + # completely random, while the 12th contains the MSB of the CRC, + # or the MSB of the file time depending on the header type + # and is used to check the correctness of the password. + bytes = zef_file.read(12) + h = map(zd, bytes[0:12]) + if zinfo.flag_bits & 0x8: + # compare against the file type from extended local headers + check_byte = (zinfo._raw_time >> 8) & 0xff + else: + # compare against the CRC otherwise + check_byte = (zinfo.CRC >> 24) & 0xff + if ord(h[11]) != check_byte: + raise RuntimeError("Bad password for file", name) + + # build and return a ZipExtFile + if zd is None: + zef = ZipExtFile(zef_file, zinfo) + else: + zef = ZipExtFile(zef_file, zinfo, zd) + + # set universal newlines on ZipExtFile if necessary + if "U" in mode: + zef.set_univ_newlines(True) + return zef + + def extract(self, member, path=None, pwd=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a ZipInfo object. You can + specify a different directory using `path'. + """ + if not isinstance(member, ZipInfo): + member = self.getinfo(member) + + if path is None: + path = os.getcwd() + + return self._extract_member(member, path, pwd) + + def extractall(self, path=None, members=None, pwd=None): + """Extract all members from the archive to the current working + directory. `path' specifies a different directory to extract to. + `members' is optional and must be a subset of the list returned + by namelist(). + """ + if members is None: + members = self.namelist() + + for zipinfo in members: + self.extract(zipinfo, path, pwd) + + def _extract_member(self, member, targetpath, pwd): + """Extract the ZipInfo object 'member' to a physical + file on the path targetpath. + """ + # build the destination pathname, replacing + # forward slashes to platform specific separators. + if targetpath[-1:] == "/": + targetpath = targetpath[:-1] + + # don't include leading "/" from file name if present + if os.path.isabs(member.filename): + targetpath = os.path.join(targetpath, member.filename[1:]) + else: + targetpath = os.path.join(targetpath, member.filename) + + targetpath = os.path.normpath(targetpath) + + # Create all upper directories if necessary. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + os.makedirs(upperdirs) + + source = self.open(member.filename, pwd=pwd) + target = file(targetpath, "wb") + shutil.copyfileobj(source, target) + source.close() + target.close() + + return targetpath + + def _writecheck(self, zinfo): + """Check for errors before writing a file to the archive.""" + if zinfo.filename in self.NameToInfo: + if self.debug: # Warning for duplicate names + print "Duplicate name:", zinfo.filename + if self.mode not in ("w", "a"): + raise RuntimeError, 'write() requires mode "w" or "a"' + if not self.fp: + raise RuntimeError, \ + "Attempt to write ZIP archive that was already closed" + if zinfo.compress_type == ZIP_DEFLATED and not zlib: + raise RuntimeError, \ + "Compression requires the (missing) zlib module" + if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED): + raise RuntimeError, \ + "That compression method is not supported" + if zinfo.file_size > ZIP64_LIMIT: + if not self._allowZip64: + raise LargeZipFile("Filesize would require ZIP64 extensions") + if zinfo.header_offset > ZIP64_LIMIT: + if not self._allowZip64: + raise LargeZipFile("Zipfile size would require ZIP64 extensions") + + def write(self, filename, arcname=None, compress_type=None): + """Put the bytes from filename into the archive under the name + arcname.""" + if not self.fp: + raise RuntimeError( + "Attempt to write to ZIP archive that was already closed") + + st = os.stat(filename) + mtime = time.localtime(st.st_mtime) + date_time = mtime[0:6] + # Create ZipInfo instance to store file information + if arcname is None: + arcname = filename + arcname = os.path.normpath(os.path.splitdrive(arcname)[1]) + while arcname[0] in (os.sep, os.altsep): + arcname = arcname[1:] + zinfo = ZipInfo(arcname, date_time) + zinfo.external_attr = (st[0] & 0xFFFF) << 16L # Unix attributes + if compress_type is None: + zinfo.compress_type = self.compression + else: + zinfo.compress_type = compress_type + + zinfo.file_size = st.st_size + zinfo.flag_bits = 0x00 + zinfo.header_offset = self.fp.tell() # Start of header bytes + + self._writecheck(zinfo) + self._didModify = True + fp = open(filename, "rb") + # Must overwrite CRC and sizes with correct data later + zinfo.CRC = CRC = 0 + zinfo.compress_size = compress_size = 0 + zinfo.file_size = file_size = 0 + self.fp.write(zinfo.FileHeader()) + if zinfo.compress_type == ZIP_DEFLATED: + cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, + zlib.DEFLATED, -15) + else: + cmpr = None + while 1: + buf = fp.read(1024 * 8) + if not buf: + break + file_size = file_size + len(buf) + CRC = crc32(buf, CRC) & 0xffffffff + if cmpr: + buf = cmpr.compress(buf) + compress_size = compress_size + len(buf) + self.fp.write(buf) + fp.close() + if cmpr: + buf = cmpr.flush() + compress_size = compress_size + len(buf) + self.fp.write(buf) + zinfo.compress_size = compress_size + else: + zinfo.compress_size = file_size + zinfo.CRC = CRC + zinfo.file_size = file_size + # Seek backwards and write CRC and file sizes + position = self.fp.tell() # Preserve current position in file + self.fp.seek(zinfo.header_offset + 14, 0) + self.fp.write(struct.pack(" ZIP64_LIMIT \ + or zinfo.compress_size > ZIP64_LIMIT: + extra.append(zinfo.file_size) + extra.append(zinfo.compress_size) + file_size = 0xffffffff #-1 + compress_size = 0xffffffff #-1 + else: + file_size = zinfo.file_size + compress_size = zinfo.compress_size + + if zinfo.header_offset > ZIP64_LIMIT: + extra.append(zinfo.header_offset) + header_offset = 0xffffffffL # -1 32 bit + else: + header_offset = zinfo.header_offset + + extra_data = zinfo.extra + if extra: + # Append a ZIP64 field to the extra's + extra_data = struct.pack( + '>sys.stderr, (structCentralDir, + stringCentralDir, create_version, + zinfo.create_system, extract_version, zinfo.reserved, + zinfo.flag_bits, zinfo.compress_type, dostime, dosdate, + zinfo.CRC, compress_size, file_size, + len(zinfo.filename), len(extra_data), len(zinfo.comment), + 0, zinfo.internal_attr, zinfo.external_attr, + header_offset) + raise + self.fp.write(centdir) + self.fp.write(zinfo.filename) + self.fp.write(extra_data) + self.fp.write(zinfo.comment) + + pos2 = self.fp.tell() + # Write end-of-zip-archive record + if pos1 > ZIP64_LIMIT: + # Need to write the ZIP64 end-of-archive records + zip64endrec = struct.pack( + structEndArchive64, stringEndArchive64, + 44, 45, 45, 0, 0, count, count, pos2 - pos1, pos1) + self.fp.write(zip64endrec) + + zip64locrec = struct.pack( + structEndArchive64Locator, + stringEndArchive64Locator, 0, pos2, 1) + self.fp.write(zip64locrec) + + endrec = struct.pack(structEndArchive, stringEndArchive, + 0, 0, count, count, pos2 - pos1, 0xffffffffL, 0) + self.fp.write(endrec) + + else: + endrec = struct.pack(structEndArchive, stringEndArchive, + 0, 0, count, count, pos2 - pos1, pos1, 0) + self.fp.write(endrec) + self.fp.flush() + if not self._filePassed: + self.fp.close() + self.fp = None + + +class PyZipFile(ZipFile): + """Class to create ZIP archives with Python library files and packages.""" + + def writepy(self, pathname, basename = ""): + """Add all files from "pathname" to the ZIP archive. + + If pathname is a package directory, search the directory and + all package subdirectories recursively for all *.py and enter + the modules into the archive. If pathname is a plain + directory, listdir *.py and enter all modules. Else, pathname + must be a Python *.py file and the module will be put into the + archive. Added modules are always module.pyo or module.pyc. + This method will compile the module.py into module.pyc if + necessary. + """ + dir, name = os.path.split(pathname) + if os.path.isdir(pathname): + initname = os.path.join(pathname, "__init__.py") + if os.path.isfile(initname): + # This is a package directory, add it + if basename: + basename = "%s/%s" % (basename, name) + else: + basename = name + if self.debug: + print "Adding package in", pathname, "as", basename + fname, arcname = self._get_codename(initname[0:-3], basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + dirlist = os.listdir(pathname) + dirlist.remove("__init__.py") + # Add all *.py files and package subdirectories + for filename in dirlist: + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if os.path.isdir(path): + if os.path.isfile(os.path.join(path, "__init__.py")): + # This is a package directory, add it + self.writepy(path, basename) # Recursive call + elif ext == ".py": + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + else: + # This is NOT a package directory, add its files at top level + if self.debug: + print "Adding files from directory", pathname + for filename in os.listdir(pathname): + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if ext == ".py": + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print "Adding", arcname + self.write(fname, arcname) + else: + if pathname[-3:] != ".py": + raise RuntimeError, \ + 'Files added with writepy() must end with ".py"' + fname, arcname = self._get_codename(pathname[0:-3], basename) + if self.debug: + print "Adding file", arcname + self.write(fname, arcname) + + def _get_codename(self, pathname, basename): + """Return (filename, archivename) for the path. + + Given a module name path, return the correct file path and + archive name, compiling if necessary. For example, given + /python/lib/string, return (/python/lib/string.pyc, string). + """ + file_py = pathname + ".py" + file_pyc = pathname + ".pyc" + file_pyo = pathname + ".pyo" + if os.path.isfile(file_pyo) and \ + os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime: + fname = file_pyo # Use .pyo file + elif not os.path.isfile(file_pyc) or \ + os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime: + import py_compile + if self.debug: + print "Compiling", file_py + try: + py_compile.compile(file_py, file_pyc, None, True) + except py_compile.PyCompileError,err: + print err.msg + fname = file_pyc + else: + fname = file_pyc + archivename = os.path.split(fname)[1] + if basename: + archivename = "%s/%s" % (basename, archivename) + return (fname, archivename) + + +def main(args = None): + import textwrap + USAGE=textwrap.dedent("""\ + Usage: + zipfile.py -l zipfile.zip # Show listing of a zipfile + zipfile.py -t zipfile.zip # Test if a zipfile is valid + zipfile.py -e zipfile.zip target # Extract zipfile into target dir + zipfile.py -c zipfile.zip src ... # Create zipfile from sources + """) + if args is None: + args = sys.argv[1:] + + if not args or args[0] not in ('-l', '-c', '-e', '-t'): + print USAGE + sys.exit(1) + + if args[0] == '-l': + if len(args) != 2: + print USAGE + sys.exit(1) + zf = ZipFile(args[1], 'r') + zf.printdir() + zf.close() + + elif args[0] == '-t': + if len(args) != 2: + print USAGE + sys.exit(1) + zf = ZipFile(args[1], 'r') + zf.testzip() + print "Done testing" + + elif args[0] == '-e': + if len(args) != 3: + print USAGE + sys.exit(1) + + zf = ZipFile(args[1], 'r') + out = args[2] + for path in zf.namelist(): + if path.startswith('./'): + tgt = os.path.join(out, path[2:]) + else: + tgt = os.path.join(out, path) + + tgtdir = os.path.dirname(tgt) + if not os.path.exists(tgtdir): + os.makedirs(tgtdir) + fp = open(tgt, 'wb') + fp.write(zf.read(path)) + fp.close() + zf.close() + + elif args[0] == '-c': + if len(args) < 3: + print USAGE + sys.exit(1) + + def addToZip(zf, path, zippath): + if os.path.isfile(path): + zf.write(path, zippath, ZIP_DEFLATED) + elif os.path.isdir(path): + for nm in os.listdir(path): + addToZip(zf, + os.path.join(path, nm), os.path.join(zippath, nm)) + # else: ignore + + zf = ZipFile(args[1], 'w', allowZip64=True) + for src in args[2:]: + addToZip(zf, src, os.path.basename(src)) + + zf.close() + +if __name__ == "__main__": + main() diff --git a/obitools/zipfile.pyc b/obitools/zipfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35dace048f12eb5e0b8574b5a4b9f01f60347ed5 GIT binary patch literal 38336 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 literal 0 HcmV?d00001