bio-mummer 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 37345c73287e9b80c15dc7426ee1045caad772f7
4
- data.tar.gz: 8b57b1755f5da0ba52af5ccbb92c3d55f06964e5
3
+ metadata.gz: 299d153188c65b1881bf1635b7389276ab1733f2
4
+ data.tar.gz: 3e3bf43c9c1551715d82c40bfd6c1b151f58dc2f
5
5
  SHA512:
6
- metadata.gz: c86e8bb5d957da2331d817101577aa9990e4685a6445d5b194747c069fc8d5708b93465e20ff95b4e1a37de9ab02402552c1d77a7ba1acd98ddbeb61bb98d574
7
- data.tar.gz: ffb5c859d7add8bb10c9d0a8dc5dc98a51dcb9a06370633d7fe6d69a3bd8d295836bafe4e32796ba6b62a855cde9a224731aa1c1cf45e077e8cc21eb4c31fb84
6
+ metadata.gz: c397099eb1478ceb5bc65d9657c64cfbe1bd02d2d0ce8e91a878065d1cf24407553d2f9609f8102af8b361d30b9048a499c4863a2b0772462f1977ae7d4d75ea
7
+ data.tar.gz: 3b1cdf0711552be2cdbfb3d1ad309d5d8abcdf7bbdae096cbcf439c6a9da2bba181b1085088ff4e1dcf1f8f84839186337977b048f7a89700b8c800717c71501
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -20,6 +20,7 @@ class OptParser
20
20
  opts.on("-d", "--delta FILENAME", "Delta file generated by nucmer") do |fn|
21
21
  pn = Pathname.new(fn)
22
22
  if pn.size?
23
+ options.samplename = pn.basename(".delta")
23
24
  options.delta = pn
24
25
  match = pn.each_line.first.match(/(?<ref>\/.*) (?<qry>\/.*)/)
25
26
  if match
@@ -38,6 +39,10 @@ class OptParser
38
39
  exit(1)
39
40
  end
40
41
  end
42
+
43
+ opts.on("-s", "--sample SAMPLENAME", "gVCF Sample name") do |samplename|
44
+ options.samplename = samplename
45
+ end
41
46
  end
42
47
  opt_parser.parse!(args)
43
48
  return options
@@ -45,7 +50,6 @@ class OptParser
45
50
  end
46
51
  options = OptParser.parse(ARGV)
47
52
 
48
- # Open the Delta File
49
53
  d = BioMummer::DeltaFile.new(StringIO.new(`delta-filter -r -q #{options.delta}`))
50
54
 
51
55
  puts '##fileformat=VCFv4.1
@@ -57,125 +61,173 @@ puts '##fileformat=VCFv4.1
57
61
  ##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
58
62
  ##FORMAT=<ID=MIN_DP,Number=1,Type=Integer,Description="Minimum DP observed within the GVCF block">
59
63
  ##FORMAT=<ID=PL,Number=G,Type=Integer,Description="Normalized, Phred-scaled likelihoods for genotypes as defined in the VCF specification">'
60
-
61
64
  options.ref.each do |name, seq|
62
65
  puts "##contig=<ID=#{name},length=#{seq.length}>"
63
66
  end
64
- puts "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tDUMMYSAMPLENAME"
67
+ puts '##INFO=<ID=BaseQRankSum,Number=1,Type=Float,Description="Z-score from Wilcoxon rank sum test of Alt Vs. Ref base qualities">
68
+ ##INFO=<ID=ClippingRankSum,Number=1,Type=Float,Description="Z-score From Wilcoxon rank sum test of Alt vs. Ref number of hard clipped bases">
69
+ ##INFO=<ID=DP,Number=1,Type=Integer,Description="Approximate read depth; some reads may have been filtered">
70
+ ##INFO=<ID=DS,Number=0,Type=Flag,Description="Were any of the samples downsampled?">
71
+ ##INFO=<ID=END,Number=1,Type=Integer,Description="Stop position of the interval">
72
+ ##INFO=<ID=HaplotypeScore,Number=1,Type=Float,Description="Consistency of the site with at most two segregating haplotypes">
73
+ ##INFO=<ID=InbreedingCoeff,Number=1,Type=Float,Description="Inbreeding coefficient as estimated from the genotype likelihoods per-sample when compared against the Hardy-Weinberg expectation">
74
+ ##INFO=<ID=MLEAC,Number=A,Type=Integer,Description="Maximum likelihood expectation (MLE) for the allele counts (not necessarily the same as the AC), for each ALT allele, in the same order as listed">
75
+ ##INFO=<ID=MLEAF,Number=A,Type=Float,Description="Maximum likelihood expectation (MLE) for the allele frequency (not necessarily the same as the AF), for each ALT allele, in the same order as listed">
76
+ ##INFO=<ID=MQ,Number=1,Type=Float,Description="RMS Mapping Quality">
77
+ ##INFO=<ID=MQ0,Number=1,Type=Integer,Description="Total Mapping Quality Zero Reads">
78
+ ##INFO=<ID=MQRankSum,Number=1,Type=Float,Description="Z-score From Wilcoxon rank sum test of Alt vs. Ref read mapping qualities">
79
+ ##INFO=<ID=ReadPosRankSum,Number=1,Type=Float,Description="Z-score from Wilcoxon rank sum test of Alt vs. Ref read position bias">'
80
+ puts "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t#{options.samplename}"
65
81
 
