Starling oral-bioavailability transfer model
Given two molecules (SMILES + study metadata) and molecule A's measured oral bioavailability, this model predicts whether oral-bioavailability behavior transfers from A to B — i.e. whether the two molecules behave similarly under the given study context. It is self-contained: the frozen encoders are bundled with the trained head, so it runs end-to-end on raw inputs.
Architecture
Per molecule (siamese — the same encoders + projections are applied to A and B; only the head is position-aware):
- Molecule encoder —
ibm-research/MoLFormer-XL-both-10pct(MolFormer-XL), frozen: SMILES → mean-pooled token embedding → 768-d, then a 2-layer MLP (768→384→256). - Metadata encoder —
sentence-transformers/all-MiniLM-L6-v2(MiniLM), frozen: each of the 7 metadata fields is embedded separately (mean-pooled, L2-normalized) → 384-d, then a learned per-field projection → 32-d (7×32 = 224-d total). A missing/empty field uses a learned per-field "missing" embedding instead of the text embedding, so absent metadata is handled gracefully and distinctly from any real value. - Per molecule =
[mol_mlp (256) | metadata (224)]= 480-d.
Pair head:
- Concatenate
[z_A, z_B]→ 960-d input. - A pre-norm residual SwiGLU MLP (6 blocks, width 384, FFN 1024) → one logit.
sigmoid(logit)= P(transfer). ~8M trainable params; encoders frozen.
Metadata fields (order matters)
molecule_name, species_or_population, dose, oral_exposure_mode, qualifying_conditions, comparator, extra_details
Pass a dict per molecule keyed by these names. Omit a key, or pass None/"", for a missing field
— the model then uses its learned per-field "missing" embedding.
Usage
Install the runtime dependencies:
pip install torch transformers safetensors sentence-transformers
Load the model with trust_remote_code=True. The repository includes the custom model wrapper plus
the frozen MolFormer and MiniLM encoders, so inference runs directly from raw SMILES and metadata.
from transformers import AutoModel
repo_id = "jiosephlee/starling-transfer-shared-eval-same-species-v2-no-source-value-small-lt10m-v1-best-knn-val1"
model = AutoModel.from_pretrained(repo_id, trust_remote_code=True).eval()
out = model(
smiles_a=["CC(=O)Oc1ccccc1C(=O)O"], # molecule A (bioavailability known)
smiles_b=["CCO"], # molecule B (candidate)
metadata_a=[{"species_or_population": "human", "dose": "325 mg", "oral_exposure_mode": "tablet"}],
metadata_b=[{"species_or_population": "human"}], # missing fields are fine
# no source_value argument for this variant
)
p_transfer = out.logits.sigmoid()
print(float(p_transfer[0]))
Inputs are batched lists of equal length. For each pair, pass:
smiles_a: SMILES for molecule A.smiles_b: SMILES for molecule B.metadata_a: dicts for molecule A metadata.metadata_b: dicts for molecule B metadata.
This is the no-source-value variant, so do not pass source_value.
Supported metadata keys are:
[
"molecule_name",
"species_or_population",
"dose",
"oral_exposure_mode",
"qualifying_conditions",
"comparator",
"extra_details",
]
You may omit unknown fields, or set them to None or "". The model has learned missing-field
embeddings for those cases.
The output is a logit per pair. Use out.logits.sigmoid() to get P(transfer), where higher values
mean the model predicts that oral-bioavailability behavior transfers from molecule A to molecule B
under the provided study context.
Example batched inference:
out = model(
smiles_a=["CCO", "CCN"],
smiles_b=["CCC", "CCCl"],
metadata_a=[
{"species_or_population": "rat", "oral_exposure_mode": "oral gavage"},
{"species_or_population": "human", "dose": "50 mg"},
],
metadata_b=[
{"species_or_population": "rat"},
{"species_or_population": "human", "dose": "100 mg"},
],
)
probs = out.logits.sigmoid().tolist()
Training & performance
Trained on the same_species_v2 no-source-value shared-eval split. The frozen embeddings were
precomputed once and the head was trained on top. The label is |value_A - value_B| thresholded.
This no-source-value variant learns transfer from molecule structure and metadata only.
This upload is the checkpoint selected by record_knn_validation_1_macro_f1, not the checkpoint
selected by assay-transfer validation macro-F1.
Checkpoint provenance:
- Universe:
same_species_v2 - Lane:
no_source_value - Model:
small_lt10m_v1 - Checkpoint directory:
best_record_knn_validation_1_macro_f1 - Selection metric:
record_knn_validation_1_macro_f1 = 0.704843 - Selection step:
1000
Record-KNN final evaluation on condition_key_v3_record_splits_hf/full_metadata:
- validation_1: macro-F1
0.699383, accuracy0.733871 - validation_2: macro-F1
0.766394, accuracy0.796000
The source run and full checkpoint comparison are included in checkpoint_metadata.json.