HTSeq 2.1.2__cp313-cp313-macosx_10_15_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
HTSeq/scripts/qa.py ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env python
2
+
3
+ # HTSeq_QA.py
4
+ #
5
+ # (c) Simon Anders, European Molecular Biology Laboratory, 2010
6
+ # released under GNU General Public License
7
+
8
+ import sys
9
+ import os.path
10
+ import argparse
11
+ from itertools import islice
12
+ import numpy as np
13
+ import HTSeq
14
+
15
+ try:
16
+ import matplotlib
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.pyplot import Normalize
19
+ except ImportError:
20
+ sys.stderr.write("htseq-qa needs 'matplotlib >= 1.5'")
21
+ raise
22
+
23
+
24
+ def get_read_length(readfile, isAlnmntFile):
25
+ readlen = 0
26
+ if isAlnmntFile:
27
+ reads = (a.read for a in readfile)
28
+ else:
29
+ reads = readfile
30
+ for r in islice(reads, 10000):
31
+ if len(r) > readlen:
32
+ readlen = len(r)
33
+
34
+ return readlen
35
+
36
+
37
+ def compute_quality(
38
+ readfilename,
39
+ file_type,
40
+ nosplit,
41
+ readlen,
42
+ max_qual,
43
+ gamma,
44
+ primary_only=False,
45
+ max_records=-1,
46
+ ):
47
+
48
+ if file_type in ("sam", "bam"):
49
+ readfile = HTSeq.BAM_Reader(readfilename)
50
+ isAlnmntFile = True
51
+ elif file_type == "solexa-export":
52
+ readfile = HTSeq.SolexaExportReader(readfilename)
53
+ isAlnmntFile = True
54
+ elif file_type == "fastq":
55
+ readfile = HTSeq.FastqReader(readfilename)
56
+ isAlnmntFile = False
57
+ elif file_type == "solexa-fastq":
58
+ readfile = HTSeq.FastqReader(readfilename, "solexa")
59
+ isAlnmntFile = False
60
+ else:
61
+ raise ValueError('File format not recognized: {:}'.format(file_type))
62
+
63
+ twoColumns = isAlnmntFile and (not nosplit)
64
+
65
+ if readlen is None:
66
+ readlen = get_read_length(readfile, isAlnmntFile)
67
+
68
+ # Initialize count arrays
69
+ base_arr_U = np.zeros((readlen, 5), np.int64)
70
+ qual_arr_U = np.zeros((readlen, max_qual+1), np.int64)
71
+ if twoColumns:
72
+ base_arr_A = np.zeros((readlen, 5), np.int64)
73
+ qual_arr_A = np.zeros((readlen, max_qual+1), np.int64)
74
+
75
+ # Main counting loop
76
+ i = 0
77
+ try:
78
+ for a in readfile:
79
+ if isAlnmntFile:
80
+ r = a.read
81
+ else:
82
+ r = a
83
+
84
+ # Exclude non-primary alignments if requested
85
+ if isAlnmntFile and primary_only:
86
+ if a.aligned and a.not_primary_alignment:
87
+ continue
88
+
89
+ if twoColumns and isAlnmntFile and a.aligned:
90
+ r.add_bases_to_count_array(base_arr_A)
91
+ r.add_qual_to_count_array(qual_arr_A)
92
+ else:
93
+ r.add_bases_to_count_array(base_arr_U)
94
+ r.add_qual_to_count_array(qual_arr_U)
95
+
96
+ i += 1
97
+
98
+ if i == max_records:
99
+ break
100
+
101
+ if (i % 200000) == 0:
102
+ if (not isAlnmntFile) or primary_only:
103
+ print(i, "reads processed")
104
+ else:
105
+ print(i, "alignments processed")
106
+
107
+ except:
108
+ sys.stderr.write("Error occured in: %s\n" %
109
+ readfile.get_line_number_string())
110
+ raise
111
+
112
+ if (not isAlnmntFile) or primary_only:
113
+ print(i, "reads processed")
114
+ else:
115
+ print(i, "alignments processed")
116
+
117
+ # Normalize result
118
+ def norm_by_pos(arr):
119
+ arr = np.array(arr, np.float64)
120
+ arr_n = (arr.T / arr.sum(1)).T
121
+ arr_n[arr == 0] = 0
122
+ return arr_n
123
+
124
+ def norm_by_start(arr):
125
+ arr = np.array(arr, np.float64)
126
+ arr_n = (arr.T / arr.sum(1)[0]).T
127
+ arr_n[arr == 0] = 0
128
+ return arr_n
129
+
130
+ result = {
131
+ 'isAlnmntFile': isAlnmntFile,
132
+ 'readlen': readlen,
133
+ 'twoColumns': twoColumns,
134
+ 'base_arr_U_n': norm_by_pos(base_arr_U),
135
+ 'qual_arr_U_n': norm_by_start(qual_arr_U),
136
+ 'nreads_U': base_arr_U[0, :].sum(),
137
+ }
138
+
139
+ if twoColumns:
140
+ result['base_arr_A_n'] = norm_by_pos(base_arr_A)
141
+ result['qual_arr_A_n'] = norm_by_start(qual_arr_A)
142
+ result['nreads_A'] = base_arr_A[0, :].sum()
143
+
144
+ return result
145
+
146
+
147
+ def plot(
148
+ result,
149
+ readfilename,
150
+ outfile,
151
+ max_qual,
152
+ gamma,
153
+ primary_only=False,
154
+ ):
155
+
156
+ def plot_bases(arr, ax):
157
+ xg = np.arange(readlen)
158
+ ax.plot(xg, arr[:, 0], marker='.', color='red')
159
+ ax.plot(xg, arr[:, 1], marker='.', color='darkgreen')
160
+ ax.plot(xg, arr[:, 2], marker='.', color='lightgreen')
161
+ ax.plot(xg, arr[:, 3], marker='.', color='orange')
162
+ ax.plot(xg, arr[:, 4], marker='.', color='grey')
163
+ ax.set_xlim(0, readlen-1)
164
+ ax.set_ylim(0, 1)
165
+ ax.text(readlen*.70, .9, "A", color="red")
166
+ ax.text(readlen*.75, .9, "C", color="darkgreen")
167
+ ax.text(readlen*.80, .9, "G", color="lightgreen")
168
+ ax.text(readlen*.85, .9, "T", color="orange")
169
+ ax.text(readlen*.90, .9, "N", color="grey")
170
+
171
+ if outfile is None:
172
+ outfilename = os.path.basename(readfilename) + ".pdf"
173
+ else:
174
+ outfilename = outfile
175
+
176
+ isAlnmntFile = result['isAlnmntFile']
177
+ readlen = result['readlen']
178
+ twoColumns = result['twoColumns']
179
+
180
+ base_arr_U_n = result['base_arr_U_n']
181
+ qual_arr_U_n = result['qual_arr_U_n']
182
+ nreads_U = result['nreads_U']
183
+
184
+ if twoColumns:
185
+ base_arr_A_n = result['base_arr_A_n']
186
+ qual_arr_A_n = result['qual_arr_A_n']
187
+ nreads_A = result['nreads_A']
188
+
189
+ cur_backend = matplotlib.get_backend()
190
+
191
+ try:
192
+ matplotlib.use('PDF')
193
+
194
+ fig = plt.figure()
195
+ fig.subplots_adjust(top=.85)
196
+ fig.suptitle(os.path.basename(readfilename), fontweight='bold')
197
+
198
+ if twoColumns:
199
+
200
+ ax = fig.add_subplot(221)
201
+ plot_bases(base_arr_U_n, ax)
202
+ ax.set_ylabel("proportion of base")
203
+ ax.set_title(
204
+ "non-aligned reads\n{:.0%} ({:.4f} million)".format(
205
+ 1.0 * nreads_U / (nreads_U+nreads_A),
206
+ 1.0 * nreads_U / 1e6,
207
+ ))
208
+
209
+ ax2 = fig.add_subplot(222)
210
+ plot_bases(base_arr_A_n, ax2)
211
+ ax2.set_title(
212
+ "{:}\n{:.0%} ({:.4f} million)".format(
213
+ 'aligned reads' if primary_only else 'alignments',
214
+ 1.0 * nreads_A / (nreads_U+nreads_A),
215
+ 1.0 * nreads_A / 1e6,
216
+ ))
217
+
218
+ ax3 = fig.add_subplot(223)
219
+ ax3.pcolor(
220
+ qual_arr_U_n.T ** gamma,
221
+ cmap=plt.cm.Greens,
222
+ norm=Normalize(0, 1))
223
+ ax3.set_xlim(0, readlen-1)
224
+ ax3.set_ylim(0, max_qual+1)
225
+ ax3.set_xlabel("position in read")
226
+ ax3.set_ylabel("base-call quality score")
227
+
228
+ ax4 = fig.add_subplot(224)
229
+ ax4.pcolor(
230
+ qual_arr_A_n.T ** gamma,
231
+ cmap=plt.cm.Greens,
232
+ norm=Normalize(0, 1))
233
+ ax4.set_xlim(0, readlen-1)
234
+ ax4.set_ylim(0, max_qual+1)
235
+ ax4.set_xlabel("position in read")
236
+
237
+ else:
238
+
239
+ ax = fig.add_subplot(211)
240
+ plot_bases(base_arr_U_n, ax)
241
+ ax.set_ylabel("proportion of base")
242
+ ax.set_title("{:.3f} million {:}".format(
243
+ 1.0 * nreads_U / 1e6,
244
+ 'reads' if (not isAlnmntFile) or primary_only else 'alignments',
245
+ ))
246
+
247
+ ax2 = fig.add_subplot(212)
248
+ ax2.pcolor(
249
+ qual_arr_U_n.T ** gamma,
250
+ cmap=plt.cm.Greens,
251
+ norm=Normalize(0, 1))
252
+ ax2.set_xlim(0, readlen-1)
253
+ ax2.set_ylim(0, max_qual+1)
254
+ ax2.set_xlabel("position in read")
255
+ ax2.set_ylabel("base-call quality score")
256
+
257
+ fig.savefig(outfilename)
258
+
259
+ finally:
260
+ matplotlib.use(cur_backend)
261
+
262
+
263
+ def main():
264
+
265
+ # **** Parse command line ****
266
+ pa = argparse.ArgumentParser(
267
+ description=
268
+ "This script take a file with high-throughput sequencing reads " +
269
+ "(supported formats: SAM, Solexa _export.txt, FASTQ, Solexa " +
270
+ "_sequence.txt) and performs a simply quality assessment by " +
271
+ "producing plots showing the distribution of called bases and " +
272
+ "base-call quality scores by position within the reads. The " +
273
+ "plots are output as a PDF file.",
274
+ )
275
+ pa.add_argument(
276
+ 'readfilename',
277
+ help='The file to count reads in (SAM/BAM or Fastq)',
278
+ )
279
+ pa.add_argument(
280
+ "-t", "--type", type=str, dest="type",
281
+ choices=("sam", "bam", "solexa-export", "fastq", "solexa-fastq"),
282
+ default="sam", help="type of read_file (one of: sam [default], bam, " +
283
+ "solexa-export, fastq, solexa-fastq)")
284
+ pa.add_argument(
285
+ "-o", "--outfile", type=str, dest="outfile",
286
+ help="output filename (default is <read_file>.pdf)")
287
+ pa.add_argument(
288
+ "-r", "--readlength", type=int, dest="readlen",
289
+ help="the maximum read length (when not specified, the script guesses from the file")
290
+ pa.add_argument(
291
+ "-g", "--gamma", type=float, dest="gamma",
292
+ default=0.3,
293
+ help="the gamma factor for the contrast adjustment of the quality score plot")
294
+ pa.add_argument(
295
+ "-n", "--nosplit", action="store_true", dest="nosplit",
296
+ help="do not split reads in unaligned and aligned ones")
297
+ pa.add_argument(
298
+ "-m", "--maxqual", type=int, dest="maxqual", default=41,
299
+ help="the maximum quality score that appears in the data (default: 41)")
300
+ pa.add_argument(
301
+ '--primary-only', action='store_true',
302
+ help="For SAM/BAM input files, ignore alignments that are not primary. " +
303
+ "This only affects 'multimapper' reads that align to several regions " +
304
+ "in the genome. By choosing this option, each read will only count as " +
305
+ "one; without this option, each of its alignments counts as one."
306
+ )
307
+ pa.add_argument(
308
+ '--max-records', type=int, default=-1, dest='max_records',
309
+ help="Limit the analysis to the first N reads/alignments."
310
+ )
311
+
312
+ args = pa.parse_args()
313
+
314
+ result = compute_quality(
315
+ args.readfilename,
316
+ args.type,
317
+ args.nosplit,
318
+ args.readlen,
319
+ args.maxqual,
320
+ args.gamma,
321
+ args.primary_only,
322
+ args.max_records,
323
+ )
324
+
325
+ plot(
326
+ result,
327
+ args.readfilename,
328
+ args.outfile,
329
+ args.maxqual,
330
+ args.gamma,
331
+ args.primary_only,
332
+ )
333
+
334
+
335
+ if __name__ == "__main__":
336
+ main()
HTSeq/scripts/utils.py ADDED
@@ -0,0 +1,372 @@
1
+ import sys
2
+ import numpy as np
3
+
4
+
5
+ class UnknownChrom(Exception):
6
+ pass
7
+
8
+
9
+ def my_showwarning(message, category, filename, lineno=None, file=None,
10
+ line=None):
11
+ sys.stderr.write("Warning: %s\n" % message)
12
+
13
+
14
+ def invert_strand(iv):
15
+ iv2 = iv.copy()
16
+ if iv2.strand == "+":
17
+ iv2.strand = "-"
18
+ elif iv2.strand == "-":
19
+ iv2.strand = "+"
20
+ else:
21
+ raise ValueError("Illegal strand")
22
+ return iv2
23
+
24
+
25
+ def _merge_counts(
26
+ results,
27
+ attributes,
28
+ additional_attributes,
29
+ sparse=False,
30
+ dtype=np.float32,
31
+ ):
32
+ barcodes = 'cell_barcodes' in results
33
+
34
+ if barcodes:
35
+ cbs = results['cell_barcodes']
36
+ counts = results['counts']
37
+
38
+ feature_attr = sorted(attributes.keys())
39
+ other_features = [
40
+ ('__no_feature', 'empty'),
41
+ ('__ambiguous', 'ambiguous'),
42
+ ('__too_low_aQual', 'lowqual'),
43
+ ('__not_aligned', 'notaligned'),
44
+ ('__alignment_not_unique', 'nonunique'),
45
+ ]
46
+
47
+ fea_names = [fea for fea in feature_attr] + [fea[0] for fea in other_features]
48
+ L = len(fea_names)
49
+ if barcodes:
50
+ n = len(cbs)
51
+ else:
52
+ n = len(results)
53
+ if not sparse:
54
+ table = np.zeros(
55
+ (n, L),
56
+ dtype=dtype,
57
+ )
58
+ else:
59
+ from scipy.sparse import lil_matrix
60
+ table = lil_matrix((n, L), dtype=dtype)
61
+
62
+ if not barcodes:
63
+ fea_ids = [fea for fea in feature_attr] + [fea[1] for fea in other_features]
64
+ for j, r in enumerate(results):
65
+ for i, fn in enumerate(fea_ids):
66
+ if i < len(feature_attr):
67
+ countji = r['counts'][fn]
68
+ else:
69
+ countji = r[fn]
70
+ if countji > 0:
71
+ table[j, i] = countji
72
+ else:
73
+ for j, cb in enumerate(cbs):
74
+ for i, fn in enumerate(fea_names):
75
+ countji = counts[cb][fn]
76
+ if countji > 0:
77
+ table[j, i] = countji
78
+
79
+ if sparse:
80
+ table = table.tocsr()
81
+
82
+ feature_metadata = {
83
+ 'id': fea_names,
84
+ }
85
+ for iadd, attr in enumerate(additional_attributes):
86
+ feature_metadata[attr] = [attributes[fn][iadd] for fn in feature_attr]
87
+
88
+ return {
89
+ 'feature_metadata': feature_metadata,
90
+ 'table': table,
91
+ }
92
+
93
+
94
+ def _count_results_to_tsv(
95
+ results,
96
+ samples_name,
97
+ attributes,
98
+ additional_attributes,
99
+ output_filename,
100
+ output_delimiter,
101
+ output_append=False,
102
+ add_tsv_header=False
103
+ ):
104
+
105
+ barcodes = 'cell_barcodes' in results
106
+
107
+ pad = ['' for attr in additional_attributes]
108
+
109
+ if barcodes:
110
+ cbs = results['cell_barcodes']
111
+ counts = results['counts']
112
+
113
+ # Print or write header
114
+ fields = [''] + pad + cbs
115
+ line = output_delimiter.join(fields)
116
+ if output_filename == '':
117
+ print(line)
118
+ else:
119
+ with open(output_filename, 'w') as f:
120
+ f.write(line)
121
+ f.write('\n')
122
+
123
+ elif add_tsv_header:
124
+ # Write the header.
125
+ # Only get here if we don't have cell barcodes, i.e. this is not called by htseq-count-barcode,
126
+ # and user wants the tsv header
127
+ file_header = output_delimiter.join([''] + pad + samples_name)
128
+
129
+ if output_filename == '':
130
+ print(file_header)
131
+ else:
132
+ # If append to existing file, then open as a
133
+ file_open_opt = 'a' if output_append else 'w'
134
+
135
+ with open(output_filename, file_open_opt) as f:
136
+ f.write(file_header)
137
+ f.write('\n')
138
+
139
+ # Each feature is a row with feature id, additional attrs, and counts
140
+ feature_attr = sorted(attributes.keys())
141
+ for ifn, fn in enumerate(feature_attr):
142
+ if not barcodes:
143
+ fields = [fn] + attributes[fn] + [str(r['counts'][fn]) for r in results]
144
+ else:
145
+ fields = [fn] + attributes[fn] + [str(counts[cb][fn]) for cb in cbs]
146
+
147
+ line = output_delimiter.join(fields)
148
+ if output_filename == '':
149
+ print(line)
150
+ else:
151
+ omode = 'a' if output_append or (ifn > 0) or barcodes or add_tsv_header else 'w'
152
+ with open(output_filename, omode) as f:
153
+ f.write(line)
154
+ f.write('\n')
155
+
156
+ # Add other features (unmapped, etc.)
157
+ other_features = [
158
+ ('__no_feature', 'empty'),
159
+ ('__ambiguous', 'ambiguous'),
160
+ ('__too_low_aQual', 'lowqual'),
161
+ ('__not_aligned', 'notaligned'),
162
+ ('__alignment_not_unique', 'nonunique'),
163
+ ]
164
+ for title, fn in other_features:
165
+ if not barcodes:
166
+ fields = [title] + pad + [str(r[fn]) for r in results]
167
+ else:
168
+ fields = [title] + pad + [str(counts[cb][title]) for cb in cbs]
169
+ line = output_delimiter.join(fields)
170
+ if output_filename == '':
171
+ print(line)
172
+ else:
173
+ with open(output_filename, 'a') as f:
174
+ f.write(line)
175
+ f.write('\n')
176
+
177
+
178
+ def _count_table_to_mtx(
179
+ filename,
180
+ table,
181
+ feature_metadata,
182
+ samples,
183
+ ):
184
+ if not str(filename).endswith('.mtx'):
185
+ raise ValueError('Matrix Marker filename should end with ".mtx"')
186
+
187
+ try:
188
+ from scipy.io import mmwrite
189
+ except ImportError:
190
+ raise ImportError('Install scipy for mtx support')
191
+
192
+ filename_pfx = str(filename)[:-4]
193
+ filename_feature_meta = filename_pfx+'_features.tsv'
194
+ filename_samples = filename_pfx+'_samples.tsv'
195
+
196
+ # Write main matrix (features as columns)
197
+ mmwrite(
198
+ filename,
199
+ table,
200
+ )
201
+
202
+ # Write input filenames
203
+ with open(filename_samples, 'wt') as fout:
204
+ for fn in samples:
205
+ fout.write(fn+'\n')
206
+
207
+ # Write feature metadata (ids and additional attributes)
208
+ with open(filename_feature_meta, 'wt') as fout:
209
+ nkeys = len(feature_metadata)
210
+ for ik, key in enumerate(feature_metadata):
211
+ if ik != nkeys - 1:
212
+ fout.write(key+'\t')
213
+ else:
214
+ fout.write(key+'\n')
215
+ nfeatures = len(feature_metadata[key])
216
+ for i in range(nfeatures):
217
+ for ik, key in enumerate(feature_metadata):
218
+ if ik != nkeys - 1:
219
+ fout.write(feature_metadata[key][i]+'\t')
220
+ else:
221
+ fout.write(feature_metadata[key][i]+'\n')
222
+
223
+
224
+ def _count_table_to_h5ad(
225
+ filename,
226
+ table,
227
+ feature_metadata,
228
+ samples,
229
+ ):
230
+ try:
231
+ import anndata
232
+ except ImportError:
233
+ raise ImportError('Install the anndata package for h5ad support')
234
+
235
+ # If they have anndata, they have scipy and pandas too
236
+ import pandas as pd
237
+
238
+ # We don't have additional attribute (e.g. gene name) for htseq specific features like __no_feature.
239
+ # Hence the trick is to convert the array to series so the value for htseq specific features like __no_feature
240
+ # column is set NaN.
241
+ # See: https://stackoverflow.com/questions/19736080/creating-dataframe-from-a-dictionary-where-entries-have-different-lengths
242
+ feature_metadata = pd.DataFrame(dict([(k, pd.Series(v)) for k, v in feature_metadata.items()]))
243
+ feature_metadata.set_index(feature_metadata.columns[0], inplace=True)
244
+
245
+ adata = anndata.AnnData(
246
+ X=table,
247
+ obs=pd.DataFrame([], index=samples),
248
+ var=feature_metadata,
249
+ )
250
+ adata.write_h5ad(filename)
251
+
252
+
253
+ def _count_table_to_loom(
254
+ filename,
255
+ table,
256
+ feature_metadata,
257
+ samples,
258
+ ):
259
+
260
+ try:
261
+ import loompy
262
+ except ImportError:
263
+ raise ImportError('Install the loompy package for loom support')
264
+
265
+ # Loom uses features as rows...
266
+ layers = {'': table.T}
267
+ row_attrs = feature_metadata
268
+ col_attrs = {'_index': samples}
269
+ loompy.create(
270
+ filename,
271
+ layers=layers,
272
+ row_attrs=row_attrs,
273
+ col_attrs=col_attrs,
274
+ )
275
+
276
+
277
+ def _write_output(
278
+ results,
279
+ samples,
280
+ attributes,
281
+ additional_attributes,
282
+ output_filename,
283
+ output_delimiter,
284
+ output_append,
285
+ sparse=False,
286
+ dtype=np.float32,
287
+ add_tsv_header=False
288
+ ):
289
+
290
+ """
291
+ Export the gene counts as tsv/csv, mtx, loom, h5ad files.
292
+ Note, need to update the parameter documentations.
293
+
294
+ Parameters
295
+ ----------
296
+ results : list
297
+ List of dictionaries with each element representing the counts for an input BAM file.
298
+ Note, the list is in order of the samples parameter. So the first element in the list corresponds to
299
+ the first file in samples parameter.
300
+ samples : list
301
+ List of input BAM files.
302
+
303
+ """
304
+
305
+ # Write output to stdout or TSV/CSV
306
+ if output_filename == '':
307
+ _count_results_to_tsv(
308
+ results,
309
+ samples,
310
+ attributes,
311
+ additional_attributes,
312
+ output_filename,
313
+ output_delimiter,
314
+ output_append=False,
315
+ add_tsv_header=add_tsv_header
316
+ )
317
+ return
318
+
319
+ # Get file extension/format
320
+ output_sfx = output_filename.split('.')[-1].lower()
321
+
322
+ if output_sfx in ('csv', 'tsv'):
323
+ _count_results_to_tsv(
324
+ results,
325
+ samples,
326
+ attributes,
327
+ additional_attributes,
328
+ output_filename,
329
+ output_delimiter,
330
+ output_append,
331
+ add_tsv_header=add_tsv_header
332
+ )
333
+ return
334
+
335
+ # Make unified object of counts and feature metadata
336
+ output_dict = _merge_counts(
337
+ results,
338
+ attributes,
339
+ additional_attributes,
340
+ sparse=sparse,
341
+ dtype=dtype,
342
+ )
343
+
344
+ if output_sfx == 'mtx':
345
+ _count_table_to_mtx(
346
+ output_filename,
347
+ output_dict['table'],
348
+ output_dict['feature_metadata'],
349
+ samples,
350
+ )
351
+ return
352
+
353
+ if output_sfx == 'loom':
354
+ _count_table_to_loom(
355
+ output_filename,
356
+ output_dict['table'],
357
+ output_dict['feature_metadata'],
358
+ samples,
359
+ )
360
+ return
361
+
362
+ if output_sfx == 'h5ad':
363
+ _count_table_to_h5ad(
364
+ output_filename,
365
+ output_dict['table'],
366
+ output_dict['feature_metadata'],
367
+ samples,
368
+ )
369
+ return
370
+
371
+ raise ValueError(
372
+ f'Format not recognized for output count file: {output_sfx}')