66
82
  d.alignments
67
- .group_by{|a| a.refname}.sort_by{|refname, alignments| refname}
68
- .take(2)
83
+ .group_by{ |alignment| alignment.refname }
84
+ .sort_by{ |refname, alignments| refname }
69
85
  .each do |refname, alignments|
70
- cursor = 1
71
- overlap = 0
72
- alignments.sort_by{|a| [a.refstart, a.refstop].min}.each do |a|
73
- ref = options.ref[a.refname].subseq(a.refstart, a.refstop)
74
- qry = a.strand ? options.qry[a.queryname].subseq(a.querystart, a.querystop) : options.qry[a.queryname].subseq(a.querystart, a.querystop).complement
75
- if a.refstart > cursor
76
- puts [refname, cursor, '.', options.ref[refname][cursor].upcase, "<NON_REF>", '.', '.', "END=#{a.refstart - 1}", "GT:DP:GQ:MIN_DP:PL", "0:0:0:0:0,0"].join("\t")
77
- cursor = a.refstart
78
- elsif a.refstart < cursor
79
- overlap = cursor - a.refstart
80
- end
81
-
82
- a.distances.inject([0,0]) do |mem, distance|
83
- if distance > 0
84
- qry.insert(mem.first + distance - 1, ".")
85
- [mem.first + distance, mem.last + distance]
86
- else
87
- ref.insert(mem.last - distance - 1, ".")
88
- [mem.first - distance, mem.last - distance]
89
- end
90
- end
91
- overlap2 = 0
92
-
93
- ref.chars.zip(0..(ref.length-1), qry.chars).chunk do |refBase, i, qryBase|
94
- if refBase == qryBase
95
- "NOVAR"
96
- elsif refBase == "."
97
- "INS"
98
- elsif qryBase == "."
99
- "DEL"
86
+
87
+ clusters = Enumerator.new do |yielder|
88
+ # We sweep along each of the reference sequences. The cursor tracks
89
+ # our position as we go so we don't concatenate regions that are
90
+ # actually overlapping.
91
+ cursor = 1
92
+
93
+ # The =alignments= object is an array of BioMummer::Alignment
94
+ # objects that all match a particular scaffold.
95
+ alignments
96
+ .sort_by{ |alignment| alignment.refstart }.
97
+ each do |aln|
98
+ # Create Bio::Sequence::NA subsequences for the reference and
99
+ # query. They are initially unaligned.
100
+ refSeq = options.ref[aln.refname].subseq(aln.refstart, aln.refstop)
101
+ qrySeq = options.qry[aln.qryname].subseq(aln.qrystart, aln.qrystop)
102
+ qrySeq.complement! unless aln.strand
103
+
104
+ # Do we have uncovered bases before the alignment starts? These
105
+ # are detected as gaps between the cursor and the alignment start
106
+ # site in the reference.
107
+ gap_size = aln.refstart - cursor
108
+ overlap_size = 0
109
+ if gap_size > 0
110
+ # There is a stretch of reference bases without an alignment
111
+ gapSeq = options.ref[aln.refname].subseq(cursor, aln.refstart - 1)
112
+ gapSeq
113
+ .chars
114
+ .zip(("-" * gapSeq.length).chars, Range.new(cursor, aln.refstart, true))
115
+ .each{ |ary| yielder << ary }
116
+ cursor = aln.refstart
100
117
  else
101
- "SNP"
102
- end
103
- end.drop_while do |varClass, arr|
104
- if varClass != "INS"
105
- overlap, overlap2 = overlap - arr.length , overlap
118
+ overlap_size = cursor - aln.refstart
106
119
  end
