Skip to content

fix(drug_discovery): fingerprint readouts leave padding atoms out - #46

Merged
mahdi-shafiei merged 2 commits into
mainfrom
fix/masked-fingerprint-readout
Nov 8, 2026
Merged

mahdi-shafiei merged 2 commits into
mainfrom
fix/masked-fingerprint-readout

Conversation

@mahdi-shafiei

Copy link
Copy Markdown
Collaborator

Summary

 _compute_differentiable_fp (CircularFingerprintOperator, MACCSKeysOperator)
   node_hidden = message_passing(...)
-  node_hidden = node_hidden * node_mask[:, None]     # zero rows, then...
   logits = biased layers(node_hidden)                # ...bias makes them nonzero
   bits = soft_ops.greater(logits, 0)
-  fingerprint = soft_ops.max(bits, axis=0)           # padding atoms in the max
+  fingerprint = soft_ops.max(bits, axis=0, where=node_mask > 0)

Two commits:

  1. soft_ops.max and soft_ops.argmax gain where, with the semantics of jax.nn.standardize(where=) and jax.nn.softmax(where=): an excluded entry takes no part in the standardization statistics and gets zero weight and zero gradient. Smooth-mode proj_simplex is jax.nn.softmax(x / softness), so the masked form is JAX's own. It's supported for mode="hard" and for method="softsort" in smooth mode. neuralsort, the sorting network, optimal transport and the sparse simplex modes refuse where, because no implementation found (softjax, fast-soft-sort, OTT, the SoftSort and NeuralSort code) has a masked form of them.
  2. The two fingerprint readouts use it.

Evidence

Before, padded-and-masked against unpadded output for every drug-discovery graph operator (ethanol and benzene padded to 13 atoms):

operator before after
CircularFingerprint 0.135 1.8e-7
MACCSKeys 0.058 8.9e-8
AttentiveFP (already masks its scores) 0.0 unchanged
Property / ADMET predictors (sum and mean readouts) ~1e-6 unchanged

The remaining 1e-7 is float32 summation order over a longer axis, not padding.

Tests, written first:

  • tests/core/test_soft_ops/test_masked_max.py (8). Inputs whose means, variances and softmax denominators are exact under any summation order, so the comparisons are exact: masked soft argmax, soft max, per-row masks, hard mode, zero gradient for excluded entries, and refusal for unsupported methods. Before: 8 failed (no where). After: 8 passed.
  • tests/operators/drug_discovery/test_padded_readout.py (2). Padded versus unpadded methanol, exact by construction: parameters from {-1/4, 0, 1/4}, one-hot features, and two heavy atoms, so every sum over atoms has at most two nonzero terms. Against main's code: 15 of 16 bits differ in both operators. After: equal bit for bit.
  • An earlier draft of this test, "the fingerprint ignores what the padding atoms hold", passed on main too. Main already zeroes the padding rows; the leak is the biased layers' constant output. It was replaced by the comparison above.

Gate: the soft-op, drug-discovery, DTI and MolNet benchmark tests pass 587 of 587 on CPU. The first commit was gated on its own exported index (511 soft-op and drug-discovery tests). pre-commit run --all-files passes all 24 hooks.

Merge Danger

Door: two-way.

Blast Radius: padded fingerprints.

  • What changes: fingerprints computed on padded input, which means every DTI drug encoding (dti.py indexes padded batch rows) and any user code that pads. Unpadded fingerprints don't change.
  • New keyword: where is keyword-only, and every existing soft_ops.max and argmax call works as before.
  • Not in this PR: protein/secondary_structure.py:367-371 zeroes local H-bonds before a soft max, the same defect class, measured at up to 0.069 on a random map. It's a different operator, and a residue with no non-local partner needs handling, so it gets its own PR.

soft_ops.max and soft_ops.argmax reduce over every entry along the axis:
the soft argmax standardizes the axis and projects it onto the simplex,
so a padded or excluded entry shifts the mean and variance and takes
weight. They now take where, a boolean mask of the entries to reduce
over, with the semantics of jax.nn.standardize(where=) and
jax.nn.softmax(where=): an excluded entry takes no part in the
statistics and gets zero weight and zero gradient, so a padded axis
gives the unpadded result. standardize_and_squash and proj_simplex take
the same mask.

Supported for mode="hard" (jnp.max with where and initial) and for
method="softsort" in smooth mode, the default; neuralsort, the sorting
network, optimal transport and the sparse simplex modes refuse where,
since no published implementation of them has a masked form.
The differentiable circular and MACCS fingerprints multiplied the
padding atoms' hidden states by the node mask and then passed them
through biased layers (the hash network and the pattern detectors), so
every padding atom entered the soft max over atoms with nonzero bits.
Ethanol and benzene padded to 13 atoms read up to 0.135 (circular) and
0.058 (MACCS) away from their unpadded fingerprints, so a DTI batch's
drug encodings depended on the largest molecule in the batch.

The readout now reduces over the real atoms only, with
soft_ops.max(..., where=node_mask), the mask sitting at the reduction as
in jax.nn.softmax(where=). Message passing already gives real atoms an
exact zero from padding atoms, which have no bonds. A test makes the
padded-versus-unpadded comparison exact by construction (parameters from
{-1/4, 0, 1/4}, a two-atom molecule) and fails on the old readout in 15
of 16 bits.
@mahdi-shafiei
mahdi-shafiei marked this pull request as ready for review November 8, 2026 15:32
@mahdi-shafiei
mahdi-shafiei merged commit 53ed19e into main Nov 8, 2026
14 checks passed
@mahdi-shafiei
mahdi-shafiei deleted the fix/masked-fingerprint-readout branch November 8, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant