sequenceserver 0.8.9 → 1.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sequenceserver might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/{README.txt → README.md} +2 -0
- data/bin/sequenceserver +255 -55
- data/config.ru +2 -4
- data/lib/sequenceserver.rb +293 -447
- data/lib/sequenceserver/blast.rb +464 -64
- data/lib/sequenceserver/database.rb +185 -19
- data/lib/sequenceserver/links.rb +114 -0
- data/lib/sequenceserver/logger.rb +27 -0
- data/lib/sequenceserver/sequence.rb +141 -0
- data/public/css/bootstrap.min.css +8 -413
- data/public/css/custom.css +363 -122
- data/public/css/font-awesome.min.css +4 -0
- data/public/fonts/FontAwesome.otf +0 -0
- data/public/fonts/fontawesome-webfont.eot +0 -0
- data/public/fonts/fontawesome-webfont.svg +565 -0
- data/public/fonts/fontawesome-webfont.ttf +0 -0
- data/public/fonts/fontawesome-webfont.woff +0 -0
- data/public/fonts/fontawesome-webfont.woff2 +0 -0
- data/public/js/bootstrap.min.js +11 -0
- data/public/js/d3.v3.min.js +5 -0
- data/public/js/html5shiv.min.js +4 -0
- data/public/js/jquery.scrollspy.js +74 -0
- data/public/js/jquery.t.js +353 -0
- data/public/js/sequence.js +2419 -0
- data/public/js/sequenceserver.blast.js +29 -30
- data/public/js/sequenceserver.js +544 -120
- data/public/js/underscore.min.js +6 -0
- data/public/js/webshims/polyfiller.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/canvas2png.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/flashcanvas.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/flashcanvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/canvas2png.js +1 -0
- data/public/js/webshims/shims/FlashCanvasPro/flash10canvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/flash9canvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/flashcanvas.js +1 -0
- data/public/js/webshims/shims/canvas-blob.js +1 -0
- data/public/js/webshims/shims/color-picker.js +2 -0
- data/public/js/webshims/shims/combos/1.js +6 -0
- data/public/js/webshims/shims/combos/10.js +2 -0
- data/public/js/webshims/shims/combos/11.js +2 -0
- data/public/js/webshims/shims/combos/12.js +6 -0
- data/public/js/webshims/shims/combos/13.js +1 -0
- data/public/js/webshims/shims/combos/14.js +1 -0
- data/public/js/webshims/shims/combos/15.js +2 -0
- data/public/js/webshims/shims/combos/16.js +7 -0
- data/public/js/webshims/shims/combos/17.js +2 -0
- data/public/js/webshims/shims/combos/18.js +3 -0
- data/public/js/webshims/shims/combos/2.js +7 -0
- data/public/js/webshims/shims/combos/21.js +2 -0
- data/public/js/webshims/shims/combos/22.js +1 -0
- data/public/js/webshims/shims/combos/23.js +6 -0
- data/public/js/webshims/shims/combos/25.js +2 -0
- data/public/js/webshims/shims/combos/27.js +1 -0
- data/public/js/webshims/shims/combos/28.js +1 -0
- data/public/js/webshims/shims/combos/29.js +1 -0
- data/public/js/webshims/shims/combos/3.js +1 -0
- data/public/js/webshims/shims/combos/30.js +2 -0
- data/public/js/webshims/shims/combos/31.js +1 -0
- data/public/js/webshims/shims/combos/33.js +1 -0
- data/public/js/webshims/shims/combos/34.js +1 -0
- data/public/js/webshims/shims/combos/4.js +1 -0
- data/public/js/webshims/shims/combos/5.js +2 -0
- data/public/js/webshims/shims/combos/6.js +2 -0
- data/public/js/webshims/shims/combos/7.js +7 -0
- data/public/js/webshims/shims/combos/8.js +7 -0
- data/public/js/webshims/shims/combos/9.js +2 -0
- data/public/js/webshims/shims/combos/97.js +1 -0
- data/public/js/webshims/shims/combos/98.js +1 -0
- data/public/js/webshims/shims/combos/99.js +1 -0
- data/public/js/webshims/shims/details.js +1 -0
- data/public/js/webshims/shims/dom-extend.js +1 -0
- data/public/js/webshims/shims/es5.js +1 -0
- data/public/js/webshims/shims/es6.js +1 -0
- data/public/js/webshims/shims/excanvas.js +1 -0
- data/public/js/webshims/shims/filereader-xhr.js +1 -0
- data/public/js/webshims/shims/form-combat.js +1 -0
- data/public/js/webshims/shims/form-core.js +1 -0
- data/public/js/webshims/shims/form-datalist-lazy.js +1 -0
- data/public/js/webshims/shims/form-datalist.js +1 -0
- data/public/js/webshims/shims/form-fixrangechange.js +1 -0
- data/public/js/webshims/shims/form-inputmode.js +1 -0
- data/public/js/webshims/shims/form-message.js +1 -0
- data/public/js/webshims/shims/form-native-extend.js +1 -0
- data/public/js/webshims/shims/form-number-date-api.js +1 -0
- data/public/js/webshims/shims/form-number-date-ui.js +1 -0
- data/public/js/webshims/shims/form-shim-extend.js +1 -0
- data/public/js/webshims/shims/form-shim-extend2.js +1 -0
- data/public/js/webshims/shims/form-validation.js +1 -0
- data/public/js/webshims/shims/form-validators.js +1 -0
- data/public/js/webshims/shims/forms-picker.js +1 -0
- data/public/js/webshims/shims/geolocation.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ar.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ch-CN.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-cs.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-de.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-el.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-en.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-es.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-fa.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-fr.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-he.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-hi.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-hu.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-it.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ja.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-lt.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-nl.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pl.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt-BR.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt-PT.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ru.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-sv.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-zh-CN.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-zh-TW.js +1 -0
- data/public/js/webshims/shims/jme/alternate-media.js +1 -0
- data/public/js/webshims/shims/jme/base.js +1 -0
- data/public/js/webshims/shims/jme/controls.css +1 -0
- data/public/js/webshims/shims/jme/jme.eot +0 -0
- data/public/js/webshims/shims/jme/jme.svg +36 -0
- data/public/js/webshims/shims/jme/jme.ttf +0 -0
- data/public/js/webshims/shims/jme/jme.woff +0 -0
- data/public/js/webshims/shims/jme/mediacontrols-lazy.js +1 -0
- data/public/js/webshims/shims/jme/mediacontrols.js +1 -0
- data/public/js/webshims/shims/jme/playlist.js +1 -0
- data/public/js/webshims/shims/jpicker/images/AlphaBar.png +0 -0
- data/public/js/webshims/shims/jpicker/images/Bars.png +0 -0
- data/public/js/webshims/shims/jpicker/images/Maps.png +0 -0
- data/public/js/webshims/shims/jpicker/images/NoColor.png +0 -0
- data/public/js/webshims/shims/jpicker/images/bar-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/map-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/mappoint.gif +0 -0
- data/public/js/webshims/shims/jpicker/images/picker.gif +0 -0
- data/public/js/webshims/shims/jpicker/images/preview-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/rangearrows.gif +0 -0
- data/public/js/webshims/shims/jpicker/jpicker.css +1 -0
- data/public/js/webshims/shims/matchMedia.js +3 -0
- data/public/js/webshims/shims/mediacapture-picker.js +1 -0
- data/public/js/webshims/shims/mediacapture.js +1 -0
- data/public/js/webshims/shims/mediaelement-core.js +1 -0
- data/public/js/webshims/shims/mediaelement-debug.js +1 -0
- data/public/js/webshims/shims/mediaelement-jaris.js +1 -0
- data/public/js/webshims/shims/mediaelement-native-fix.js +1 -0
- data/public/js/webshims/shims/mediaelement-yt.js +1 -0
- data/public/js/webshims/shims/moxie/flash/Moxie.cdn.swf +0 -0
- data/public/js/webshims/shims/moxie/flash/Moxie.min.swf +0 -0
- data/public/js/webshims/shims/moxie/js/moxie-html4.js +3 -0
- data/public/js/webshims/shims/moxie/js/moxie-swf.js +2 -0
- data/public/js/webshims/shims/picture.js +1 -0
- data/public/js/webshims/shims/plugins/jquery.ui.position.js +11 -0
- data/public/js/webshims/shims/range-ui.js +1 -0
- data/public/js/webshims/shims/sizzle.js +11 -0
- data/public/js/webshims/shims/sticky.js +1 -0
- data/public/js/webshims/shims/styles/color-picker.png +0 -0
- data/public/js/webshims/shims/styles/forms-ext.css +1 -0
- data/public/js/webshims/shims/styles/forms-picker.css +1 -0
- data/public/js/webshims/shims/styles/progress.gif +0 -0
- data/public/js/webshims/shims/styles/progress.png +0 -0
- data/public/js/webshims/shims/styles/shim-ext.css +1 -0
- data/public/js/webshims/shims/styles/shim.css +1 -0
- data/public/js/webshims/shims/styles/transparent.png +0 -0
- data/public/js/webshims/shims/styles/widget.eot +0 -0
- data/public/js/webshims/shims/styles/widget.svg +12 -0
- data/public/js/webshims/shims/styles/widget.ttf +0 -0
- data/public/js/webshims/shims/styles/widget.woff +0 -0
- data/public/js/webshims/shims/swf/JarisFLVPlayer.swf +0 -0
- data/public/js/webshims/shims/swfmini-embed.js +1 -0
- data/public/js/webshims/shims/swfmini.js +6 -0
- data/public/js/webshims/shims/track-ui.js +1 -0
- data/public/js/webshims/shims/track.js +1 -0
- data/public/js/webshims/shims/url.js +1 -0
- data/public/js/webshims/shims/usermedia-core.js +1 -0
- data/public/js/webshims/shims/usermedia-shim.js +1 -0
- data/sequenceserver.gemspec +16 -13
- data/views/400.erb +28 -0
- data/views/500.erb +35 -19
- data/views/_options.erb +6 -15
- data/views/result.erb +218 -0
- data/views/search.erb +354 -151
- metadata +254 -62
- data/example.config.yml +0 -39
- data/lib/sequenceserver/customisation.rb +0 -60
- data/lib/sequenceserver/database_formatter.rb +0 -190
- data/lib/sequenceserver/helpers.rb +0 -136
- data/lib/sequenceserver/sequencehelpers.rb +0 -93
- data/lib/sequenceserver/sinatralikeloggerformatter.rb +0 -12
- data/lib/sequenceserver/version.rb +0 -9
- data/public/css/beige.css.css +0 -254
- data/public/css/bootstrap.dropdown.css +0 -29
- data/public/css/bootstrap.icons.css +0 -155
- data/public/css/bootstrap.modal.css +0 -28
- data/public/js/bootstrap.dropdown.js +0 -92
- data/public/js/bootstrap.modal.js +0 -7
- data/public/js/bootstrap.transition.js +0 -7
- data/public/js/jquery-scrollspy.js +0 -98
- data/public/js/jquery.activity.js +0 -10
- data/public/js/jquery.enablePlaceholder.min.js +0 -10
- data/public/js/store.min.js +0 -2
- data/public/sequence.html +0 -28
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta +0 -5486
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta +0 -6449
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.psq +0 -0
- data/tests/run +0 -26
- data/tests/test_sequencehelpers.rb +0 -77
- data/tests/test_sequenceserver_blast.rb +0 -60
- data/tests/test_ui.rb +0 -104
- data/tests/ui.specs.todo +0 -10
data/lib/sequenceserver/blast.rb
CHANGED
@@ -1,92 +1,492 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'tempfile'
|
3
|
+
require 'ox'
|
4
|
+
|
5
|
+
require 'sequenceserver/links'
|
2
6
|
|
3
7
|
module SequenceServer
|
4
|
-
# Simple BLAST+
|
5
|
-
|
8
|
+
# Simple wrapper around BLAST+ search algorithms.
|
9
|
+
#
|
10
|
+
# `BLAST::ArgumentError` and `BLAST::RuntimeError` signal errors encountered
|
11
|
+
# when attempting a BLAST search.
|
12
|
+
module BLAST
|
13
|
+
# To signal error in query sequence or options.
|
14
|
+
#
|
15
|
+
# ArgumentError is raised when BLAST+'s exit status is 1; see [1].
|
16
|
+
class ArgumentError < ArgumentError
|
17
|
+
end
|
6
18
|
|
7
|
-
|
19
|
+
# To signal internal errors.
|
20
|
+
#
|
21
|
+
# RuntimeError is raised when BLAST+'s exits status is one of 2, 3, 4, or
|
22
|
+
# 255; see [1]. These are rare, infrastructure errors, used internally,
|
23
|
+
# and of concern only to the admins/developers.
|
24
|
+
class RuntimeError < RuntimeError
|
25
|
+
def initialize(status, message)
|
26
|
+
@status = status
|
27
|
+
@message = message
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :status, :message
|
8
31
|
|
9
|
-
|
10
|
-
|
32
|
+
def to_s
|
33
|
+
"#{status}, #{message}"
|
34
|
+
end
|
35
|
+
end
|
11
36
|
|
12
|
-
#
|
13
|
-
|
37
|
+
# Capture results per query of a BLAST search.
|
38
|
+
# @member [String] number
|
39
|
+
# @member [String] def
|
40
|
+
# @member [Fixnum] len
|
41
|
+
# @member [Array(Hit)] hits
|
42
|
+
Query = Struct.new(:number, :def, :len, :hits) do
|
43
|
+
def initialize(*args)
|
44
|
+
args[0] = args[0].to_i
|
45
|
+
args[1] = "Query_#{args[0]}" if args[1] == 'No definition line'
|
46
|
+
args[2] = args[2].to_i
|
47
|
+
@id, *rest = args[1].split
|
48
|
+
@title = rest.join(' ')
|
49
|
+
super
|
50
|
+
end
|
14
51
|
|
15
|
-
|
16
|
-
|
52
|
+
def sort_hits_by_evalue!
|
53
|
+
@hits = hits.sort_by(&:evalue)
|
54
|
+
end
|
17
55
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# * method (String) - blast executable (shell executable, or absolute path)
|
22
|
-
# * query (String) - query string
|
23
|
-
# * databases (String) - database name as returned by 'blastdbcmd -list'
|
24
|
-
# * options (String) - other options
|
25
|
-
#
|
26
|
-
# ---
|
27
|
-
# Examples:
|
28
|
-
#
|
29
|
-
# b = Blast.new("blastn", 'ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG', "S.cdna.fasta", "-html -num_threads 4")
|
30
|
-
#
|
31
|
-
# b.run! => true
|
32
|
-
# b.result => "blast output"
|
33
|
-
def initialize(method, query, databases, options = nil)
|
34
|
-
@method = method
|
35
|
-
@databases = databases
|
36
|
-
|
37
|
-
# create a tempfile for the given query
|
38
|
-
@qfile = Tempfile.new('sequenceserver_query')
|
39
|
-
@qfile.puts(query)
|
40
|
-
@qfile.close
|
41
|
-
|
42
|
-
# Add -outfmt 11 to list of options so that it outputs a blast archive
|
43
|
-
@options = options.to_s
|
44
|
-
@options += " -html"
|
56
|
+
attr_reader :id, :title
|
57
|
+
|
58
|
+
alias_method :length, :len
|
45
59
|
end
|
46
60
|
|
47
|
-
#
|
48
|
-
#
|
49
|
-
|
50
|
-
|
61
|
+
# Hit Object to store all the hits per Query.
|
62
|
+
# @member [Fixnum] number
|
63
|
+
# @member [String] id
|
64
|
+
# @member [String] def
|
65
|
+
# @member [String] accession
|
66
|
+
# @member [Fixnum] len
|
67
|
+
# @member [HSP] hsp
|
68
|
+
Hit = Struct.new(:number, :id, :title, :accession, :len, :hsps) do
|
69
|
+
def initialize(*args)
|
70
|
+
args[0] = args[0].to_i
|
71
|
+
args[2] = '' if args[2] == 'No definition line'
|
72
|
+
args[4] = args[4].to_i
|
73
|
+
super
|
74
|
+
end
|
51
75
|
|
52
|
-
|
76
|
+
alias_method :length, :len
|
53
77
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@error = [400, message]
|
58
|
-
else
|
59
|
-
@error = [500, @error]
|
78
|
+
# Hit evalue is the minimum evalue of all HSP(s).
|
79
|
+
def evalue
|
80
|
+
hsps.map(&:evalue).min
|
60
81
|
end
|
61
82
|
|
62
|
-
|
83
|
+
# Hit score is the sum of bit scores of all HSP(s).
|
84
|
+
def score
|
85
|
+
hsps.map(&:bit_score).reduce(:+)
|
86
|
+
end
|
63
87
|
end
|
64
88
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
89
|
+
# Structure to hold the HSP information about each hit. For more
|
90
|
+
# information, check the link contained in the references section at the
|
91
|
+
# end of the file.
|
92
|
+
HSP = Struct.new(:number, :bit_score, :score, :evalue, :qstart, :qend,
|
93
|
+
:sstart, :send, :qframe, :sframe, :identity, :positives,
|
94
|
+
:gaps, :len, :qseq, :sseq, :midline) do
|
95
|
+
|
96
|
+
INTEGER_ARGS = [0, 2].concat((4..13).to_a)
|
97
|
+
FLOAT_ARGS = [1, 3]
|
98
|
+
|
99
|
+
def initialize(*args)
|
100
|
+
INTEGER_ARGS.each do |i|
|
101
|
+
args[i] = args[i].to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
FLOAT_ARGS.each do |i|
|
105
|
+
args[i] = args[i].to_f
|
106
|
+
end
|
107
|
+
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
alias_method :length, :len
|
112
|
+
|
68
113
|
end
|
69
114
|
|
70
|
-
#
|
71
|
-
|
72
|
-
|
115
|
+
# Captures BLAST results from BLAST+'s XML output.
|
116
|
+
class Report
|
117
|
+
|
118
|
+
include Links
|
119
|
+
|
120
|
+
# Expects a File object and Database objects used to BLAST against.
|
121
|
+
#
|
122
|
+
# Parses the XML file into an intermediate representation (ir) and
|
123
|
+
# constructs an object model from that.
|
124
|
+
#
|
125
|
+
# NOTE:
|
126
|
+
# Databases param is optional for test suite.
|
127
|
+
def initialize(rfile, databases = nil)
|
128
|
+
ir = node_to_array Ox.parse(rfile.read).root
|
129
|
+
|
130
|
+
@program = ir[0]
|
131
|
+
@program_version = ir[1]
|
132
|
+
@querydb = Array databases
|
133
|
+
@parameters = {
|
134
|
+
:matrix => ir[7][0],
|
135
|
+
:evalue => ir[7][1],
|
136
|
+
:gapopen => ir[7][2],
|
137
|
+
:gapextend => ir[7][3],
|
138
|
+
:filters => ir[7][4]
|
139
|
+
}
|
140
|
+
|
141
|
+
ir[8].each_with_index do |n, i|
|
142
|
+
@stats ||= n[5][0]
|
143
|
+
@queries ||= []
|
144
|
+
@queries.push(Query.new(n[0], n[2], n[3], []))
|
145
|
+
|
146
|
+
# Ensure a hit object is received. No hits, returns a newline. Note
|
147
|
+
# that checking to "\n" doesn't work since n[4] = ["\n"]
|
148
|
+
if n[4] == ["\n"]
|
149
|
+
@queries[i][:hits] = []
|
150
|
+
else
|
151
|
+
n[4].each_with_index do |hits, j|
|
152
|
+
@queries[i][:hits].push(Hit.new(hits[0], hits[1], hits[2],
|
153
|
+
hits[3], hits[4], []))
|
154
|
+
hits[5].each do |hsp|
|
155
|
+
@queries[i][:hits][j][:hsps].push(HSP.new(*hsp))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
@queries[i].sort_hits_by_evalue!
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
attr_reader :program, :program_version
|
164
|
+
|
165
|
+
# :nodoc:
|
166
|
+
# params are defaults provided by BLAST or user input to tweak the
|
167
|
+
# result. stats are computed metrics provided by BLAST.
|
168
|
+
#
|
169
|
+
# BLAST+ doesn't list all input params (like word_size) in the XML
|
170
|
+
# output. Only matrix, evalue, gapopen, gapextend, and filters.
|
171
|
+
attr_reader :params, :stats
|
172
|
+
|
173
|
+
attr_reader :querydb
|
174
|
+
|
175
|
+
attr_reader :queries
|
176
|
+
|
177
|
+
# Helper methods for pretty printing results
|
178
|
+
|
179
|
+
# FIXME: HTML!!
|
180
|
+
def pretty_evalue(hsp)
|
181
|
+
hsp.evalue.to_s.sub(/(\d*\.\d*)e?([+-]\d*)?/) do
|
182
|
+
s = '%.3f' % Regexp.last_match[1]
|
183
|
+
s << " × 10<sup>#{Regexp.last_match[2]}</sup>" if Regexp.last_match[2]
|
184
|
+
s
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def identity_fraction(hsp)
|
189
|
+
"#{hsp.identity}/#{hsp.length}"
|
190
|
+
end
|
191
|
+
|
192
|
+
def positives_fraction(hsp)
|
193
|
+
"#{hsp.positives}/#{hsp.length}"
|
194
|
+
end
|
195
|
+
|
196
|
+
def gaps_fraction(hsp)
|
197
|
+
"#{hsp.gaps}/#{hsp.length}"
|
198
|
+
end
|
199
|
+
|
200
|
+
def identity_percentage(hsp)
|
201
|
+
"#{'%.2f' % (hsp.identity * 100.0 / hsp.length)}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def positives_percentage(hsp)
|
205
|
+
"#{'%.2f' % (hsp.positives * 100.0 / hsp.length)}"
|
206
|
+
end
|
207
|
+
|
208
|
+
def gaps_percentage(hsp)
|
209
|
+
"#{'%.2f' % (hsp.gaps * 100.0 / hsp.length)}"
|
210
|
+
end
|
211
|
+
|
212
|
+
# FIXME: Test me.
|
213
|
+
def pp_hsp(hsp)
|
214
|
+
# In many of the BLAST algorithms, translated queries are performed
|
215
|
+
# which has to be taken care while determining end co ordinates of
|
216
|
+
# query and subject sequences. Since each amino acid is encoded using
|
217
|
+
# three nucl. referred to as codons, necessary value is multiplied
|
218
|
+
# to determine the coordinates.
|
219
|
+
|
220
|
+
# blastn and blastp search the nucleotide and protein databases using
|
221
|
+
# nucleotide and protein queries respectively.
|
222
|
+
qframe_unit = 1
|
223
|
+
sframe_unit = 1
|
224
|
+
# tblastn searches translated nucleotide database using a protein query
|
225
|
+
if @program == 'tblastn'
|
226
|
+
sframe_unit = 3
|
227
|
+
# blastx searches protein database using a translated nucleotide query,
|
228
|
+
elsif @program == 'blastx'
|
229
|
+
qframe_unit = 3
|
230
|
+
# tblastx searches translated nucleotide database using a translated
|
231
|
+
# nucleotide query.
|
232
|
+
elsif @program == 'tblastx'
|
233
|
+
qframe_unit = 3
|
234
|
+
sframe_unit = 3
|
235
|
+
end
|
236
|
+
|
237
|
+
qframe_sign = hsp.qframe >= 0 ? 1 : -1
|
238
|
+
sframe_sign = hsp.sframe >= 0 ? 1 : -1
|
239
|
+
|
240
|
+
chars = 60
|
241
|
+
lines = (hsp.length / chars.to_f).ceil
|
242
|
+
width = [hsp.qend, hsp.send, hsp.qstart,
|
243
|
+
hsp.sstart].map(&:to_s).map(&:length).max
|
244
|
+
|
245
|
+
# blastn results are inconsistent with the other methods as it
|
246
|
+
# automatically reverse the start and end coordinates (based on
|
247
|
+
# frame), while for others it has to be inferred.
|
248
|
+
if @program != 'blastn'
|
249
|
+
nqseq = hsp.qframe >= 0 ? hsp.qstart : hsp.qend
|
250
|
+
nsseq = hsp.sframe >= 0 ? hsp.sstart : hsp.send
|
251
|
+
else
|
252
|
+
nqseq = hsp.qstart
|
253
|
+
nsseq = hsp.sstart
|
254
|
+
end
|
255
|
+
|
256
|
+
s = ''
|
257
|
+
(1..lines).each do |i|
|
258
|
+
lqstart = nqseq
|
259
|
+
lqseq = hsp.qseq[chars * (i - 1), chars]
|
260
|
+
nqseq += (lqseq.length - lqseq.count('-')) * qframe_unit * qframe_sign
|
261
|
+
lqend = nqseq - qframe_sign
|
262
|
+
s << "Query %#{width}d #{lqseq} #{lqend}\n" % lqstart
|
263
|
+
|
264
|
+
lmseq = hsp.midline[chars * (i - 1), chars]
|
265
|
+
s << "#{' ' * (width + 8)} #{lmseq}\n"
|
266
|
+
|
267
|
+
lsstart = nsseq
|
268
|
+
lsseq = hsp.sseq[chars * (i - 1), chars]
|
269
|
+
nsseq += (lsseq.length - lsseq.count('-')) * sframe_unit * sframe_sign
|
270
|
+
lsend = nsseq - sframe_sign
|
271
|
+
s << "Subject %#{width}d #{lsseq} #{lsend}\n" % lsstart
|
272
|
+
|
273
|
+
s << "\n" unless i == lines
|
274
|
+
end
|
275
|
+
s
|
276
|
+
end
|
277
|
+
|
278
|
+
# FIXME: Document me.
|
279
|
+
def filter_hsp_stats(hsp)
|
280
|
+
hsp_stats = {
|
281
|
+
'Score' => "#{'%.2f' % hsp[:bit_score]} (#{hsp[:score]})",
|
282
|
+
'E value' => "#{pretty_evalue hsp}",
|
283
|
+
'Identities' => "#{identity_fraction hsp} " \
|
284
|
+
"(#{identity_percentage hsp}%)",
|
285
|
+
'Gaps' => "#{gaps_fraction hsp} (#{gaps_percentage hsp}%)"
|
286
|
+
}
|
287
|
+
|
288
|
+
if @program == 'blastp'
|
289
|
+
hsp_stats['Positives'] = "#{positives_fraction hsp}" \
|
290
|
+
"(#{positives_percentage hsp}%)"
|
291
|
+
elsif @program == 'blastx'
|
292
|
+
hsp_stats['Query frame'] = "#{hsp[:qframe]}"
|
293
|
+
elsif @program == 'tblastn'
|
294
|
+
hsp_stats['Hit frame'] = "#{hsp[:sframe]}"
|
295
|
+
elsif @program == 'tblastx'
|
296
|
+
hsp_stats['Positives'] = "#{positives_fraction hsp}" \
|
297
|
+
"(#{positives_percentage hsp}%)"
|
298
|
+
hsp_stats['Frame'] = "#{hsp[:qframe]}/#{hsp[:sframe]}"
|
299
|
+
elsif @program == 'blastn'
|
300
|
+
hsp_stats['Strand'] = "#{hsp[:qframe] > 0 ? '+' : '-'}/" \
|
301
|
+
"#{hsp[:sframe] > 0 ? '+' : '-'}"
|
302
|
+
end
|
303
|
+
|
304
|
+
hsp_stats
|
305
|
+
end
|
306
|
+
|
307
|
+
def link_per_hit(sequence_id)
|
308
|
+
links = Links.instance_methods.map {|m| send(m, sequence_id)}
|
309
|
+
|
310
|
+
# Sort links based on :order key (ascending)
|
311
|
+
links.compact!.sort_by! {|link| link[:order]}
|
312
|
+
end
|
313
|
+
|
314
|
+
# Returns an array of database objects which contain the queried
|
315
|
+
# sequence id.
|
316
|
+
# NOTE: This function may return more than one database object for
|
317
|
+
# a single sequence id.
|
318
|
+
#
|
319
|
+
# e.g., which_blastdb('SI_2.2.23') => [<Database: ...>, ...]
|
320
|
+
def which_blastdb(sequence_id)
|
321
|
+
querydb.select {|db| db.include? sequence_id}
|
322
|
+
end
|
323
|
+
|
324
|
+
private
|
325
|
+
|
326
|
+
PARSEABLE_AS_ARRAY = %w(Parameters BlastOutput_param Iteration_stat
|
327
|
+
Statistics Iteration_hits BlastOutput_iterations
|
328
|
+
Iteration Hit Hit_hsps Hsp)
|
329
|
+
|
330
|
+
def node_to_array(element)
|
331
|
+
element.nodes.map {|n| node_to_value n}
|
332
|
+
end
|
333
|
+
|
334
|
+
def node_to_value(node)
|
335
|
+
# Ensure that the recursion doesn't fails when String value is received.
|
336
|
+
return node if node.is_a?(String)
|
337
|
+
|
338
|
+
if PARSEABLE_AS_ARRAY.include? node.name
|
339
|
+
value = node_to_array(node)
|
340
|
+
else
|
341
|
+
value = first_text(node)
|
342
|
+
end
|
343
|
+
value
|
344
|
+
end
|
345
|
+
|
346
|
+
def first_text(node)
|
347
|
+
node.nodes.find {|n| n.is_a? String}
|
348
|
+
end
|
73
349
|
end
|
74
350
|
|
75
|
-
|
351
|
+
ERROR_LINE = /\(CArgException.*\)\s(.*)/
|
352
|
+
|
353
|
+
ALGORITHMS = %w(blastn blastp blastx tblastn tblastx)
|
354
|
+
|
355
|
+
extend self
|
356
|
+
|
357
|
+
extend Forwardable
|
358
|
+
|
359
|
+
def_delegators SequenceServer, :config, :logger
|
76
360
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
efile = Tempfile.new('sequenceserver_error')
|
81
|
-
[rfile, efile].each {|file| file.close}
|
361
|
+
def run(params)
|
362
|
+
pre_process params
|
363
|
+
validate_blast_params params
|
82
364
|
|
365
|
+
# Compile parameters for BLAST search into a shell executable command.
|
366
|
+
#
|
367
|
+
# BLAST method to use.
|
368
|
+
method = params[:method]
|
369
|
+
#
|
370
|
+
# BLAST+ expects query sequence as a file.
|
371
|
+
qfile = Tempfile.new('sequenceserver_query')
|
372
|
+
qfile.puts(params[:sequence])
|
373
|
+
qfile.close
|
374
|
+
#
|
375
|
+
# Retrieve database objects from database id.
|
376
|
+
databases = Database[params[:databases]]
|
377
|
+
#
|
378
|
+
# Concatenate other blast options.
|
379
|
+
options = params[:advanced].to_s.strip + defaults
|
380
|
+
#
|
381
|
+
# blastn implies blastn, not megablast; but let's not interfere if a user
|
382
|
+
# specifies `task` herself.
|
383
|
+
if method == 'blastn' and not options =~ /task/
|
384
|
+
options << ' -task blastn'
|
385
|
+
end
|
386
|
+
|
387
|
+
# Run BLAST search.
|
388
|
+
#
|
389
|
+
# Command to execute.
|
390
|
+
command = "#{method} -db '#{databases.map(&:name).join(' ')}'" \
|
391
|
+
" -query '#{qfile.path}' #{options}"
|
392
|
+
#
|
393
|
+
# Debugging log.
|
394
|
+
logger.debug("Executing: #{command}")
|
395
|
+
#
|
396
|
+
# Temporary files to capture stdout and stderr.
|
397
|
+
rfile = Tempfile.new('sequenceserver_blast_result')
|
398
|
+
efile = Tempfile.new('sequenceserver_blast_error')
|
399
|
+
[rfile, efile].each(&:close)
|
400
|
+
#
|
401
|
+
# Execute.
|
83
402
|
system("#{command} > #{rfile.path} 2> #{efile.path}")
|
403
|
+
|
404
|
+
# Capture error.
|
84
405
|
status = $?.exitstatus
|
406
|
+
case status
|
407
|
+
when 1 # error in query sequence or options; see [1]
|
408
|
+
efile.open
|
409
|
+
|
410
|
+
# Most of the time BLAST+ generates a verbose error message with
|
411
|
+
# details we don't require. So we parse out the relevant lines.
|
412
|
+
error = efile.each_line do |l|
|
413
|
+
break Regexp.last_match[1] if l.match(ERROR_LINE)
|
414
|
+
end
|
415
|
+
|
416
|
+
# But sometimes BLAST+ returns the exact/relevant error message.
|
417
|
+
# Trying to parse such messages returns nil, and we use the error
|
418
|
+
# message from BLAST+ as it is.
|
419
|
+
error = efile.rewind && efile.read unless error.is_a? String
|
420
|
+
|
421
|
+
efile.close
|
422
|
+
raise ArgumentError, error
|
423
|
+
when 2, 3, 4, 255 # see [1]
|
424
|
+
efile.open
|
425
|
+
error = efile.read
|
426
|
+
efile.close
|
427
|
+
raise RuntimeError.new(status, error)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Report the results, ensures that file is closed after execution.
|
431
|
+
File.open(rfile.path) {|f| Report.new(f, databases)}
|
432
|
+
end
|
433
|
+
|
434
|
+
def pre_process(params)
|
435
|
+
unless params[:sequence].nil?
|
436
|
+
params[:sequence].strip!
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def validate_blast_params(params)
|
441
|
+
validate_blast_method params[:method]
|
442
|
+
validate_blast_sequences params[:sequence]
|
443
|
+
validate_blast_databases params[:databases]
|
444
|
+
validate_blast_options params[:advanced]
|
445
|
+
end
|
85
446
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
447
|
+
def defaults
|
448
|
+
" -outfmt 5 -num_threads #{config[:num_threads]}"
|
449
|
+
end
|
450
|
+
|
451
|
+
def validate_blast_method(method)
|
452
|
+
return true if ALGORITHMS.include? method
|
453
|
+
raise ArgumentError, "BLAST algorithm should be one of:" \
|
454
|
+
" #{ALGORITHMS.join(', ')}."
|
455
|
+
end
|
456
|
+
|
457
|
+
def validate_blast_sequences(sequences)
|
458
|
+
return true if sequences.is_a? String and not sequences.empty?
|
459
|
+
raise ArgumentError, 'Sequences should be a non-empty string.'
|
460
|
+
end
|
461
|
+
|
462
|
+
def validate_blast_databases(database_ids)
|
463
|
+
ids = Database.ids
|
464
|
+
return true if database_ids.is_a?(Array) && !database_ids.empty? &&
|
465
|
+
(ids & database_ids).length == database_ids.length
|
466
|
+
raise ArgumentError, "Database id should be one of:" \
|
467
|
+
" #{ids.join("\n")}."
|
468
|
+
end
|
469
|
+
|
470
|
+
# Advanced options are specified by the user. Here they are checked for
|
471
|
+
# interference with SequenceServer operations.
|
472
|
+
# raise ArgumentError if an error has occurred, else return without value
|
473
|
+
def validate_blast_options(options)
|
474
|
+
return true if !options || (options.is_a?(String) && options.strip.empty?)
|
475
|
+
|
476
|
+
unless options =~ /\A[a-z0-9\-_\. ']*\Z/i
|
477
|
+
raise ArgumentError, 'Invalid characters detected in options.'
|
478
|
+
end
|
479
|
+
|
480
|
+
disallowed_options = %w(-out -html -outfmt -db -query)
|
481
|
+
disallowed_options.each do |o|
|
482
|
+
if options =~ /#{o}/i
|
483
|
+
raise ArgumentError, "Option \"#{o}\" is prohibited."
|
484
|
+
end
|
485
|
+
end
|
90
486
|
end
|
91
487
|
end
|
92
488
|
end
|
489
|
+
|
490
|
+
# References
|
491
|
+
# ----------
|
492
|
+
# [1]: http://www.ncbi.nlm.nih.gov/books/NBK1763/
|