107
- overlap >= 0
108
- end.each do |varClass, arr|
109
- case varClass
110
- when "NOVAR"
111
- if overlap2 > 0
112
- refBase = arr[overlap2].first.upcase
120
+
121
+ # Nucmer records insertions and deletions as a series of positive
122
+ # and negative integers corresponding to how many steps along the
123
+ # alignment until you get to the next gap in the reference
124
+ # sequence (positive integer) or how many steps until the next gap
125
+ # in the query (negative integer). BioMummer exposes these as the
126
+ # alignments 'distances' array.
127
+ # We walk along the Bio::Sequence::NA objects and insert gaps
128
+ # (".") when needed. We store our current position in the
129
+ # reference and the query in a two-element array.
130
+ aln.distances.inject([0,0]) do |mem, distance_to_gap|
131
+ if distance_to_gap > 0
132
+ qrySeq.insert(mem[0] + distance_to_gap - 1, ".")
133
+ [mem[0] + distance_to_gap, mem[1] + distance_to_gap]
113
134
  else
114
- refBase = arr.first.first.upcase
115
- overlap2 = 0
116
- end
117
- puts [refname,
118
- cursor,
119
- ".",
120
- refBase,
121
- "<NON_REF>",
122
- '.',
123
- '.',
124
- "END=#{cursor + arr.length - 1 - overlap2};ALTSCAFF=#{a.queryname};RLEN=#{arr.length};OVERLAP2=#{overlap2}",
125
- "GT:DP:GQ:MIN_DP:PL",
126
- "0:200:200:200:0,800"].join("\t")
127
- cursor += arr.length - overlap2
128
- overlap2 = 0
129
- when "SNP"
130
- arr.each do |snp|
131
- puts [refname,
132
- cursor,
133
- ".",
134
- snp.first.upcase,
135
- [snp.last.upcase, "<NON_REF>"].join(","),
136
- '.',
137
- '.',
138
- "END=#{cursor};ALTSCAFF=#{a.queryname}",
139
- "GT:DP:GQ:MIN_DP:PL",
140
- "1:200:200:200:800,0,800"].join("\t")
141
- cursor += 1
135
+ refSeq.insert(mem[1] - distance_to_gap - 1, ".")
136
+ [mem[0] - distance_to_gap, mem[1] - distance_to_gap]
142
137
  end
143
- # puts [refname,
144
- # cursor,
145
- # ".",
146
- # arr.map{|a| a.first.upcase}.join,
147
- # [arr.map{|a| a.last.upcase}.join, "<NON_REF>"].join(","),
148
- # '.',
149
- # '.',
150
- # "END=#{cursor + arr.length - 1};ALTSCAFF=#{a.queryname}",
151
- # "GT:DP:GQ:MIN_DP:PL",
152
- # "1:200:200:200:800,0,800"].join("\t")
153
- # cursor += arr.length
154
- when "INS"
155
- refBase = options.ref[refname][cursor-2].upcase
156
- puts [refname,
157
- cursor-1,
158
- ".",
159
- refBase,
160
- [(refBase + arr.map{|a| a.last}.join).upcase, "<NON_REF>"].join(","),
161
- '.',
162
- '.',
163
- "END=#{cursor - 1};ALTSCAFF=#{a.queryname};STRAND=#{a.strand}",
164
- "GT:DP:GQ:MIN_DP:PL",
165
- "1:200:200:200:800,0,800"].join("\t")
166
- when "DEL"
167
- puts [refname,
168
- cursor,
169
- ".",
170
- arr.map{|a| a.first.upcase}.join,
171
- ".,<NON_REF>",
172
- '.',
173
- '.',
174
- "END=#{cursor + arr.length - 1};ALTSCAFF=#{a.queryname}",
175
- "GT:DP:GQ:MIN_DP:PL",
176
- "1:200:200:200:800,0,800"].join("\t")
177
- cursor += arr.length
138
+ end
139
+
140
+ if overlap_size < refSeq.length
141
+ i = aln.refstart - 1
142
+ positions = refSeq.chars.map{ |base| base == '.' ? i : i+=1 }
143
+ refSeq
144
+ .chars
145
+ .zip(qrySeq.chars, positions)
146
+ .drop(overlap_size)
147
+ .each{ |ary| yielder << ary }
178
148
  end
179
149
  end
180
150
  end
