hogprof 0.3.0__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.
hogprof/lshbuilder.py ADDED
@@ -0,0 +1,710 @@
1
+ import functools
2
+ import glob
3
+ import logging
4
+ import multiprocessing as mp
5
+ import pickle
6
+ import queue
7
+ import random
8
+ import time
9
+ import time as t
10
+ import xml.etree.ElementTree as ET
11
+ from contextlib import nullcontext
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ import h5py
16
+ import numpy as np
17
+ import pandas as pd
18
+ import polars as pl
19
+ from datasketch import MinHashLSHForest, WeightedMinHash, WeightedMinHashGenerator
20
+ from pyoma.browser import db
21
+ from tables import open_file
22
+
23
+ from hogprof.cli import track_progress
24
+ from hogprof.metadata import HogEntry, HogEntryIndex
25
+ from hogprof.presets import DB_PRESETS
26
+ from hogprof.utils import hashutils, phylo, pyhamutils
27
+ from hogprof.utils import orthoxml as oxml
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class LSHBuilder:
33
+ """
34
+ This class contains the stuff you need to make
35
+ a phylogenetic profiling
36
+ database with input orthxml files and a taxonomic tree
37
+ You must either input an OMA hdf5 file or an ensembl tarfile
38
+ containing orthoxml file with orthologous groups.
39
+
40
+ You can provide a species tree or use the ncbi taxonomy
41
+ with a list of taxonomic codes for all the species in your db
42
+ """
43
+
44
+ def __init__(self,
45
+ h5_oma=None, fileglob=None, taxa=None,
46
+ masterTree=None,
47
+ output_dir=None,
48
+ numperm=256,
49
+ treeweights=None, taxfilter=None, taxmask=None,
50
+ lossonly=False, duplonly=False, verbose=False,
51
+ use_taxcodes=False,
52
+ datetime=datetime.now(),
53
+ reformat_names=False,
54
+ slicesubhogs=False,
55
+ limit_species=10, limit_events=0,
56
+ seed=0):
57
+
58
+ """
59
+ Initializes the LSHBuilder class with the specified parameters and sets up the necessary objects.
60
+
61
+ Args:
62
+ - tarfile_ortho (str): path to an ensembl tarfile containing orthoxml files
63
+ - h5_oma (str): path to an OMA hdf5 file
64
+ - masterTree (str): path to a newick tree file
65
+ - output_dir (str): path to the directory where the output files will be saved
66
+ - numperm (int): the number of permutations to use in the MinHash generation (default: 256)
67
+ - treeweights (str): path to a pickled file containing the weights for the tree
68
+ - taxfilter (str): path to a file containing a list of taxonomic codes to filter from the tree
69
+ - taxmask (str): path to a file containing a list of taxonomic codes to mask from the tree
70
+ - verbose (bool): whether to print verbose output (default: False)
71
+ - slicesubhogs (bool): whether to slice the subhogs (default: False)
72
+ - limit_species (int): the minimum number of species in a subHOG that is included in the database (default: 10)
73
+ - limit_events (int): the minimum number of events (loss,duplication) in a subHOG that is included in the database (default: 0)
74
+
75
+ """
76
+ logger.debug("Initializing LSHBuilder")
77
+
78
+ random.seed(seed)
79
+ np.random.seed(seed)
80
+
81
+ if h5_oma:
82
+ self.h5OMA = h5_oma
83
+ self.db_obj = db.Database(h5_oma)
84
+ self.oma_id_obj = db.OmaIdMapper(self.db_obj)
85
+ else:
86
+ self.h5OMA = None
87
+ self.db_obj = None
88
+ self.oma_id_obj = None
89
+
90
+ self.reformat_names = reformat_names
91
+ self.slicesubhogs = slicesubhogs
92
+ self.swap2taxcode = use_taxcodes
93
+ self.tax_filter = taxfilter
94
+ self.tax_mask = taxmask
95
+ self.verbose = verbose
96
+ self.datetime = datetime
97
+ self.fileglob = fileglob
98
+ self.idmapper = None
99
+ self.date_string = "{:%B_%d_%Y_%H_%M}".format(datetime.now())
100
+ self.limit_species = limit_species
101
+ self.limit_events = limit_events
102
+
103
+ self.min_hog_size = 0 if self.slicesubhogs else self.limit_species
104
+ self.max_hog_size = np.inf
105
+
106
+ self.output_dir = output_dir
107
+ self.output_dir.mkdir(parents=True, exist_ok=True)
108
+
109
+ species = self._get_species_names()
110
+ if masterTree is None:
111
+ if not h5_oma:
112
+ raise TypeError('Please specify either a database or a tree')
113
+
114
+ self.tree_string, self.tree = phylo.get_tree(genomes=species, outdir=self.output_dir)
115
+ else:
116
+ # validate tree
117
+ self.tree = phylo.TreeValidator(masterTree, species).run()
118
+
119
+ # save the corrected tree
120
+ phylo.to_file(self.tree, self.output_dir / 'master_tree.corrected.nwk')
121
+ self.tree_string = phylo.to_string(self.tree)
122
+
123
+ self.taxaIndex, self.reverse = phylo.generate_taxa_index(self.tree, self.tax_filter, self.tax_mask)
124
+
125
+ with open(self.output_dir / 'taxaIndex.pkl', 'wb') as taxout:
126
+ taxout.write( pickle.dumps(self.taxaIndex))
127
+
128
+ self.numperm = numperm
129
+ ### if no weights are provided, generate them
130
+ if treeweights is None:
131
+ #generate aconfig_utilsll ones
132
+ self.treeweights = hashutils.generate_treeweights(self.tree, self.taxaIndex, self.tax_filter, self.tax_mask)
133
+ else:
134
+ #load machine learning weights
135
+ self.treeweights = treeweights
136
+ tax_max = max(self.taxaIndex.values())+1
137
+ wmg = WeightedMinHashGenerator(3*tax_max , sample_size = numperm , seed=1)
138
+
139
+ with open(self.output_dir / 'wmg.pkl', 'wb') as wmgout:
140
+ wmgout.write( pickle.dumps(wmg))
141
+
142
+ self.wmg = wmg
143
+
144
+ logger.debug("Configuring pyham functions")
145
+ logger.debug("taxfilter: %s", self.tax_filter)
146
+ logger.debug("taxmask: %s", self.tax_mask)
147
+ logger.debug("swap ids: %s", self.swap2taxcode)
148
+ logger.debug("reformat names: %s", self.reformat_names)
149
+ logger.debug("use taxcodes: %s", self.swap2taxcode)
150
+
151
+ hamfunction = pyhamutils.get_ham_treemap_from_row
152
+ hashfunction = hashutils.row2hash
153
+ if slicesubhogs:
154
+ hashfunction = hashutils.hash_trees_subhogs
155
+
156
+ ### set up the pyHAM pipeline with different parameters depending on whether the input is an OMA hdf5 file or a list of orthoxml files
157
+ logger.debug('lossonly', lossonly)
158
+ logger.debug('duplonly', duplonly)
159
+
160
+ self.dataset_nodes = None
161
+ self.idmapper = None
162
+
163
+ # remap taxfilter and taxmask
164
+ if taxfilter:
165
+ #self.tax_filter = [ self.idmapper[tax] for tax in taxfilter ]
166
+ unacceptable_nodes = []
167
+ for filterobj in self.tax_filter:
168
+ filter_node = self.tree.search_nodes(name=filterobj)
169
+ print('Found filter node:', filter_node)
170
+ unacceptable_nodes.extend([node.name for node in filter_node[0].traverse()])
171
+
172
+ # print(f"Error searching for node with name: {filterobj}")
173
+ # print("Clade could not be excluded")
174
+ # continue
175
+ # update dataset_nodes to exclude the filtered nodes
176
+ self.dataset_nodes = [node.name for node in self.tree.traverse() if node.name not in unacceptable_nodes]
177
+
178
+ if taxmask:
179
+ #print(self.idmapper)
180
+ #self.tax_mask = self.idmapper[taxmask]
181
+ ### get acceptable ids here:
182
+ tax_mask_node = self.tree.search_nodes(name=self.tax_mask)
183
+ if tax_mask_node:
184
+ tax_mask_node = tax_mask_node[0]
185
+ print(f"Found tax_mask_node: {tax_mask_node.name}")
186
+ self.dataset_nodes = [node.name for node in tax_mask_node.traverse()]
187
+ else:
188
+ raise RuntimeError(f"No node found with name: {self.tax_mask}")
189
+
190
+ self.HAM_PIPELINE = functools.partial(
191
+ hamfunction, tree_string=self.tree_string, swap_ids=self.swap2taxcode,
192
+ orthoXML_as_string=self.h5OMA is not None, slicesubhogs=self.slicesubhogs,
193
+ limit_species=self.limit_species, limit_events=self.limit_events,
194
+ dataset_nodes=self.dataset_nodes, verbose=self.verbose)
195
+
196
+ ### set up the hash pipeline
197
+ self.HASH_PIPELINE = functools.partial( hashfunction , taxaIndex=self.taxaIndex, treeweights=self.treeweights, wmg=wmg , lossonly = lossonly, duplonly = duplonly)
198
+
199
+ logger.debug("Setting up input data reader")
200
+ ### if the input is an OMA hdf5 file, set up the function to read the orthoxml data
201
+ if self.h5OMA:
202
+ self.READ_ORTHO = functools.partial(pyhamutils.get_orthoxml_oma, db_obj=self.db_obj)
203
+ self.n_groups = len(self.h5OMA.root.OrthoXML.Index)
204
+ logger.info( 'reading oma hdf5 with n groups: %d', self.n_groups)
205
+ elif self.fileglob:
206
+ logger.info('reading orthoxml files: %d' , len(self.fileglob))
207
+ self.n_groups = len(self.fileglob)
208
+ else:
209
+ raise RuntimeError('please specify an input file' )
210
+
211
+ self.hashes_path = self.output_dir / 'hashes.h5'
212
+ self.lshpath = self.output_dir / 'newlsh.pkl'
213
+ self.lshforestpath = self.output_dir / 'newlshforest.pkl'
214
+ self.mat_path = self.output_dir / 'hogmat.h5'
215
+ self.columns = len(self.taxaIndex)
216
+ self.verbose = verbose
217
+ logger.debug('Done.')
218
+
219
+ def _get_species_names(self):
220
+ """Read the DB or orthoxml to extract the list of species"""
221
+ if self.h5OMA:
222
+ field = "NCBITaxonId" if self.swap2taxcode else "SciName"
223
+ values = self.h5OMA.root.Genome.read(field=field)
224
+ return {
225
+ value.decode("utf-8") if isinstance(value, bytes) else str(value)
226
+ for value in values
227
+ }
228
+ else:
229
+ names = set()
230
+ for filename in self.fileglob:
231
+ for child in oxml.iter_species(filename):
232
+ field = "NCBITaxId" if self.swap2taxcode else "name"
233
+ names.add(child.attrib[field])
234
+
235
+ return names
236
+
237
+ def load_one(self, fam):
238
+ #test function to try out the pipeline on one orthoxml
239
+ ortho_fam = self.READ_ORTHO(fam)
240
+ pyham_tree = self.HAM_PIPELINE([fam, ortho_fam])
241
+ hog_matrix,weighted_hash = hashutils.hash_tree(pyham_tree , self.taxaIndex , self.treeweights , self.wmg)
242
+ return ortho_fam , pyham_tree, weighted_hash,hog_matrix
243
+
244
+ def _hog_size_ok(self, hog_size):
245
+ result = (self.max_hog_size is None or hog_size <= self.max_hog_size)
246
+ result &= (self.min_hog_size is None or hog_size >= self.min_hog_size)
247
+ return result
248
+
249
+ def _check_hog_size(self, orthoxml_file):
250
+ with open(orthoxml_file) as ortho:
251
+ ortho_str = ortho.read()
252
+
253
+ # filtering by HOG size in the # of species
254
+ hog_size = oxml.count_species(ortho_str)
255
+ return self._hog_size_ok(hog_size)
256
+
257
+ def generate_dataframes(self, size=100):
258
+ families = {}
259
+ start = -1
260
+ if self.h5OMA:
261
+ self.groups = self.h5OMA.root.OrthoXML.Index
262
+ buffer = self.h5OMA.root.OrthoXML.Buffer
263
+ oxml_builder = oxml.OrthoXMLBuilder()
264
+ self.rows = len(self.groups)
265
+
266
+ unique_hog_id = 0
267
+
268
+ for i, row in enumerate(track_progress(
269
+ self.groups,
270
+ description="Processing OMA groups",
271
+ total=self.rows,
272
+ )):
273
+ if i > start:
274
+ # The current index row already locates the unaugmented XML
275
+ offset = int(row['HogBufferOffset'])
276
+ stop = offset + int(row['HogBufferLength'])
277
+ xml = ET.fromstring(buffer[offset:stop].tobytes().decode())
278
+
279
+ # filtering by HOG size in the # of species
280
+ hog_size = oxml.count_species(xml)
281
+ if self._hog_size_ok(hog_size):
282
+ ortho_fam = oxml_builder.add_loft_ids(xml)
283
+ families[unique_hog_id] = {
284
+ 'ortho': ortho_fam,
285
+ 'hog_id': oxml.get_root_hog_id(xml),
286
+ }
287
+ unique_hog_id += 1
288
+
289
+ if len(families) >= size:
290
+ pd_dataframe = pd.DataFrame.from_dict(families, orient='index')
291
+ pd_dataframe['hog_key'] = pd_dataframe.index
292
+ yield pd_dataframe
293
+ families = {}
294
+ if families:
295
+ pd_dataframe = pd.DataFrame.from_dict(families, orient='index')
296
+ pd_dataframe['hog_key'] = pd_dataframe.index
297
+ yield pd_dataframe
298
+
299
+ elif self.fileglob:
300
+ unique_fam_id = 0
301
+
302
+ for orthoxml_file in track_progress(
303
+ self.fileglob,
304
+ description="Processing OrthoXML files",
305
+ total=len(self.fileglob),
306
+ ):
307
+ if self._check_hog_size(orthoxml_file):
308
+ families[unique_fam_id] = {'ortho': orthoxml_file, 'hog_id': ''}
309
+ unique_fam_id += 1
310
+
311
+ if len(families) >= size:
312
+ pd_dataframe = pd.DataFrame.from_dict(families, orient='index')
313
+ pd_dataframe['hog_key'] = pd_dataframe.index
314
+ #print(pd_dataframe)
315
+ yield pd_dataframe
316
+ families = {}
317
+
318
+ if families:
319
+ pd_dataframe = pd.DataFrame.from_dict(families, orient='index')
320
+ pd_dataframe['hog_key'] = pd_dataframe.index
321
+ yield pd_dataframe
322
+
323
+ def _extract_hog_name(self, orthoxml_file: Path) -> str:
324
+ return Path(orthoxml_file).name.split(".", 1)[0]
325
+
326
+ def worker(self, i, work_queue, result_queue):
327
+ logger.debug('Starting worker #%d', i)
328
+ while True:
329
+ df = work_queue.get()
330
+ if df is None:
331
+ logger.debug("Worker #%d done", i)
332
+ break
333
+
334
+ if self.slicesubhogs:
335
+ tree_dicts = []
336
+ for index, row in df[["hog_key", "ortho"]].iterrows():
337
+ result = self.HAM_PIPELINE(row)
338
+ tree_dicts.append(result)
339
+
340
+ # Assign the results back to the DataFrame
341
+ df["tree_dicts"] = tree_dicts
342
+ df["hash_dicts"] = df[["hog_key", "tree_dicts"]].apply(
343
+ self.HASH_PIPELINE, axis=1
344
+ )
345
+
346
+ # Filter out rows with empty hash_dicts to save time
347
+ df = df[df["hash_dicts"].apply(bool)]
348
+ if df.empty:
349
+ continue
350
+
351
+ rows = []
352
+ for _, row in df.iterrows():
353
+ fam_id = self._extract_hog_name(row["ortho"]) if self.fileglob else str(row["hog_id"])
354
+ for subhog in row["hash_dicts"]:
355
+ level = row["tree_dicts"][subhog].name
356
+ hashed = row["hash_dicts"][subhog][1]
357
+
358
+ rows.append(
359
+ {
360
+ "hog_key": row["hog_key"],
361
+ "fam_id": fam_id,
362
+ "hog_id": subhog.removeprefix(f"{level}_"),
363
+ "level": level,
364
+ # Polars Object columns cannot be pickled by mp.Queue.
365
+ # Send native values and reconstruct the hash in saver
366
+ "hash": hashed.hashvalues.tolist(),
367
+ "hash_seed": hashed.seed,
368
+ "source": str(row["ortho"]) if self.fileglob else "",
369
+ }
370
+ )
371
+
372
+ result_queue.put(pl.DataFrame(rows))
373
+ else:
374
+ df["tree"] = df[["hog_key", "ortho"]].apply(self.HAM_PIPELINE, axis=1)
375
+ df[["hash", "rows"]] = df[["hog_key", "tree"]].apply(self.HASH_PIPELINE, axis=1)
376
+
377
+ # HOG names are extracted from the names of orthoxml files.
378
+ # Let's hope they are correct
379
+ if self.fileglob:
380
+ df["hog_id"] = df["ortho"].apply(self._extract_hog_name)
381
+ df["source"] = df["ortho"].map(str)
382
+ else:
383
+ df["hog_id"] = df["hog_id"].map(str)
384
+ df["source"] = ""
385
+ df["fam_id"] = df["hog_id"]
386
+ df["level"] = df["tree"].map(lambda tree: tree.name if tree is not None else "")
387
+
388
+ result_queue.put(df[[*HogEntryIndex.COLUMNS, "hash"]])
389
+
390
+
391
+ def _make_tax_str(self):
392
+ taxstr = ""
393
+ if self.tax_filter is None:
394
+ taxstr = "NoFilter"
395
+ if self.tax_mask is None:
396
+ taxstr += "NoMask"
397
+ else:
398
+ taxstr = str(self.tax_filter)
399
+ return taxstr
400
+
401
+ def _save_forest(self, forest):
402
+ logger.debug("Saving forest at: %.2f", t.time() - self.start_time)
403
+
404
+ forest.index()
405
+ with open(self.lshforestpath, "wb") as forest_out:
406
+ forest_out.write(pickle.dumps(forest, -1))
407
+
408
+ logger.debug("Save done at: %.2f", t.time() - self.start_time)
409
+
410
+ def _save_metadata(self, entries):
411
+ df = pd.DataFrame(entries, columns=HogEntryIndex.COLUMNS)
412
+ df.to_csv(self.output_dir / 'fam2orthoxml.csv', index=False)
413
+
414
+
415
+ def saver(self, result_queue):
416
+ global_time = t.time()
417
+ self.start_time = global_time
418
+
419
+ chunk_size = 100
420
+ count = 0
421
+ forest = MinHashLSHForest(num_perm=self.numperm)
422
+ metadata_entries = []
423
+ taxstr = self._make_tax_str()
424
+
425
+ # With --slicesubhogs, we don't know in advance into how many
426
+ # subHOGs a family will be expanded. Therefore, the global hog_key
427
+ # has to be reassigned so that:
428
+ # 1) every subHOG gets a unique key
429
+ # 2) hog_keys are continuous
430
+ hog_key = 0
431
+
432
+ with h5py.File(self.hashes_path, 'w', libver='latest') as h5hashes:
433
+ hash_width = 2 * self.numperm
434
+
435
+ # The main hash mapping. LSHForest.add takes a pair = (key, MihHash object),
436
+ # however you cannot query it with 'key', only with MinHash.
437
+ # To support that, we store the mapping { key -> MinHash }
438
+ # for everything that is indexed in the forest.
439
+ dataset = h5hashes.create_dataset(
440
+ taxstr,
441
+ (chunk_size, hash_width),
442
+ maxshape=(None, hash_width),
443
+ dtype="int32",
444
+ )
445
+ h5hashes.flush()
446
+ logger.debug('Creating dataset filtered at taxonomic level: %s', taxstr)
447
+
448
+ while True:
449
+ current_df = result_queue.get()
450
+ if current_df is None:
451
+ break
452
+
453
+ # Temporary: Pandas is to be replaced by Polars everywhere.
454
+ # currently we still use pd for the rootHOG mode
455
+ is_polars = isinstance(current_df, pl.DataFrame)
456
+ rows = current_df.to_dicts() if is_polars else current_df.to_dict("records")
457
+ for row in rows:
458
+ hashed = row["hash"]
459
+ if hashed is None:
460
+ continue
461
+
462
+ if is_polars:
463
+ hashed = WeightedMinHash(
464
+ seed=row["hash_seed"],
465
+ hashvalues=np.asarray(hashed, dtype=np.int64),
466
+ )
467
+
468
+ key = hog_key if self.slicesubhogs else int(row["hog_key"])
469
+
470
+ # resize the HogEntryIndex dataset if all rows filled
471
+ if dataset.shape[0] <= key:
472
+ dataset.resize(key + chunk_size, axis=0)
473
+
474
+ # save the hash
475
+ dataset[key, :] = hashed.hashvalues.ravel()
476
+
477
+ # index the hash
478
+ forest.add(str(key), hashed)
479
+
480
+ metadata_row = HogEntry(
481
+ hog_key=key, fam_id=row["fam_id"], hog_id=row["hog_id"],
482
+ level=row["level"], source=row["source"],
483
+ )
484
+ metadata_entries.append(metadata_row)
485
+ hog_key += 1
486
+ count += 1
487
+
488
+ logger.info('Wrapping up the run')
489
+ self._save_forest(forest)
490
+ h5hashes.flush()
491
+ self._save_metadata(metadata_entries)
492
+ logger.info('Saver wrote %d hashes', count)
493
+
494
+
495
+ @staticmethod
496
+ def _raise_if_failed(processes, required_alive=()):
497
+ """Raise if a child process failed"""
498
+ failed = [
499
+ process for process in processes
500
+ if process.exitcode not in (None, 0)
501
+ ]
502
+ if failed:
503
+ details = ", ".join(
504
+ f"{process.name} (exit code {process.exitcode})"
505
+ for process in failed
506
+ )
507
+ raise RuntimeError(f"Child process failure: {details}")
508
+
509
+ stopped = [
510
+ process for process in required_alive
511
+ if process.exitcode is not None
512
+ ]
513
+ if stopped:
514
+ names = ", ".join(process.name for process in stopped)
515
+ raise RuntimeError(f"Child process exited unexpectedly: {names}")
516
+
517
+ @classmethod
518
+ def _safe_put(cls, destination, value, processes, required_alive=()):
519
+ """
520
+ Enqueue data value with process/queue checks:
521
+ - if any of the workers failed already, don't send
522
+ - if queue is full, try again
523
+ """
524
+ while True:
525
+ cls._raise_if_failed(processes, required_alive)
526
+
527
+ try:
528
+ destination.put(value, timeout=0.2)
529
+ return
530
+ except queue.Full:
531
+ pass
532
+
533
+ @classmethod
534
+ def _wait_for_all(cls, processes, monitored_processes=()):
535
+ """
536
+ Joins a list of processes; raises if any of them
537
+ failed or if the monitored processes failed
538
+ """
539
+ alive = list(processes)
540
+ all_processes = [*alive, *monitored_processes]
541
+
542
+ while alive:
543
+ for process in alive:
544
+ process.join(timeout=0)
545
+
546
+ cls._raise_if_failed(all_processes)
547
+
548
+ alive = [process for process in alive if process.is_alive()]
549
+ if alive:
550
+ time.sleep(0.1)
551
+
552
+ @staticmethod
553
+ def _terminate_all(processes):
554
+ for process in processes:
555
+ if process.is_alive():
556
+ process.terminate()
557
+
558
+ for process in processes:
559
+ process.join()
560
+
561
+
562
+ def _run_parallel(self, threads, data_generator):
563
+ if threads < 1:
564
+ raise ValueError("threads must be at least 1")
565
+
566
+ queue_size = max(2, threads * 2)
567
+ work_queue = mp.Queue(maxsize=queue_size)
568
+ result_queue = mp.Queue(maxsize=queue_size)
569
+ saver = mp.Process(name="saver", target=self.saver, args=(result_queue,))
570
+ workers = [
571
+ mp.Process(name=f"worker-{i}", target=self.worker, args=(i, work_queue, result_queue))
572
+ for i in range(threads)
573
+ ]
574
+ processes = [saver, *workers]
575
+ started_processes = []
576
+ completed = False
577
+
578
+ try:
579
+ # start saver
580
+ saver.start()
581
+ started_processes.append(saver)
582
+ logger.info('Started one saver process')
583
+
584
+ # start workers
585
+ for process in workers:
586
+ process.start()
587
+ started_processes.append(process)
588
+ logger.info('Started %d workers', threads)
589
+
590
+ # send data batches to workers
591
+ for data in data_generator:
592
+ self._safe_put(work_queue, data, processes, required_alive=processes)
593
+
594
+ # send sentinels (end of work signals) to all workers
595
+ for _ in workers:
596
+ self._safe_put(work_queue,None, processes, required_alive=(saver,))
597
+ logger.info('Input queued; waiting for workers to finish')
598
+
599
+ # the main join -- wait for workers to be done
600
+ self._wait_for_all(workers, monitored_processes=(saver,))
601
+
602
+ logger.info('Workers finished; finalizing saver')
603
+ # send the sentinel to the saver
604
+ self._safe_put(result_queue,None,(saver,), required_alive=(saver,))
605
+
606
+ # wait for the saver
607
+ self._wait_for_all((saver,))
608
+ completed = True
609
+
610
+ finally:
611
+ # if error
612
+ if not completed:
613
+ self._terminate_all(started_processes)
614
+
615
+ # finalize queues
616
+ for process_queue in (work_queue, result_queue):
617
+ if not completed:
618
+ # if failed, the join below is ignored
619
+ process_queue.cancel_join_thread()
620
+
621
+ process_queue.close()
622
+ if completed:
623
+ process_queue.join_thread()
624
+
625
+ logger.info('Pipeline complete')
626
+
627
+
628
+ def run_pipeline(self , threads):
629
+ logger.info('Running with %d threads', threads)
630
+ # Levels mode counts represented species on each parsed HOG
631
+ data_generator = self.generate_dataframes(size=100)
632
+ self._run_parallel(threads=threads, data_generator=data_generator)
633
+ return self.hashes_path, self.lshforestpath, self.mat_path
634
+
635
+
636
+ def run(
637
+ *,
638
+ output_dir,
639
+ tree=None,
640
+ oma=None,
641
+ orthoxml_glob=None,
642
+ tax_weights=None,
643
+ taxon_mask=None,
644
+ exclude_taxa=None,
645
+ min_species=10,
646
+ min_events=0,
647
+ db_type=None,
648
+ num_permutations=256,
649
+ loss_only=False,
650
+ duplication_only=False,
651
+ use_tax_ids=False,
652
+ slice_subhogs=False,
653
+ seed=0,
654
+ njobs=1,
655
+ verbose=False,
656
+ ):
657
+ if (oma is not None) == (orthoxml_glob is not None):
658
+ raise ValueError("Supply exactly one of oma and orthoxml_glob")
659
+
660
+ if loss_only and duplication_only:
661
+ raise ValueError("loss_only and duplication_only are mutually exclusive")
662
+
663
+ if db_type is not None:
664
+ preset = DB_PRESETS[db_type]
665
+ if taxon_mask is None:
666
+ taxon_mask = preset['taxon_mask']
667
+ if exclude_taxa is None:
668
+ exclude_taxa = preset['exclude_taxa']
669
+
670
+ weights = None
671
+ if tax_weights is not None:
672
+ from keras.models import model_from_json
673
+
674
+ with open(f"{tax_weights}.json") as model_file:
675
+ model = model_from_json(model_file.read())
676
+ model.load_weights(f"{tax_weights}.h5")
677
+ logger.info("Loaded model from disk")
678
+ weights = model.get_weights()[0] + 1e-10
679
+
680
+ # Discover input files from the glob
681
+ orthoxml_files = None
682
+ if orthoxml_glob is not None:
683
+ orthoxml_files = glob.glob(orthoxml_glob)
684
+ logger.info("Using OrthoXML files from: %s", orthoxml_glob)
685
+
686
+ start = time.monotonic()
687
+ database = open_file(oma, mode="r") if oma is not None else nullcontext(None)
688
+ with database as h5_oma:
689
+ lsh_builder = LSHBuilder(
690
+ h5_oma=h5_oma,
691
+ fileglob=orthoxml_files,
692
+ output_dir=Path(output_dir),
693
+ masterTree=Path(tree) if tree is not None else None,
694
+ numperm=num_permutations,
695
+ treeweights=weights,
696
+ taxfilter=exclude_taxa,
697
+ taxmask=taxon_mask,
698
+ lossonly=loss_only,
699
+ duplonly=duplication_only,
700
+ use_taxcodes=use_tax_ids,
701
+ slicesubhogs=slice_subhogs,
702
+ limit_species=min_species,
703
+ limit_events=min_events,
704
+ seed=seed,
705
+ verbose=verbose,
706
+ )
707
+ paths = lsh_builder.run_pipeline(njobs)
708
+
709
+ logger.info("Done in %.2fs", time.monotonic() - start)
710
+ return paths