OPDiv
Molecular portfolio selection and diversity evaluation.
Choose exactly k candidates with the highest mean score while enforcing a pairwise similarity ceiling. Use the resulting mean to evaluate a screening or generative method's candidate archive.
Implements Optimal Portfolio Diversity (OPDiv), its score-ordered greedy approximation GPDiv, from Measuring Diversity of Top-K Molecules as an Optimal Portfolio Selection Problem by Miroslav Lžičař (preprint draft, September 2026).
Install
Python 3.10 or newer:
pip install opdiv
For development, from this repository:
pip install -e '.[chem,dev]'
The core requires NumPy and OR-Tools. The optional chem extra adds RDKit for
building Morgan/Tanimoto similarities from SMILES.
Select a portfolio
from opdiv import select, opdiv, gpdiv
# The paper's counterexample: A conflicts with B and C; B and C are compatible.
scores = [16.0, 12.0, 11.0, 6.0] # A, B, C, D; larger is better
conflicts = [
[0, 1, 1, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
]
result = select(scores, k=2, conflicts=conflicts)
print(result.indices) # (1, 2): B and C
print(result.value) # 11.5
print(result.status) # optimal
# Metric-only calls also perform selection internally.
print(opdiv(scores, k=2, conflicts=conflicts)) # 11.5
print(gpdiv(scores, k=2, conflicts=conflicts)) # 11.0: A and D
greedy = select(scores, k=2, conflicts=conflicts, method="greedy")
indices are zero-based input positions, sorted by descending score, then input
position. Index a Python list with [items[i] for i in result.indices], or a
DataFrame with df.iloc[list(result.indices)]. Scores are utilities: for
lower-is-better docking energies, pass their negatives.
From molecules or custom similarities
Install RDKit support with pip install 'opdiv[chem]'.
from opdiv import select, tanimoto_similarity
smiles = ["CCO", "CCCO", "c1ccccc1", "CC(=O)O"]
scores = [0.9, 0.8, 0.7, 0.6]
similarities = tanimoto_similarity(smiles) # Morgan radius 2, 2048 bits, no chirality
result = select(scores, k=2, similarities=similarities, max_similarity=0.4)
selected_smiles = [smiles[i] for i in result.indices]
Any finite symmetric similarity matrix works, including shape or electrostatic comparisons. A pair conflicts exactly when similarity > max_similarity; equality is allowed, and no numerical epsilon is added. The similarity diagonal is ignored. Asymmetric matrices must be explicitly symmetrized before use.
Alternatively, supply a symmetric boolean/0–1 conflicts matrix with a false
diagonal. For distances, construct conflicts with distances < min_distance
and set the diagonal to false. Use <= if equality should conflict. With neither
matrix, select(scores, k) returns ordinary top-k.
Filter ineligible candidates and resolve molecular identity before selection. The SMILES helper rejects invalid/empty molecules and duplicate canonical isomeric SMILES; it does not normalize salts or tautomers. Matrix-based calls assume each row is a distinct eligible candidate.
Feasibility and bounds
result = select(scores, k=2, similarities=similarities,
max_similarity=0.4, time_limit=10.0)
print(result.status, result.value, result.upper_bound, result.gap)
| Status | Meaning |
|---|---|
optimal |
Optimum certified within the numerical tolerance below; inspect gap. |
feasible |
Full portfolio found; its mean is a lower bound on OPDiv. |
incomplete |
Greedy stopped short; another full portfolio may exist. |
infeasible |
The requested capacity is proven infeasible. |
unknown |
Search stopped without finding a full portfolio or proving infeasibility. |
value is the mean of a full selection, or None when underfilled. A partial
greedy result retains its indices, but has no GPDiv value at the requested k.
upper_bound bounds the optimum; gap is the absolute difference from value.
Neither repeating candidates nor relaxing the diversity threshold fills a result.
opdiv(...) raises MetricUndefinedError unless optimality is established;
gpdiv(...) raises it when its metric is undefined. The exception
has a .result containing the selection and available bounds. Use select(...)
when you want to inspect a time-limited result rather than require a scalar metric.
Optimal selection uses OR-Tools CP-SAT, with binary variables, exactly k selections, and one exclusion constraint per conflict edge. A complete greedy portfolio supplies a solver hint and an objective lower bound. One search worker and a fixed seed make runs reproducible within a solver version.
Scores are centered and rescaled before conversion to integer coefficients at
precision 1e-9 in normalized units. The largest k coefficient-rounding errors
are included in the returned upper bound. optimal means CP-SAT proved the
integer optimum and the original-score mean gap is at most 1e-8 times the
centered score scale (the largest absolute centered score, or 1 for constant
scores), subject to floating-point arithmetic. A small nonzero gap can remain
due to rounding; extremely close portfolios can be indistinguishable at this
precision. Returned value always uses the original scores.
There is no default time limit; time_limit limits the solver only. On a stopped
search, a complete greedy portfolio is retained if it beats the solver's candidate.
Greedy ties follow input order. Equally optimal selections can differ between
solver versions. Pairwise matrices require O(m²) memory; difficult conflict graphs
can be expensive to optimize.
This package uses the paper's CP-SAT backend and OPDiv/GPDiv definitions. It keeps
a direct full-graph model; the experiment runner's clique compression, score-prefix
relaxations, and benchmark pipeline are not included. Its normalized integer
scaling also differs from the experiment runner's fixed score multiplier.
For reproduction, use identical eligible candidates, row ordering, scores, and
conflict graphs: experiment-specific threshold tolerances must be encoded in
conflicts explicitly. The molecular helper matches the paper's non-chiral
fingerprint settings; compute fingerprints from the original archived graphs
when reproducing the archive experiments.
Development
python -m pytest
ruff check .
python -m build
python -m twine check dist/*
Tests compare optimal results and rounding-aware bounds with exhaustive enumeration, exercise the paper's greedy failure, and cover threshold boundaries, negative utilities, time-limit outcomes, input validation, and optional molecular support.
The source repository is mireklzicar/opdiv; Deep-MedChem/opdiv is its organization fork.
Attribution
The portfolio metrics and their molecular evaluation application are introduced in the paper above. The underlying graph optimization and score-ordered greedy algorithms are established methods; this package does not claim their invention. See the paper for the scientific formulation and prior work. MIT licensed.
Metadata
Release files for opdiv 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| opdiv-0.1.0.tar.gz | 15.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| opdiv-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.8 kB
Release files / opdiv-0.1.0.tar.gz
| Download URL | opdiv-0.1.0.tar.gz |
|---|---|
| Size | 15.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
46ffb342691d495da30b38c3452bc1850870aede6480cf8173240b756cd38f9c
|
|
BLAKE2b-256 checksum How to use checksums |
596392dbe2385305e26ff0c7284376c4c1643eefbab6ad4229661d22117b16c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / opdiv-0.1.0-py3-none-any.whl
| Download URL | opdiv-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe2b3007ac521bfb0deb473b1c53d78303d8d86b632c69061b0940d03b42c6b3
|
|
BLAKE2b-256 checksum How to use checksums |
18281eaf7794eaa3ea581190dfa859d51247865166cea91bd960272ac4f6ad9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|