151
+
152
+ cursor = 0
153
+ stolen_zipped = [nil, nil, nil]
154
+ chunked = clusters.find_all do |refBase, qryBase, position|
155
+ if position > cursor || (position == cursor && refBase == ".")
156
+ cursor = position
157
+ true
158
+ else
159
+ false
160
+ end
161
+ end.chunk do |refBase, qryBase, position|
162
+ # Classify runs of identiry, insertions, deletions or SNPs.
163
+ if refBase == qryBase
164
+ :NO_VARIATION
165
+ elsif qryBase == "-"
166
+ :NO_COVERAGE
167
+ elsif qryBase == '.'
168
+ :DELETION
169
+ elsif refBase == '.'
170
+ :INSERTION
171
+ else
172
+ :SNP
173
+ end
174
+ end.to_a
175
+
176
+ chunked.each_cons(2) do |current, upcoming|
177
+ cur_class = current.first
178
+ cur_zipped = current.last
179
+ nxt_class = upcoming.first
180
+
181
+ if nxt_class == :INSERTION || nxt_class == :DELETION
182
+ stolen_zipped = cur_zipped.pop
183
+ next if cur_zipped.length == 0
184
+ end
185
+
186
+ case cur_class
187
+ when :NO_COVERAGE
188
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{cur_zipped.first.first.upcase}\t<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t0:0:0:0,0"
189
+ when :NO_VARIATION
190
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{cur_zipped.first.first.upcase}\t<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t0:1:200:0,800"
191
+ when :SNP
192
+ cur_zipped.each do |refBase, qryBase, position|
193
+ puts "#{refname}\t#{position}\t.\t#{refBase.upcase}\t#{qryBase.upcase},<NON_REF>\t.\t.\tEND=#{position}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
194
+ end
195
+ when :INSERTION
196
+ refSeq, qrySeq = cur_zipped.unshift(stolen_zipped).transpose.map{|seq| seq.reject{|c|c=='.'}.join.upcase }
197
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{refSeq}\t#{qrySeq},<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
198
+ when :DELETION
199
+ refSeq, qrySeq = cur_zipped.unshift(stolen_zipped).transpose.map{|seq| seq.reject{|c|c=='.'}.join.upcase }
200
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{refSeq}\t#{qrySeq},<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
201
+ end
202
+ cursor = [cursor, cur_zipped.last.last].max
203
+ end
204
+
205
+ # We iterated over the entries in a window of length two, outputting
206
+ # the first in the pair. This leaves the last entry unobserved, so
207
+ # we take special care to output it here
208
+ cur_class, cur_zipped = chunked.last
209
+ case cur_class
210
+ when :NO_COVERAGE
211
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{cur_zipped.first.first.upcase}\t<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t0:0:0:0,0"
212
+ when :NO_VARIATION
213
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{cur_zipped.first.first.upcase}\t<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t0:1:200:0,800"
214
+ when :SNP
215
+ cur_zipped.each do |refBase, qryBase, position|
216
+ puts "#{refname}\t#{position}\t.\t#{refBase.upcase}\t#{qryBase.upcase},<NON_REF>\t.\t.\tEND=#{position}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
217
+ end
218
+ when :INSERTION
219
+ refSeq, qrySeq = cur_zipped.unshift(stolen_zipped).transpose.map{|seq| seq.reject{|c|c=='.'}.join.upcase }
220
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{refSeq}\t#{qrySeq},<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
221
+ when :DELETION
222
+ refSeq, qrySeq = cur_zipped.unshift(stolen_zipped).transpose.map{|seq| seq.reject{|c|c=='.'}.join.upcase }
223
+ puts "#{refname}\t#{cur_zipped.first.last}\t.\t#{refSeq}\t#{qrySeq},<NON_REF>\t.\t.\tEND=#{cur_zipped.last.last}\tGT:DP:GQ:PL\t1:1:200:800,0,800"
224
+ end
225
+
226
+ # It's likely that the alignments don't run to the end of the
227
+ # scaffold, so we fill in to the scaffold end with the 'no coverage'
228
+ # entry
229
+ remaining = options.ref[refname].length - cursor
230
+ if remaining > 0
231
+ puts "#{refname}\t#{cursor+1}\t.\t#{options.ref[refname][cursor].upcase}\t<NON_REF>\t.\t.\tEND=#{options.ref[refname].length}\tGT:DP:GQ:PL\t0:0:0:0,0"
232
+ end
181
233
  end
@@ -3,15 +3,15 @@ require 'stringio'
3
3
  module BioMummer
4
4
 
5
5
  class Alignment
6
- attr_accessor :refname, :queryname, :refstart, :refstop, :querystart, :querystop, :strand, :distances
6
+ attr_accessor :refname, :qryname, :refstart, :refstop, :qrystart, :qrystop, :strand, :distances
7
7
 
8
- def initialize(refname, queryname, refstart, refstop, querystart, querystop, strand, distances)
8
+ def initialize(refname, qryname, refstart, refstop, qrystart, qrystop, strand, distances)
9
9
  @refname = refname
