Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project (attempts to) adhere to [Semantic Versioning](http://semver.org/).

## [4.0.2] - 2026-09-30

### Fixed
- Averaging reciprocal ANI values before secondary clustering is now vectorized
instead of looping over every row of Ndb (issue #308). Results are identical,
but it's ~25-30x faster on large primary clusters (a 2,000-genome primary
cluster went from 133s to 6s to cluster) and no longer builds a string-keyed
dictionary with an entry for every comparison

## [4.0.1] - 2026-09-03

**You can now hand raw CheckM2 output straight to `--genomeInfo`.** No awk, no
Expand Down
2 changes: 1 addition & 1 deletion drep/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.0.1
4.0.2
36 changes: 25 additions & 11 deletions drep/d_cluster/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,17 +717,31 @@ def add_avani(db):
db: dataframe
'''

logging.debug('making dictionary for average_ani')
combo2value = {}
for i, row in db.iterrows():
combo2value["{0}-vs-{1}".format(row['querry'], row['reference'])] \
= row['ani']

logging.debug('list comprehension for average_ani')
db['av_ani'] = [np.mean([combo2value["{0}-vs-{1}".format(q, r)],
combo2value["{0}-vs-{1}".format(r, q)]]) if r != q else 1\
for q, r in zip(db['querry'].tolist(),
db['reference'].tolist())]
# Vectorized: this used to iterrows() into a string-keyed dict, which took
# hours and hundreds of GB once a single primary cluster held tens of
# thousands of genomes (issue #308). Each (reference, querry) pair is encoded
# as one integer so its reverse can be looked up in bulk.
logging.debug('averaging reciprocal ANI values')
ref = db['reference'].values
qry = db['querry'].values
names = pd.Index(pd.unique(np.concatenate([ref, qry])))
n = len(names)
r = names.get_indexer(ref).astype(np.int64)
q = names.get_indexer(qry).astype(np.int64)

# When a pair appears more than once the last value wins, as before
ani = pd.Series(db['ani'].values, index=r * n + q)
ani = ani[~ani.index.duplicated(keep='last')]
forward = ani.reindex(r * n + q).values
reverse = ani.reindex(q * n + r).values

same = r == q
missing = ~np.isin(q * n + r, ani.index.values) & ~same
if missing.any():
i = np.argmax(missing)
raise KeyError("{0}-vs-{1}".format(ref[i], qry[i]))

db['av_ani'] = np.where(same, 1, (forward + reverse) / 2)

logging.debug('averageing done')

Expand Down
Loading