isiteuk 0.0.2__py3-none-any.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.
isiteuk/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = '0.0.2'
isiteuk/main.py ADDED
@@ -0,0 +1,251 @@
1
+ ###############################################################################
2
+ #
3
+ # Copyright (C) 2024 Ben Woodcroft
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+ ###############################################################################
19
+
20
+ import argparse
21
+ import logging
22
+ import sys
23
+ import os
24
+ import polars as pl
25
+
26
+ import isiteuk
27
+ from isiteuk.metapackage import Metapackage as IsiteukMetapackage, DATA_ENVIRONMENT_VARIABLE
28
+
29
+ import singlem.metapackage
30
+ import singlem.pipe
31
+ import singlem.taxonomy
32
+
33
+
34
+ def process_cmd(args):
35
+ logging.info("isiteuk version {}".format(isiteuk.__version__))
36
+
37
+ if args.metapackage:
38
+ metapackage_object = singlem.metapackage.Metapackage.acquire(args.metapackage)
39
+ else:
40
+ metapackage_object = IsiteukMetapackage.acquire_default()
41
+
42
+ marker_to_target_domains = {}
43
+ domain_total_counts = {}
44
+ for pkg in metapackage_object.singlem_packages:
45
+ name = pkg.base_directory()
46
+ target_domains = pkg.target_domains()
47
+ marker_to_target_domains[name] = target_domains
48
+ for domain in target_domains:
49
+ if domain not in domain_total_counts:
50
+ domain_total_counts[domain] = 0
51
+ domain_total_counts[domain] += 1
52
+ log_str = []
53
+ for domain, count in domain_total_counts.items():
54
+ log_str.append(f"{domain}: {count}")
55
+ logging.info("Number of markers for each domain: " + ', '.join(log_str))
56
+
57
+ if args.genomes:
58
+ genomes = args.genomes
59
+ else:
60
+ with open(args.genome_list) as f:
61
+ genomes = [line.strip() for line in f.readlines()]
62
+ logging.info(f"Processing {len(genomes)} genomes")
63
+
64
+ if args.continuing:
65
+ completed_genomes = set()
66
+ if not os.path.exists(args.output):
67
+ logging.error(f"Output file {args.output} does not exist. Cannot continue.")
68
+ sys.exit(1)
69
+ with open(args.output) as f:
70
+ for line in f:
71
+ if line.split("\t")[1] != "domain":
72
+ completed_genomes.add(line.split("\t")[0])
73
+ logging.info(f"Skipping {len(completed_genomes)} genomes already completed")
74
+ genomes = [g for g in genomes if os.path.splitext(os.path.basename(g))[0] not in completed_genomes]
75
+
76
+ genome_chunks = [genomes[i:i + args.chunk_size] for i in range(0, len(genomes), args.chunk_size)]
77
+
78
+ for i, genome_chunk in enumerate(genome_chunks):
79
+ logging.info(f"Processing chunk {i+1} of {len(genome_chunks)}")
80
+ first_line = (i == 0) and (not args.continuing)
81
+
82
+ logging.info("Running SingleM pipe to get an OTU table")
83
+ otu_table = singlem.pipe.SearchPipe().run_to_otu_table(
84
+ genomes=genome_chunk,
85
+ metapackage_object=metapackage_object,
86
+ threads=args.threads,
87
+ min_orf_length=singlem.pipe.SearchPipe.DEFAULT_GENOME_MIN_ORF_LENGTH,
88
+ diamond_prefilter=True,
89
+ assignment_method=singlem.taxonomy.DIAMOND_ASSIGNMENT_METHOD,
90
+ )
91
+
92
+ gene_counts = []
93
+ for otu in otu_table:
94
+ tax_split = otu.taxonomy.split('; ')
95
+ if len(tax_split) == 1:
96
+ continue
97
+ domain = tax_split[1]
98
+ gene_counts.append((otu.sample_name, otu.marker, domain))
99
+
100
+ if not gene_counts:
101
+ logging.info(f"No reads found for chunk {i+1}, writing empty results to mark genomes as completed")
102
+ open_type = 'w' if first_line else 'a'
103
+ with open(args.output, open_type) as f:
104
+ if first_line:
105
+ print('genome\tdomain\tnum_in_target_domain\tnum_not_in_target_domain', file=f)
106
+ for genome in genome_chunk:
107
+ genome_name = os.path.splitext(os.path.basename(genome))[0]
108
+ print(f'{genome_name}\t\t0\t0', file=f)
109
+ continue
110
+
111
+ df = pl.DataFrame(gene_counts).transpose()
112
+ df.columns = ['genome', 'gene', 'domain']
113
+
114
+ genes_and_domains = set()
115
+ for gene, domains in marker_to_target_domains.items():
116
+ for domain in domains:
117
+ genes_and_domains.add(
118
+ os.path.basename(gene).replace('.spkg', '') + '_d__' + domain
119
+ )
120
+ df = df.with_columns(
121
+ (pl.col('gene') + '_' + pl.col('domain')).is_in(genes_and_domains).alias('is_target_domain')
122
+ )
123
+
124
+ domain_cumulative = {}
125
+ for _, group in df.group_by(['genome', 'gene', 'domain', 'is_target_domain']).len().group_by(['genome', 'gene']):
126
+ total_hits = sum(group['len'])
127
+ for row in group.rows(named=True):
128
+ genome = row['genome']
129
+ domain = row['domain']
130
+ count = row['len']
131
+ is_target_domain = row['is_target_domain']
132
+ if genome not in domain_cumulative:
133
+ domain_cumulative[genome] = {}
134
+ if domain not in domain_cumulative[genome]:
135
+ domain_cumulative[genome][domain] = {}
136
+ if is_target_domain not in domain_cumulative[genome][domain]:
137
+ domain_cumulative[genome][domain][True] = 0
138
+ domain_cumulative[genome][domain][False] = 0
139
+ domain_cumulative[genome][domain][is_target_domain] += count / total_hits
140
+
141
+ logging.info(f"Writing results to {args.output}")
142
+ domains = df.unique("domain").get_column("domain").to_list()
143
+ open_type = 'w' if first_line else 'a'
144
+ with open(args.output, open_type) as f:
145
+ if first_line:
146
+ print('genome\tdomain\tnum_in_target_domain\tnum_not_in_target_domain', file=f)
147
+ for genome, domain_counts in domain_cumulative.items():
148
+ for domain in domains:
149
+ if domain not in domain_counts:
150
+ domain_counts[domain] = {True: 0, False: 0}
151
+ for domain, is_target_domain_count in domain_counts.items():
152
+ print(
153
+ f'{genome}\t{domain}\t{is_target_domain_count[True]}\t{is_target_domain_count[False]}',
154
+ file=f
155
+ )
156
+ genomes_with_hits = set(domain_cumulative.keys())
157
+ for genome in genome_chunk:
158
+ genome_name = os.path.splitext(os.path.basename(genome))[0]
159
+ if genome_name not in genomes_with_hits:
160
+ print(f'{genome_name}\t\t0\t0', file=f)
161
+
162
+
163
+ def data_cmd(args):
164
+ if args.verify_only:
165
+ IsiteukMetapackage.verify(output_directory=args.output_directory)
166
+ else:
167
+ IsiteukMetapackage.download(output_directory=args.output_directory)
168
+
169
+
170
+ def main():
171
+ parent_parser = argparse.ArgumentParser(
172
+ prog='isiteuk',
173
+ description='In-situ identification of eukaryotes in metagenomic data',
174
+ )
175
+ parent_parser.add_argument('--debug', help='output debug information', action='store_true')
176
+ parent_parser.add_argument('--quiet', help='only output errors', action='store_true')
177
+ parent_parser.add_argument('--version', action='version', version=f'%(prog)s {isiteuk.__version__}')
178
+
179
+ subparsers = parent_parser.add_subparsers(
180
+ title='subcommands',
181
+ dest='subparser_name',
182
+ description='choose one of these subcommands',
183
+ )
184
+
185
+ # process subcommand
186
+ process_parser = subparsers.add_parser(
187
+ 'process',
188
+ help='Classify genomes by domain using SingleM',
189
+ description='Classify genomes by domain using SingleM marker genes',
190
+ )
191
+ process_parser.add_argument('--threads', type=int, default=1,
192
+ help='Number of threads to use [default: %(default)s]')
193
+ process_parser.add_argument('--output', required=True,
194
+ help='Output TSV file')
195
+ process_parser.add_argument('--chunk-size', type=int, default=10**4,
196
+ help='Number of genomes to process per chunk [default: %(default)s]')
197
+ process_parser.add_argument(
198
+ '--metapackage',
199
+ help=f'SingleM metapackage to use [default: use {DATA_ENVIRONMENT_VARIABLE} env var]',
200
+ )
201
+ process_parser.add_argument('--continue', dest='continuing',
202
+ help='Continue from a previous incomplete run', action='store_true')
203
+ genome_group = process_parser.add_argument_group('Genomes')
204
+ genome_group_mut = genome_group.add_mutually_exclusive_group(required=True)
205
+ genome_group_mut.add_argument('--genomes', nargs='+',
206
+ help='Genome(s) to classify in FASTA format')
207
+ genome_group_mut.add_argument('--genome-list',
208
+ help='File containing newline-separated list of genome paths in FASTA format')
209
+
210
+ # data subcommand
211
+ data_parser = subparsers.add_parser(
212
+ 'data',
213
+ help='Download reference metapackage data',
214
+ description='Download the isiteuk reference metapackage from Zenodo',
215
+ )
216
+ data_parser.add_argument(
217
+ '--output-directory',
218
+ help=f'Directory to download the metapackage to [required unless using --verify-only]',
219
+ )
220
+ data_parser.add_argument(
221
+ '--verify-only',
222
+ help=f'Check that the data in {DATA_ENVIRONMENT_VARIABLE} is up to date with correct checksums',
223
+ action='store_true',
224
+ default=False,
225
+ )
226
+
227
+ args = parent_parser.parse_args()
228
+
229
+ if args.debug:
230
+ loglevel = logging.DEBUG
231
+ elif args.quiet:
232
+ loglevel = logging.ERROR
233
+ else:
234
+ loglevel = logging.INFO
235
+ logging.basicConfig(
236
+ level=loglevel,
237
+ format='%(asctime)s %(levelname)s: %(message)s',
238
+ datefmt='%Y/%m/%d %I:%M:%S %p',
239
+ )
240
+
241
+ if args.subparser_name == 'process':
242
+ process_cmd(args)
243
+ elif args.subparser_name == 'data':
244
+ data_cmd(args)
245
+ else:
246
+ parent_parser.print_help()
247
+ sys.exit(1)
248
+
249
+
250
+ if __name__ == '__main__':
251
+ main()
isiteuk/metapackage.py ADDED
@@ -0,0 +1,70 @@
1
+ import os
2
+ import logging
3
+ import zenodo_backpack
4
+
5
+ DATA_ENVIRONMENT_VARIABLE = 'ISITEUK_METAPACKAGE_PATH'
6
+ DATA_DEFAULT_VERSION = '0.0.1'
7
+ # TODO: Update DATA_DOI after uploading the metapackage to Zenodo
8
+ DATA_DOI = '10.5281/zenodo.20755874'
9
+
10
+
11
+ class Metapackage:
12
+ @staticmethod
13
+ def acquire_default():
14
+ import singlem.metapackage
15
+ env_path = os.environ.get(DATA_ENVIRONMENT_VARIABLE)
16
+ if env_path is None:
17
+ raise Exception(
18
+ f"No metapackage specified. Either set the {DATA_ENVIRONMENT_VARIABLE} "
19
+ "environment variable, use --metapackage, or run 'isiteuk data' to download."
20
+ )
21
+ # Support both a zenodo backpack directory and a direct .smpkg path.
22
+ # Check for zenodo_backpack_version specifically — .smpkg dirs also have CONTENTS.json.
23
+ import json
24
+ contents_path = os.path.join(env_path, 'CONTENTS.json')
25
+ is_backpack = False
26
+ if os.path.exists(contents_path):
27
+ try:
28
+ with open(contents_path) as f:
29
+ is_backpack = 'zenodo_backpack_version' in json.load(f)
30
+ except Exception:
31
+ pass
32
+ if is_backpack:
33
+ backpack = zenodo_backpack.acquire(path=env_path, version=DATA_DEFAULT_VERSION)
34
+ return singlem.metapackage.Metapackage.acquire(
35
+ backpack.payload_directory_string(enter_single_payload_directory=True)
36
+ )
37
+ else:
38
+ return singlem.metapackage.Metapackage.acquire(env_path)
39
+
40
+ @staticmethod
41
+ def download(output_directory=None):
42
+ if not output_directory:
43
+ raise Exception("Must specify an output directory to download the isiteuk metapackage to")
44
+ logging.info(f"Downloading isiteuk metapackage version {DATA_DEFAULT_VERSION} from {DATA_DOI} ...")
45
+ backpack = zenodo_backpack.ZenodoBackpackDownloader().download_and_extract(
46
+ output_directory,
47
+ DATA_DOI,
48
+ progress_bar=True,
49
+ )
50
+ logging.info("Finished downloading data")
51
+ logging.info(f"export {DATA_ENVIRONMENT_VARIABLE}='{os.path.abspath(backpack.base_directory)}'")
52
+
53
+ @staticmethod
54
+ def verify(output_directory=None):
55
+ if output_directory is not None:
56
+ raise Exception(
57
+ f"Verification does not take an output directory. "
58
+ f"Set {DATA_ENVIRONMENT_VARIABLE} to the downloaded data location."
59
+ )
60
+ if DATA_ENVIRONMENT_VARIABLE not in os.environ:
61
+ raise Exception(
62
+ f"The {DATA_ENVIRONMENT_VARIABLE} environment variable is not set. "
63
+ "Run 'isiteuk data' to download the metapackage first."
64
+ )
65
+ backpack = zenodo_backpack.acquire(
66
+ env_var_name=DATA_ENVIRONMENT_VARIABLE,
67
+ version=DATA_DEFAULT_VERSION,
68
+ )
69
+ zenodo_backpack.ZenodoBackpackDownloader().verify(backpack)
70
+ logging.info("Metapackage verified successfully.")
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: isiteuk
3
+ Version: 0.0.2
4
+ Summary: In-situ identification of eukaryotes in metagenomic data
5
+ Author-email: Ben Woodcroft <benjwoodcroft@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/wwood/isiteuk
8
+ Keywords: metagenomics,bioinformatics,eukaryotes
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENCE.txt
16
+ Requires-Dist: singlem>=0.16.0
17
+ Requires-Dist: polars>=0.20
18
+ Requires-Dist: zenodo_backpack>=0.3
19
+ Dynamic: license-file
20
+
21
+ # isiteuk
22
+
23
+ isiteuk classifies genomes by domain (Bacteria, Archaea, Eukaryota) using [SingleM](https://github.com/wwood/SingleM) marker genes.
24
+
25
+ ## Installation
26
+
27
+ ### Conda (recommended)
28
+
29
+ ```bash
30
+ conda create -c conda-forge -c bioconda --override-channels --name isiteuk isiteuk
31
+ conda activate isiteuk
32
+ ```
33
+
34
+ ### PyPI
35
+
36
+ ```bash
37
+ pip install isiteuk
38
+ ```
39
+
40
+ Non-Python dependencies (SingleM, DIAMOND) must be installed separately — see `pixi.toml` for the full list.
41
+
42
+ ### GitHub (development)
43
+
44
+ ```bash
45
+ git clone https://github.com/wwood/isiteuk
46
+ cd isiteuk
47
+ pixi shell
48
+ isiteuk --help
49
+ ```
50
+
51
+ This installs isiteuk and all dependencies (including SingleM) into a managed environment via [pixi](https://pixi.sh).
52
+
53
+ ## Reference data
54
+
55
+ Download the isiteuk metapackage from Zenodo:
56
+
57
+ ```bash
58
+ isiteuk data --output-directory /path/to/isiteuk-data
59
+ export ISITEUK_METAPACKAGE_PATH=/path/to/isiteuk-data/isiteuk-backpack-0.0.1
60
+ ```
61
+
62
+ Add the `export` line to your `.bashrc` or equivalent to avoid repeating it. To verify the download:
63
+
64
+ ```bash
65
+ isiteuk data --verify-only
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Classify genomes
71
+
72
+ ```bash
73
+ isiteuk process \
74
+ --output results.tsv \
75
+ --genome-list genomes.txt \
76
+ --threads 64
77
+ ```
78
+
79
+ Or pass genomes directly:
80
+
81
+ ```bash
82
+ isiteuk process \
83
+ --output results.tsv \
84
+ --genomes genome1.fna.gz genome2.fna.gz \
85
+ --threads 64
86
+ ```
87
+
88
+ The `--metapackage` flag can override `ISITEUK_METAPACKAGE_PATH` for a one-off run:
89
+
90
+ ```bash
91
+ isiteuk process \
92
+ --metapackage /path/to/isiteuk-0.0.1.smpkg \
93
+ --output results.tsv \
94
+ --genomes genome1.fna.gz
95
+ ```
96
+
97
+ To resume an interrupted run:
98
+
99
+ ```bash
100
+ isiteuk process --continue \
101
+ --output results.tsv \
102
+ --genome-list genomes.txt
103
+ ```
104
+
105
+ ### Output format
106
+
107
+ Tab-separated file with one row per genome per domain detected:
108
+
109
+ | genome | domain | num_in_target_domain | num_not_in_target_domain |
110
+ |--------|--------|---------------------|--------------------------|
111
+ | genome1 | d__Bacteria | 14.2 | 0.0 |
112
+ | genome2 | d__Eukaryota | 8.7 | 1.3 |
113
+
114
+ ## Running tests
115
+
116
+ Quick tests (no metapackage required):
117
+
118
+ ```bash
119
+ pixi run test
120
+ ```
121
+
122
+ Full integration tests (requires `ISITEUK_METAPACKAGE_PATH` to be set):
123
+
124
+ ```bash
125
+ pixi run test-expensive
126
+ ```
127
+
128
+ Run a single test by name:
129
+
130
+ ```bash
131
+ pixi run run-a-test test_bacterial_genome
132
+ ```
@@ -0,0 +1,9 @@
1
+ isiteuk/__init__.py,sha256=miSsqPy2PFJ18TW9kocQJLcv4HFpkfQhcTzgAO-GTVE,22
2
+ isiteuk/main.py,sha256=Uu7yM4xkhUTrXg1nDwIhHwzV4620RuKVRIWfuuhJKMw,10471
3
+ isiteuk/metapackage.py,sha256=RAmFsifUs_qWc7FbSySPdK8llH_-VfgaSwYnDCk0mDk,2996
4
+ isiteuk-0.0.2.dist-info/licenses/LICENCE.txt,sha256=Z80VIz7M3yUiGP4za6VXSvqaNUtv_SE8cH8Xxl5vQQg,752
5
+ isiteuk-0.0.2.dist-info/METADATA,sha256=b_HNyczuHFC4gwtaQatPE2TiuLz7JbjdbIBn8h8UIMw,2987
6
+ isiteuk-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ isiteuk-0.0.2.dist-info/entry_points.txt,sha256=Hrod-2S5Ne8Jh1DxqHZqv9LBsD38jc1DvqS_7YwL7M0,46
8
+ isiteuk-0.0.2.dist-info/top_level.txt,sha256=aTbISlHjrhwoM3DHmuNnZ1Je6bdLn6qFRvsIdNTN7sY,8
9
+ isiteuk-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ isiteuk = isiteuk.main:main
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2024 Ben Woodcroft
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1 @@
1
+ isiteuk