10
- @queryname = queryname
10
+ @qryname = qryname
11
11
  @refstart = refstart
12
12
  @refstop = refstop
13
- @querystart = querystart
14
- @querystop = querystop
13
+ @qrystart = qrystart
14
+ @qrystop = qrystop
15
15
  @strand = strand
16
16
  @distances = distances
17
17
  end
@@ -35,13 +35,13 @@ module BioMummer
35
35
 
36
36
  def ref_to_query(ref_position)
37
37
  position_in_alignment = ref_position - refstart + 1
38
- querypos = nil
38
+ qrypos = nil
39
39
  if position_in_alignment >= deltas.length
40
- querypos = @querystart - 1 + position_in_alignment + deltas.last
40
+ qrypos = @qrystart - 1 + position_in_alignment + deltas.last
41
41
  elsif deltas[position_in_alignment-1]
42
- querypos = @querystart - 1 + position_in_alignment + deltas[position_in_alignment-1]
42
+ qrypos = @qrystart - 1 + position_in_alignment + deltas[position_in_alignment-1]
43
43
  end
44
- !@strand && querypos ? @querystart + @querystop - querypos : querypos
44
+ !@strand && qrypos ? @qrystart + @qrystop - qrypos : qrypos
45
45
  end
46
46
  end
47
47
 
@@ -64,21 +64,21 @@ module BioMummer
64
64
 
65
65
  def parse(string)
66
66
  string.split("\n").slice_before(/^>/).flat_map do |block|
67
- refname, queryname = block.shift.match(/>(.*) (.*) \d+ \d+/).captures
67
+ refname, qryname = block.shift.match(/>(.*) (.*) \d+ \d+/).captures
68
68
  block.slice_before(/\d+ \d+ \d+ \d+/).map do |alignment|
69
- refstart, refstop, querystart, querystop = alignment
69
+ refstart, refstop, qrystart, qrystop = alignment
70
70
  .shift
71
71
  .match(/(\d+) (\d+) (\d+) (\d+) /)
72
72
  .captures
73
73
  .map{ |c| c.to_i }
74
74
  alignment.pop
75
75
  Alignment.new(refname,
76
- queryname,
76
+ qryname,
77
77
  refstart,
78
78
  refstop,
79
- [querystart, querystop].min,
80
- [querystart, querystop].max,
81
- querystart < querystop,
79
+ [qrystart, qrystop].min,
80
+ [qrystart, qrystop].max,
81
+ qrystart < qrystop,
82
82
  alignment.map{ |i| i.to_i })
83
83
  end
84
84
  end
@@ -110,13 +110,13 @@ module BioMummer
110
110
  a.refname == refname && a.refstart <= startpos && a.refstop >= endpos
111
111
  end
112
112
  if a
113
- queryname = a.queryname
114
- querystart = a.ref_to_query(startpos)
115
- querystop = a.ref_to_query(endpos)
116
- if querystart.nil? || querystop.nil?
113
+ qryname = a.qryname
114
+ qrystart = a.ref_to_query(startpos)
115
+ qrystop = a.ref_to_query(endpos)
116
+ if qrystart.nil? || qrystop.nil?
117
117
  return nil
118
118
  else
119
- return [queryname, querystart, querystop, a.strand]
119
+ return [qryname, qrystart, qrystop, a.strand]
120
120
  end
121
121
  else
122
122
  return nil
@@ -15,8 +15,8 @@ class TestBioMummer < MiniTest::Test
15
15
  assert_kind_of Fixnum, @report.alignments.first.refstart
16
16
  assert_equal 1, @report.alignments.first.refstart
17
17
  assert_equal 2435, @report.alignments.first.refstop
18
- assert_equal 1, @report.alignments.first.querystart
19
- assert_equal 2435, @report.alignments.first.querystop
18
+ assert_equal 1, @report.alignments.first.qrystart
19
+ assert_equal 2435, @report.alignments.first.qrystop
20
20
  end
21
21
 
22
22
  should "do basic coordinate transforms on the positive strand" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bio-mummer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - robsyme
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-23 00:00:00.000000000 Z
11
+ date: 2015-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: shoulda
@@ -105,7 +105,6 @@ extra_rdoc_files:
105
105
  - README.md
106
106
  - README.rdoc
107
107
  files:
108
- - .document
109
108
  - .travis.yml
110
109
  - Gemfile
111
110
  - LICENSE.txt
data/.document DELETED
@@ -1,5 +0,0 @@
1
- lib/**/*.rb
2
- bin/*
3
- -
4
- features/**/*.feature
5
- LICENSE.txt