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
11 changes: 6 additions & 5 deletions docs/dd/ref/ligand.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@

## ADMET (molprops) attributes

After you run [`Molprops`](../how-to/ligands.md#predicting-admet-properties) on the ligand, scalar and structured predictions from the Deep Origin molprops tools are stored on the ligand as well as in `properties` and RDKit properties:
After you run [`Molprops`](../how-to/ligands.md#predicting-admet-properties) on the ligand, or load the ligand with `Ligand.from_id` / `LigandSet.from_ids` when values already exist on the platform record, scalar predictions are stored on dedicated attributes as well as in `properties` (tool row keys such as `logS`, `cyp2d6`):

- `log_s`, `log_d`, `log_p` — map to API keys `logS`, `logD`, `logP`
- `herg`, `cyp`, `ames` — nested structures keyed as `hERG`, `cyp`, `ames` in the API response
- `has_pains`, `pains_fragments` — PAINS screening results
- `log_s`, `log_d`, `log_p` — map to tool keys `logS`, `logD`, `logP` (platform columns `logs_predicted`, `logd_predicted`, `log_p`)
- `ames_probability`, `herg_inhibition_probability` — from `ames_probability`, `herg_probability` on the platform record
- `cyp_1a2`, `cyp_2c9`, `cyp_2c19`, `cyp_2d6`, `cyp_3a4` — from `cyp1a2`, …, `cyp3a4`
- `has_pains`, `pains_fragments` — PAINS screening (`pains_flag` on the platform record maps to `has_pains`)

Until molprops has been run successfully (via `Molprops`), these fields remain `None`.
Until molprops has been run or the platform record has pinned values, these fields remain `None`. `pains_fragments` is only available from a fresh `Molprops` run, not from `from_id`.

## Preparation

Expand Down
57 changes: 19 additions & 38 deletions docs/notebooks/clean/molprops.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b9118071-7e60-44bb-86a9-f08f1392e39a",
"cell_type": "markdown",
"id": "4fd04396-e167-48ef-bc6c-bf43720da753",
"metadata": {},
"outputs": [],
"source": [
"from deeporigin import config\n",
"config.set_env(\"dev\")"
"# ADME properties \n",
"\n",
"## Load a ligand set\n",
"\n",
"Here we load the BRD data set. Note that they have a custom column (`r_exp_dg`)"
]
},
{
Expand All @@ -58,23 +59,23 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "06c7fd76-0498-42bb-8578-79b089962062",
"cell_type": "markdown",
"id": "ad2aacfc-c0cb-4b80-96c5-13e04def0cfe",
"metadata": {},
"outputs": [],
"source": [
"mp = Molprops(ligands=ligands)\n",
"mp.tool_version = \"0.5.0\""
"## Estimate and run molprops\n",
"\n",
"In this section we create a `Molprops` object and use it to estimate and run these ligands:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "331a1b15-34f0-4bb6-99f3-2b15ac9d134a",
"id": "06c7fd76-0498-42bb-8578-79b089962062",
"metadata": {},
"outputs": [],
"source": [
"mp = Molprops(ligands=ligands)\n",
"mp.properties"
]
},
Expand Down Expand Up @@ -119,42 +120,22 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7ef3da46-e230-489b-89a9-641ec446a0ef",
"metadata": {},
"outputs": [],
"source": [
"client.entities.get_ligand(\"08BT9WM0NYVFY\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cf0c3209-c4b0-4d46-a5db-850dc388aaaf",
"cell_type": "markdown",
"id": "7c2bd6e1-9c4c-49fe-84db-8c3674659d9a",
"metadata": {},
"outputs": [],
"source": [
"client.results.get(compute_job_id=\"824579ec-312b-481f-8147-23526268c307\")"
"Note that computing molprops on ligands inserts those molprops into the Ligands table; this can be verifief by fetching the Ligand from the ligand table:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c83e6607-2ff8-4048-8b9f-e0272aee429e",
"id": "935858f5-9c6b-43c4-ac2f-95f1bda432f5",
"metadata": {},
"outputs": [],
"source": [
"client.user_logs.search(\"824579ec-312b-481f-8147-23526268c307\")"
"Ligand.from_id(ligands[0].id).properties"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "935858f5-9c6b-43c4-ac2f-95f1bda432f5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
23 changes: 23 additions & 0 deletions docs/notebooks/clean/test.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "58e2265c-01c2-4dc4-ac52-85ca285085bd",
"metadata": {},
"outputs": [],
"source": [
"from deeporigin.platform import DeepOriginClient\n",
"\n",
"client = DeepOriginClient()\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9a73ec46-8d6e-4a28-9e2e-064bb55a398d",
"metadata": {},
"outputs": [],
"source": [
"client.files.download_many(files=client.files.list(\"tool-runs/eb2d0ace-8637-485b-be50-3d9159cbfd2a\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
12 changes: 7 additions & 5 deletions src/drug_discovery/molprops.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,10 @@ class Molprops(Execution, SyncExecutableMixin):
in place via
:meth:`~deeporigin.drug_discovery.structures.ligand.Ligand._apply_molprops_result`.

This is a blocking flow. A normal :meth:`run` does not call
:meth:`~deeporigin.drug_discovery.execution.Execution.update_from_dto`; use
:meth:`run` with ``quote=True`` when you need the platform execution ``id``,
``status``, and :attr:`~deeporigin.drug_discovery.execution.Execution.estimate`
from a single quotation for all ligands.
This is a blocking flow. :meth:`run` refreshes ``id``, ``status``, and related
execution fields from the platform response via
:meth:`~deeporigin.drug_discovery.execution.Execution.update_from_dto`
(the last batch when :attr:`batch_size` splits the run).

Attributes:
ligands: Ligands to predict (same order as SMILES sent to the API).
Expand Down Expand Up @@ -288,6 +287,9 @@ def run(self, *, quote: bool = False) -> Molprops:
raw_responses.append(batch_raw)
pbar.update(len(batch_ligands))

if raw_responses:
self.update_from_dto(raw_responses[-1])

total_cost = 0.0
any_priced = False
for raw in raw_responses:
Expand Down
76 changes: 67 additions & 9 deletions src/drug_discovery/structures/ligand.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,43 @@
"pains_fragments": "pains_fragments",
}

# Molprops API row keys that must not be copied into ``properties`` (entity fields
# or merge keys; see :func:`ligands_to_dataframe` column layout).
_MOLPROPS_ROW_SKIP_PROPERTY_KEYS: frozenset[str] = frozenset(
{
"id",
"ligand_id",
"smiles",
"SMILES",
}
)

# Platform ligand pinned columns → molprops combined-tool row keys.
# Mirrors ``pinned-columns.registry.ts`` on the data platform.
_PLATFORM_PINNED_TO_MOLPROPS_ROW: dict[str, str] = {
"log_p": "logP",
"pains_flag": "has_pains",
"logd_predicted": "logD",
"logs_predicted": "logS",
"ames_probability": "ames_probability",
"herg_probability": "herg_inhibition_probability",
"cyp1a2": "cyp1a2",
"cyp2c9": "cyp2c9",
"cyp2c19": "cyp2c19",
"cyp2d6": "cyp2d6",
"cyp3a4": "cyp3a4",
}


def _molprops_row_from_platform_record(data: dict[str, Any]) -> dict[str, Any]:
"""Translate pinned molprops columns on a platform ligand record to tool row keys."""

row: dict[str, Any] = {}
for platform_key, tool_key in _PLATFORM_PINNED_TO_MOLPROPS_ROW.items():
if platform_key in data and data[platform_key] is not None:
row[tool_key] = data[platform_key]
return row


def _sdf_marker_present_in_prefix(path: Path, *, max_bytes: int = 16384) -> bool:
"""Return True if the start of the file looks like MOL/SDF text."""
Expand Down Expand Up @@ -543,6 +580,10 @@ def _from_platform_record(
) -> Self:
"""Create a Ligand instance from a platform ligand record.

When the record includes pinned molprops columns (``log_p``, ``cyp2d6``,
``logs_predicted``, etc.), they are applied to ADMET attributes and
:attr:`properties` via :meth:`_apply_molprops_result`.

Args:
data: Ligand record returned by the platform entities API.
client: DeepOrigin client used to download associated files.
Expand Down Expand Up @@ -578,6 +619,9 @@ def _from_platform_record(
ligand.name = data["name"]
if data.get("project_id") is not None:
ligand.project_id = str(data["project_id"])
molprops_row = _molprops_row_from_platform_record(data)
if molprops_row:
ligand._apply_molprops_result(molprops_row)
return ligand

if mol_file:
Expand All @@ -600,6 +644,10 @@ def _from_platform_record(
if data.get("project_id") is not None:
ligand.project_id = str(data["project_id"])

molprops_row = _molprops_row_from_platform_record(data)
if molprops_row:
ligand._apply_molprops_result(molprops_row)

return ligand

@classmethod
Expand Down Expand Up @@ -1516,6 +1564,8 @@ def _apply_molprops_result(self, props: dict[str, Any]) -> None:
if api_key in props:
setattr(self, attr_name, props[api_key])
for key, value in props.items():
if key in _MOLPROPS_ROW_SKIP_PROPERTY_KEYS:
continue
self.set_property(key, value)

def update_coordinates(self, coordinates: np.ndarray):
Expand Down Expand Up @@ -1631,26 +1681,34 @@ def ligands_to_dataframe(ligands: list[Ligand]) -> pd.DataFrame:
ligands: Ligands to serialize as rows.

Returns:
DataFrame with an ``id`` column, union of property columns (except
``_Name``, ``_SMILES``, ``initial_smiles``, ``id``), and ``SMILES``.
DataFrame with columns ``id``, ``SMILES``, known molprops output keys
(in schema order when present), then any other property keys sorted.
"""

# find the union of all properties in all ligands
all_keys = set()
skip_property_keys = {
"_Name",
"_SMILES",
"initial_smiles",
} | _MOLPROPS_ROW_SKIP_PROPERTY_KEYS

all_keys: set[str] = set()
for ligand in ligands:
all_keys.update(ligand.properties.keys())

property_keys = sorted(k for k in all_keys if k not in skip_property_keys)
molprops_keys = [k for k in _MOLPROPS_RESPONSE_TO_ATTR if k in property_keys]
other_keys = sorted(set(property_keys) - set(molprops_keys))

property_columns: dict[str, list[Any]] = {}
for key in all_keys:
if key in ("_Name", "_SMILES", "initial_smiles", "id"):
continue
for key in molprops_keys + other_keys:
property_columns[key] = [ligand.properties.get(key, None) for ligand in ligands]

data: dict[str, list[Any]] = {"id": [ligand.id for ligand in ligands]}
data.update(property_columns)
data["SMILES"] = [ligand.smiles for ligand in ligands]
data.update(property_columns)

return pd.DataFrame(data)
column_order = ["id", "SMILES", *molprops_keys, *other_keys]
return pd.DataFrame(data)[column_order]


def _tool_user_inputs_params(dto: dict[str, Any]) -> dict[str, Any]:
Expand Down
41 changes: 41 additions & 0 deletions tests/test_ligand.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,47 @@ def test_ligand_property_management():
assert ligand.get_property("non_existent") is None


def test_ligand_from_platform_record_hydrates_molprops():
"""Pinned platform molprops columns map to tool row keys and ADMET attrs."""
from deeporigin.drug_discovery.structures.ligand import (
_molprops_row_from_platform_record,
)

data = {
"id": "0940WE9SZH0VC",
"smiles": "COCCn1cc(-c2cccc(C(=O)N(C)C)c2)c2cc[nH]c2c1=O",
"log_p": 1.377760887145996,
"logs_predicted": -3.0474026203155518,
"logd_predicted": 0.9802079200744629,
"pains_flag": True,
"ames_probability": 0.0012685793917626143,
"herg_probability": 0.17195460200309753,
"cyp2d6": 0.012631930410861969,
"cyp3a4": 0.1912640929222107,
}
row = _molprops_row_from_platform_record(data)
assert row["logP"] == pytest.approx(1.377760887145996)
assert row["logS"] == pytest.approx(-3.0474026203155518)
assert row["logD"] == pytest.approx(0.9802079200744629)
assert row["has_pains"] is True
assert row["herg_inhibition_probability"] == pytest.approx(0.17195460200309753)

ligand = Ligand.from_smiles(data["smiles"])
ligand._apply_molprops_result(row)

assert ligand.log_p == pytest.approx(1.377760887145996)
assert ligand.log_s == pytest.approx(-3.0474026203155518)
assert ligand.log_d == pytest.approx(0.9802079200744629)
assert ligand.has_pains is True
assert ligand.ames_probability == pytest.approx(0.0012685793917626143)
assert ligand.herg_inhibition_probability == pytest.approx(0.17195460200309753)
assert ligand.cyp_2d6 == pytest.approx(0.012631930410861969)
assert ligand.cyp_3a4 == pytest.approx(0.1912640929222107)
assert ligand.properties["cyp2d6"] == pytest.approx(0.012631930410861969)
assert ligand.properties["logS"] == pytest.approx(-3.0474026203155518)
assert ligand.properties["logP"] == pytest.approx(1.377760887145996)


def test_to_sdf_requires_rehydration_when_remote_path_only():
"""to_sdf/to_file must not perform I/O; fail if remote_path set but no local file."""
ligand = Ligand.from_smiles("CCO", name="Ethanol")
Expand Down
25 changes: 25 additions & 0 deletions tests/test_ligand_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,31 @@ def test_ligandset_to_dataframe():
assert len(df) == 2
assert "SMILES" in df.columns
assert "logP" in df.columns
assert list(df.columns[:2]) == ["id", "SMILES"]


def test_ligandset_to_dataframe_after_molprops():
"""Molprops rows must not duplicate id/SMILES; columns are id, SMILES, then ADMET."""
from deeporigin.drug_discovery.structures.ligand import LigandSet

ligand = Ligand.from_smiles("CCO")
ligand.id = "0"
ligand._apply_molprops_result(
{
"ligand_id": "0",
"smiles": "CCO",
"logP": 1.2,
"cyp2c19": 0.5,
}
)

df = LigandSet(ligands=[ligand]).to_dataframe()

assert list(df.columns) == ["id", "SMILES", "logP", "cyp2c19"]
assert "ligand_id" not in df.columns
assert "smiles" not in df.columns
assert df.loc[0, "id"] == "0"
assert df.loc[0, "SMILES"] == "CCO"


def test_ligandset_indexing_and_slicing():
Expand Down
Loading
Loading