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/__init__.py +5 -0
- hogprof/cli.py +484 -0
- hogprof/extract_levels_hits.py +518 -0
- hogprof/generate_jaccard_distr.py +516 -0
- hogprof/lshbuilder.py +710 -0
- hogprof/metadata.py +126 -0
- hogprof/presets.py +21 -0
- hogprof/profiler.py +333 -0
- hogprof/splitter.py +53 -0
- hogprof/utils/__init__.py +1 -0
- hogprof/utils/goautils.py +229 -0
- hogprof/utils/hashutils.py +190 -0
- hogprof/utils/orthoxml.py +83 -0
- hogprof/utils/phylo.py +305 -0
- hogprof/utils/pyhamutils.py +256 -0
- hogprof-0.3.0.dist-info/METADATA +231 -0
- hogprof-0.3.0.dist-info/RECORD +21 -0
- hogprof-0.3.0.dist-info/WHEEL +5 -0
- hogprof-0.3.0.dist-info/entry_points.txt +3 -0
- hogprof-0.3.0.dist-info/licenses/LICENSE +21 -0
- hogprof-0.3.0.dist-info/top_level.txt +1 -0
hogprof/metadata.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class HogQueryHit:
|
|
10
|
+
"""One HOG similarity search hit"""
|
|
11
|
+
|
|
12
|
+
query_hog_id: str
|
|
13
|
+
hit_hog_id: str
|
|
14
|
+
hit_level: str
|
|
15
|
+
jaccard: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class HogQueryResult:
|
|
20
|
+
"""Search results for one or more HOG queries"""
|
|
21
|
+
|
|
22
|
+
hits: pl.DataFrame
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_hits(cls, hits: list[HogQueryHit]) -> "HogQueryResult":
|
|
26
|
+
df = pl.DataFrame(
|
|
27
|
+
[asdict(hit) for hit in hits],
|
|
28
|
+
schema={
|
|
29
|
+
"query_hog_id": pl.String,
|
|
30
|
+
"hit_hog_id": pl.String,
|
|
31
|
+
"hit_level": pl.String,
|
|
32
|
+
"jaccard": pl.Float64,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
return cls(df.sort("jaccard", descending=True))
|
|
36
|
+
|
|
37
|
+
def __len__(self) -> int:
|
|
38
|
+
return self.hits.height
|
|
39
|
+
|
|
40
|
+
def to_pandas(self):
|
|
41
|
+
return self.hits.to_pandas()
|
|
42
|
+
|
|
43
|
+
def to_dicts(self) -> list[dict]:
|
|
44
|
+
return self.hits.to_dicts()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class HogEntry:
|
|
49
|
+
"""
|
|
50
|
+
A metadata entry for one indexed HOG. Represents a row in the fam2orthoxml file
|
|
51
|
+
"""
|
|
52
|
+
# The integer primary key used by the LSHForest.add and hashes.h5
|
|
53
|
+
hog_key: int
|
|
54
|
+
|
|
55
|
+
# Family ID in string format:
|
|
56
|
+
# - its HOG ID for the main mode
|
|
57
|
+
# - its rootHOG's ID for --slicesubhogs
|
|
58
|
+
# Example: "HOG:0015853_1"
|
|
59
|
+
fam_id: str
|
|
60
|
+
|
|
61
|
+
# HOG ID in string format:
|
|
62
|
+
# - the same as fam_id for the main mode.
|
|
63
|
+
# - subHOG ID for --slicesubhogs
|
|
64
|
+
# Example: "HOG:0015853.1aa_4"
|
|
65
|
+
hog_id: str
|
|
66
|
+
|
|
67
|
+
# Taxonomic level at which the HOG is defined.
|
|
68
|
+
# Example: "Archosauria"
|
|
69
|
+
level: str
|
|
70
|
+
|
|
71
|
+
# If orthoxml:
|
|
72
|
+
# - filename of the orthoxml file where the HOG was defined
|
|
73
|
+
# otherwise empty
|
|
74
|
+
source: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HogEntryIndex:
|
|
78
|
+
"""Indexed access to HOG metadata."""
|
|
79
|
+
|
|
80
|
+
COLUMNS = ("hog_key", "fam_id", "hog_id", "level", "source")
|
|
81
|
+
|
|
82
|
+
def __init__(self, csv_path: Path):
|
|
83
|
+
schema = {
|
|
84
|
+
"hog_key": pl.Int64, "fam_id": pl.String, "hog_id": pl.String,
|
|
85
|
+
"level": pl.String, "source": pl.String
|
|
86
|
+
}
|
|
87
|
+
# read fam2orthoxml
|
|
88
|
+
self._df = pl.read_csv(csv_path, schema_overrides=schema).select(self.COLUMNS)
|
|
89
|
+
|
|
90
|
+
# check format
|
|
91
|
+
if tuple(self._df.columns) != self.COLUMNS:
|
|
92
|
+
raise ValueError(f"Invalid format for {csv_path}: expected {self.COLUMNS}, got {self._df.columns}")
|
|
93
|
+
|
|
94
|
+
# make O(1) search indexes
|
|
95
|
+
self._by_key: dict[int, int] = {}
|
|
96
|
+
self._by_hog_id: dict[str, list[int]] = defaultdict(list)
|
|
97
|
+
self._by_level: dict[str, list[int]] = defaultdict(list)
|
|
98
|
+
|
|
99
|
+
for i, (key, _, hog_id, level, _) in enumerate(self._df.iter_rows()):
|
|
100
|
+
if key in self._by_key:
|
|
101
|
+
raise ValueError(f"Duplicate hog_key: {key}")
|
|
102
|
+
self._by_key[key] = i
|
|
103
|
+
|
|
104
|
+
self._by_hog_id[hog_id].append(i)
|
|
105
|
+
self._by_level[level].append(i)
|
|
106
|
+
|
|
107
|
+
def __len__(self) -> int:
|
|
108
|
+
return self._df.height
|
|
109
|
+
|
|
110
|
+
def _entry_at(self, i: int) -> HogEntry:
|
|
111
|
+
return HogEntry(*self._df.row(i))
|
|
112
|
+
|
|
113
|
+
def get_hog_by_key(self, key: int) -> HogEntry:
|
|
114
|
+
if not isinstance(key, int):
|
|
115
|
+
raise TypeError(f"key must be an integer, got {type(key)}")
|
|
116
|
+
|
|
117
|
+
i = self._by_key.get(key)
|
|
118
|
+
if i is None:
|
|
119
|
+
raise ValueError(f"Invalid HOG key: {key}")
|
|
120
|
+
return self._entry_at(i)
|
|
121
|
+
|
|
122
|
+
def get_hog_by_hog_id(self, hog_id: str) -> list[HogEntry]:
|
|
123
|
+
return [self._entry_at(i) for i in self._by_hog_id.get(hog_id, ())]
|
|
124
|
+
|
|
125
|
+
def search_by_level(self, level: str) -> list[HogEntry]:
|
|
126
|
+
return [self._entry_at(i) for i in self._by_level.get(level, ())]
|
hogprof/presets.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Taxonomic ranges shared by the CLI and programmatic profile builder."""
|
|
2
|
+
|
|
3
|
+
DB_PRESETS = {
|
|
4
|
+
"all": {"exclude_taxa": None, "taxon_mask": None},
|
|
5
|
+
"eukarya": {"exclude_taxa": None, "taxon_mask": "Eukaryota"},
|
|
6
|
+
|
|
7
|
+
# These below are the old masks (pre 0.2.0) that use
|
|
8
|
+
# the hardcoded taxa IDs. They should be substituted
|
|
9
|
+
# with correct taxon names. To verify and enable later
|
|
10
|
+
|
|
11
|
+
#'plants': {'exclude_taxa': None, 'taxon_mask': '33090'},
|
|
12
|
+
#'archaea': {'exclude_taxa': None, 'taxon_mask': '2157'},
|
|
13
|
+
#'bacteria': {'exclude_taxa': None, 'taxon_mask': '2'},
|
|
14
|
+
#'protists': {
|
|
15
|
+
# 'exclude_taxa': ['2', '2157', '33090', '4751', '33208'],
|
|
16
|
+
# 'taxon_mask': None,
|
|
17
|
+
# },
|
|
18
|
+
#'fungi': {'exclude_taxa': None, 'taxon_mask': '4751'},
|
|
19
|
+
#'metazoa': {'exclude_taxa': None, 'taxon_mask': '33208'},
|
|
20
|
+
#'vertebrates': {'exclude_taxa': None, 'taxon_mask': '7742'},
|
|
21
|
+
}
|
hogprof/profiler.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
import random
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import h5py
|
|
9
|
+
import numpy as np
|
|
10
|
+
import tables
|
|
11
|
+
from pyoma.browser import db
|
|
12
|
+
|
|
13
|
+
from hogprof.metadata import HogEntry, HogEntryIndex, HogQueryHit, HogQueryResult
|
|
14
|
+
from hogprof.utils import hashutils
|
|
15
|
+
|
|
16
|
+
np.random.seed(0)
|
|
17
|
+
random.seed(0)
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Profiler:
|
|
22
|
+
"""
|
|
23
|
+
A profiler object allows the user to query the LSH with HOGs and get a list of result HOGs back
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self,
|
|
26
|
+
lshforestpath=None,
|
|
27
|
+
hashes_h5=None,
|
|
28
|
+
oma=False,
|
|
29
|
+
nsamples=256,
|
|
30
|
+
slicesubhogs=False):
|
|
31
|
+
"""
|
|
32
|
+
The Profiler class initializes a profiler object for querying the LSH with HOGs and returning a list of result HOGs.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
lshobj (object): LSH object for querying.
|
|
36
|
+
hashes_h5 (h5py.File): H5 file containing HOGs.
|
|
37
|
+
nsamples (int): Number of samples to use.
|
|
38
|
+
tree (ete3.Tree): Master tree used for generating taxa index.
|
|
39
|
+
tree_string (str): String representation of the master tree.
|
|
40
|
+
taxaIndex (dict): Dictionary mapping taxa names to their indices in the master tree.
|
|
41
|
+
ReverseTaxaIndex (dict): Dictionary mapping indices in the master tree to their corresponding taxa names.
|
|
42
|
+
db_obj (db.Database): OMA database object.
|
|
43
|
+
treeweights (dict): Dictionary containing the tree weight for each taxon.
|
|
44
|
+
READ_ORTHO (callable): Function for reading orthoxml files from OMA.
|
|
45
|
+
HAM_PIPELINE (callable): Function for generating the Annotated tree from a row.
|
|
46
|
+
HASH_PIPELINE (callable): Function for generating the hash from a row.
|
|
47
|
+
slicesubhogs (bool): Whether to slice subhogs.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
lshforestpath (str, optional): Path to the pickled LSH forest object.
|
|
51
|
+
hashes_h5 (str, optional): Path to the H5 file containing HOGs.
|
|
52
|
+
mat_path (str, optional): Path to the matrix file containing HOGs.
|
|
53
|
+
oma (str, optional): Path to the OMA database.
|
|
54
|
+
tar (str, optional): Path to the tar archive.
|
|
55
|
+
nsamples (int, optional): Number of samples to use. Defaults to 256.
|
|
56
|
+
mastertree (str, optional): Path to the master tree file.
|
|
57
|
+
"""
|
|
58
|
+
logger.debug('Loading LSH')
|
|
59
|
+
with open(lshforestpath, 'rb') as lshpickle:
|
|
60
|
+
self.forest = pickle.loads(lshpickle.read())
|
|
61
|
+
logger.debug('Indexing LSH')
|
|
62
|
+
self.forest.index()
|
|
63
|
+
|
|
64
|
+
self.hashes_h5 = h5py.File(hashes_h5, mode='r')
|
|
65
|
+
self.slicesubhogs = slicesubhogs
|
|
66
|
+
|
|
67
|
+
self.oma = oma
|
|
68
|
+
self.fam2orthoxmlpath = f"{os.path.dirname(lshforestpath)}/fam2orthoxml.csv"
|
|
69
|
+
|
|
70
|
+
self.hog_index = HogEntryIndex(Path(self.fam2orthoxmlpath))
|
|
71
|
+
|
|
72
|
+
logger.debug("H5" + str(self.hashes_h5) + " " + str(self.hashes_h5.keys()))
|
|
73
|
+
self.nsamples = nsamples
|
|
74
|
+
|
|
75
|
+
if oma:
|
|
76
|
+
h5_oma = tables.open_file(oma, mode="r")
|
|
77
|
+
self.db_obj = db.Database(h5_oma)
|
|
78
|
+
|
|
79
|
+
logger.debug('Profiler init over')
|
|
80
|
+
|
|
81
|
+
def get_subhogs_of_fam(self, fam_id: int) -> Iterable[int]:
|
|
82
|
+
"""
|
|
83
|
+
Returns what sub HOG ID (row numbers in the fam2orthoxml.csv)
|
|
84
|
+
belong to the family of given ID (`fam` column == fam_id)
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(fam_id, str):
|
|
87
|
+
raise TypeError("fam_id must be a string")
|
|
88
|
+
|
|
89
|
+
if not self.slicesubhogs:
|
|
90
|
+
raise ValueError("SubHOGs only supported with --slicesubhogs")
|
|
91
|
+
|
|
92
|
+
fam_dict = (
|
|
93
|
+
self.id2famsubhog_df.groupby("fam")
|
|
94
|
+
.apply(lambda x: x.index.tolist())
|
|
95
|
+
.to_dict()
|
|
96
|
+
)
|
|
97
|
+
return fam_dict[fam_id]
|
|
98
|
+
|
|
99
|
+
def hogid2fam(self, hog_entry):
|
|
100
|
+
if type(hog_entry) not in [int, np.integer, str]:
|
|
101
|
+
raise TypeError("hog_entry must be integer or string")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if isinstance(hog_entry, (int, np.integer)) or hog_entry.isdecimal():
|
|
105
|
+
family = int(hog_entry)
|
|
106
|
+
|
|
107
|
+
# OMA mode
|
|
108
|
+
elif self.oma:
|
|
109
|
+
if hog_entry.startswith('HOG:'):
|
|
110
|
+
family = int(self.db_obj.parse_hog_id(hog_entry))
|
|
111
|
+
else:
|
|
112
|
+
entry = self.db_obj.entry_by_entry_nr(self.db_obj.id_resolver.resolve(hog_entry))
|
|
113
|
+
family = int(self.db_obj.hog_family(entry=entry))
|
|
114
|
+
|
|
115
|
+
# orthoxml mode -- rootHOGs
|
|
116
|
+
elif not self.slicesubhogs and hog_entry in self.hog2fam:
|
|
117
|
+
family = self.hog2fam[hog_entry]
|
|
118
|
+
|
|
119
|
+
# orthoxml mode -- subHOGs
|
|
120
|
+
elif self.slicesubhogs and hog_entry in self.subhogname_to_id_dict:
|
|
121
|
+
row = self.subhogname_to_id_dict[hog_entry]
|
|
122
|
+
return [row], {row: hog_entry}
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
raise KeyError(f"Unknown HOG identifier in this OrthoXML index: {hog_entry!r}")
|
|
126
|
+
|
|
127
|
+
# return family if the rootHOG mode
|
|
128
|
+
if not self.slicesubhogs:
|
|
129
|
+
return family
|
|
130
|
+
|
|
131
|
+
# otherwise return a dict
|
|
132
|
+
rows = list(self.fam_dict[family])
|
|
133
|
+
return rows, {row: self.subhog_dict[row] for row in rows}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_hog_of_level(self, hog_entries: list[HogEntry], level: str) -> HogEntry:
|
|
137
|
+
for h in hog_entries:
|
|
138
|
+
if h.level == level:
|
|
139
|
+
return h
|
|
140
|
+
|
|
141
|
+
raise ValueError(f"Cannot find the HOG at level {level}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def query_hog(self, hog_name: str, level=None, k: int = 10) -> HogQueryResult:
|
|
145
|
+
"""
|
|
146
|
+
Query the LSH index directly. `hog_id` MUST be a forest key
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(hog_name, str) or not hog_name.startswith('HOG:'):
|
|
149
|
+
raise TypeError("`hog_name` must be a string of format 'HOG:{number}'")
|
|
150
|
+
|
|
151
|
+
hogs = self.hog_index.get_hog_by_hog_id(hog_name)
|
|
152
|
+
if len(hogs) == 0:
|
|
153
|
+
raise ValueError(f"Unknown HOG identifier: {hog_name!r}")
|
|
154
|
+
|
|
155
|
+
if len(hogs) > 1:
|
|
156
|
+
if not level:
|
|
157
|
+
levels = [h.level for h in hogs]
|
|
158
|
+
raise ValueError(f"{hog_name} exists at different levels: {levels}. Please indicate a level")
|
|
159
|
+
|
|
160
|
+
hog = self._get_hog_of_level(hogs, level)
|
|
161
|
+
else:
|
|
162
|
+
hog = hogs[0]
|
|
163
|
+
|
|
164
|
+
query_hash = hashutils.fam2hash_hdf5(hog.hog_key, self.hashes_h5, nsamples=self.nsamples)
|
|
165
|
+
|
|
166
|
+
rows = []
|
|
167
|
+
hit_keys = self.forest.query(query_hash, k)
|
|
168
|
+
for hit_key in hit_keys:
|
|
169
|
+
hit_key = int(hit_key)
|
|
170
|
+
|
|
171
|
+
# get hit metadata
|
|
172
|
+
hit = self.hog_index.get_hog_by_key(hit_key)
|
|
173
|
+
|
|
174
|
+
# compute jaccard
|
|
175
|
+
hit_hash = hashutils.fam2hash_hdf5(hit_key, self.hashes_h5, nsamples=self.nsamples)
|
|
176
|
+
jaccard = query_hash.jaccard(hit_hash)
|
|
177
|
+
|
|
178
|
+
# report
|
|
179
|
+
rows.append(
|
|
180
|
+
HogQueryHit(
|
|
181
|
+
query_hog_id=hog_name,
|
|
182
|
+
hit_hog_id=hit.hog_id,
|
|
183
|
+
hit_level=hit.level,
|
|
184
|
+
jaccard=jaccard,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
hqr = HogQueryResult.from_hits(rows)
|
|
189
|
+
hqr.hits.sort("jaccard", descending=True)
|
|
190
|
+
return hqr
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def query_fam(self, fam_id: str, k=10):
|
|
194
|
+
"""
|
|
195
|
+
Query HOGs of a given family.
|
|
196
|
+
|
|
197
|
+
Default mode: only the rootHOG will be queried, and
|
|
198
|
+
`fam_id` MUST be a valid forest key. O
|
|
199
|
+
|
|
200
|
+
In the --slicesubhogs mode: all subHOGs of the given
|
|
201
|
+
family are queried.
|
|
202
|
+
|
|
203
|
+
Returns a dict of type
|
|
204
|
+
{
|
|
205
|
+
HOG_id : [ list of closest neibhbor IDs ]
|
|
206
|
+
}
|
|
207
|
+
"""
|
|
208
|
+
raise NotImplementedError()
|
|
209
|
+
|
|
210
|
+
if self.slicesubhogs:
|
|
211
|
+
# resolve fam_id -> list of forest key for every subHOG
|
|
212
|
+
subhog_keys = self.get_subhogs_of_fam(fam_id)
|
|
213
|
+
|
|
214
|
+
# get subHOG hashes
|
|
215
|
+
query_hashes_dict = {
|
|
216
|
+
key: hashutils.fam2hash_hdf5(key, self.hashes_h5, nsamples=self.nsamples)
|
|
217
|
+
for key in subhog_keys
|
|
218
|
+
}
|
|
219
|
+
else:
|
|
220
|
+
query_hashes_dict = {fam_id: hashutils.fam2hash_hdf5(fam_id, self.hashes_h5, nsamples=self.nsamples)}
|
|
221
|
+
|
|
222
|
+
# query every subHOG
|
|
223
|
+
return {
|
|
224
|
+
forest_key: self.forest.query(hash, k)
|
|
225
|
+
for forest_key, hash in query_hashes_dict.items()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
def hog_query(self, *,
|
|
229
|
+
hog_id: int | None = None,
|
|
230
|
+
fam_id: int | None = None,
|
|
231
|
+
k=10):
|
|
232
|
+
"""
|
|
233
|
+
Given a hog_id or a fam_id as a query, returns a dictionary containing the results of the LSH.
|
|
234
|
+
:param hog_id: query hog id
|
|
235
|
+
:param fam_id: query fam id
|
|
236
|
+
:return: list containing the results of the LSH for the given query
|
|
237
|
+
"""
|
|
238
|
+
logger.warning("`hog_query` is deprecated, use `query_hog` or `query_fam` instead")
|
|
239
|
+
|
|
240
|
+
if hog_id:
|
|
241
|
+
return self.query_hog(hog_id, k=k)
|
|
242
|
+
elif fam_id:
|
|
243
|
+
return self.query_fam(fam_id, k=k)
|
|
244
|
+
else:
|
|
245
|
+
raise ValueError("Provide either hog_id or fam_id")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def pull_hashes(self, hoglist, fam2orthoxmlpath = None):
|
|
249
|
+
"""
|
|
250
|
+
Given a list of hog_ids , returns a dictionary containing their hashes.
|
|
251
|
+
This uses the hdf5 file to get the hashvalues
|
|
252
|
+
:param hog_id: query hog id
|
|
253
|
+
:param fam_id: query fam id
|
|
254
|
+
:return: a dict containing the hash values of the hogs in hoglist
|
|
255
|
+
"""
|
|
256
|
+
#print('pulling hashes')
|
|
257
|
+
### hogid2fam returns the index of the family in the h5 file for normal cases
|
|
258
|
+
### for sliced subhogs, it returns the list of indices of the subhogs in the h5 file
|
|
259
|
+
### returns indices, subhog_dict
|
|
260
|
+
if not self.slicesubhogs:
|
|
261
|
+
return { entry: hashutils.fam2hash_hdf5( self.hogid2fam(entry), self.hashes_h5 , nsamples= self.nsamples) for entry in hoglist}
|
|
262
|
+
else:
|
|
263
|
+
result_dict = {}
|
|
264
|
+
for entry in hoglist:
|
|
265
|
+
indices, subhog_dict = self.hogid2fam(entry)
|
|
266
|
+
result_dict[entry] = [hashutils.fam2hash_hdf5(idx, self.hashes_h5, nsamples=self.nsamples) for idx in indices][0]
|
|
267
|
+
return result_dict
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def sort_hashes(query_hash, hashes):
|
|
272
|
+
"""
|
|
273
|
+
Given a dict of hogs:hashes, returns a sorted array of hogs and jaccard distances relative to query hog.
|
|
274
|
+
:param query hash: weighted minhash of the query
|
|
275
|
+
:param hashes: a dict of hogs:hashes
|
|
276
|
+
:return: sortedhogs, jaccard
|
|
277
|
+
"""
|
|
278
|
+
# sort the hashes by their jaccard relative to query hash
|
|
279
|
+
jaccard = [query_hash.jaccard(hashes[hog]) for hog in hashes]
|
|
280
|
+
index = np.argsort(jaccard)
|
|
281
|
+
sorted_hogs = np.asarray(list(hashes.keys()))[index]
|
|
282
|
+
jaccard = jaccard[index]
|
|
283
|
+
return sorted_hogs, jaccard
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def allvall_hashes(hashes):
|
|
288
|
+
"""
|
|
289
|
+
Given a dict of hogs:hashes, returns generate an all v all jaccard distance matrix.
|
|
290
|
+
:param hashes: a dict of hogs:hashes
|
|
291
|
+
:return: hashmat
|
|
292
|
+
"""
|
|
293
|
+
#generate an all v all jaccard distance matrix
|
|
294
|
+
hashmat = np.zeros((len(hashes),len(hashes)))
|
|
295
|
+
for i , hog1 in enumerate(hashes):
|
|
296
|
+
for j, hog2 in enumerate(hashes):
|
|
297
|
+
if i < j :
|
|
298
|
+
hashmat[i,j]= hashes[hog1].jaccard(hashes[hog2])
|
|
299
|
+
hashmat = hashmat+hashmat.T
|
|
300
|
+
np.fill_diagonal(hashmat, 1)
|
|
301
|
+
return hashmat
|
|
302
|
+
|
|
303
|
+
def hog_v_hog(self, hogs):
|
|
304
|
+
"""
|
|
305
|
+
give two hogs returns jaccard distance.
|
|
306
|
+
:param hog1 , hog2: str hog id
|
|
307
|
+
:return: jaccard score
|
|
308
|
+
"""
|
|
309
|
+
hog1,hog2 = hogs
|
|
310
|
+
#generate an all v all jaccard distance matrix
|
|
311
|
+
hashes = self.pull_hashes([hog1,hog2])
|
|
312
|
+
hashes = list(hashes.values())
|
|
313
|
+
return hashes[0].jaccard(hashes[1])
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def run_search(index_dir: Path,
|
|
317
|
+
hog_id: str,
|
|
318
|
+
level: str = None,
|
|
319
|
+
k: int = 10,
|
|
320
|
+
oma=None,
|
|
321
|
+
nsamples=None,
|
|
322
|
+
slice_subhogs=False,
|
|
323
|
+
):
|
|
324
|
+
|
|
325
|
+
index_dir = Path(index_dir)
|
|
326
|
+
p = Profiler(
|
|
327
|
+
lshforestpath=index_dir / "newlshforest.pkl",
|
|
328
|
+
hashes_h5=index_dir / "hashes.h5",
|
|
329
|
+
oma=oma,
|
|
330
|
+
nsamples=nsamples,
|
|
331
|
+
slicesubhogs=slice_subhogs,
|
|
332
|
+
)
|
|
333
|
+
return p.query_hog(hog_id, level=level, k=k).hits
|
hogprof/splitter.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
level=logging.INFO,
|
|
8
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def add_fastoma_path(fastoma_path):
|
|
12
|
+
"""Add the FastOMA utils directory to Python's sys.path."""
|
|
13
|
+
utils_path = os.path.join(os.path.abspath(fastoma_path), "utils")
|
|
14
|
+
|
|
15
|
+
if not os.path.isdir(utils_path):
|
|
16
|
+
raise FileNotFoundError(
|
|
17
|
+
f"FastOMA utils directory not found: {utils_path}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if utils_path not in sys.path:
|
|
21
|
+
sys.path.append(utils_path)
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
parser = argparse.ArgumentParser(description="Split OrthoXML file by species.")
|
|
25
|
+
parser.add_argument("--orthoxml_dir", required=True, help="Path to the directory containing FastOMA_HOGs.orthoxml")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--fastoma-path",
|
|
28
|
+
required=True,
|
|
29
|
+
help="Path to the FastOMA installation directory",
|
|
30
|
+
)
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
orthoxml_dir = args.orthoxml_dir
|
|
33
|
+
|
|
34
|
+
# Add FastOMA utils to the Python path
|
|
35
|
+
add_fastoma_path(args.fastoma_path)
|
|
36
|
+
|
|
37
|
+
# Import after updating sys.path
|
|
38
|
+
from OrthoXMLSplitter import OrthoXMLSplitter
|
|
39
|
+
|
|
40
|
+
# Change to the directory so that ./splits is created there
|
|
41
|
+
os.chdir(orthoxml_dir)
|
|
42
|
+
print(f"Changed working directory to: {orthoxml_dir}")
|
|
43
|
+
|
|
44
|
+
# Define the expected filename
|
|
45
|
+
orthoxml_filename = "FastOMA_HOGs.orthoxml"
|
|
46
|
+
# Run the splitter
|
|
47
|
+
splitter = OrthoXMLSplitter(orthoxml_filename, cache_dir="./splits")
|
|
48
|
+
# Extract all HOGs (default behavior: one file per HOG, in ./splits folder)
|
|
49
|
+
splitter()
|
|
50
|
+
logging.info("OrthoXML splitting completed.")
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
name = "utils"
|