Skip to main content

OPDiv

PyPI

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)

Source distribution for opdiv 0.1.0
File Size Uploaded
opdiv-0.1.0.tar.gz 15.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for opdiv 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page