Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ ligands) registered via **Pose registration**.
_Avoid_: using `Ligand` when you mean a pose result with pose-scoped identity;
conflating with docking-only outputs

**Pose origin**:
Public provenance of a **Pose**: `cocrystal`, `docked`, `registered`, or
`manual`. The client presents legacy stored `crystal_extract` as canonical
`cocrystal`; unknown future values remain visible for forward compatibility.
_Avoid_: exposing `crystal_extract` as a separate public provenance; rejecting
an otherwise readable Pose solely because its origin is newer than the client

**Panel pose**:
A docked pose of one ligand into one member of the Secondary Pharmacology
**Panel**, stored under `result_type=panelpose` rather than `pose`. Names the
Expand Down
5 changes: 5 additions & 0 deletions docs/dd/ref/pose.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ A **Pose** is a 3D ligand conformation stored in the platform pose result table
(`result_type=pose`). It has its own platform **pose id** (`Pose.id`) and a
parent **ligand id** (`Pose.ligand_id`) in the ligands table.

**Pose origin** known values include `cocrystal`, `docked`, `registered`, and
`manual`. Older indexed rows may still store `crystal_extract`; the client
reads that as `cocrystal`. Unknown origin strings are preserved for forward
compatibility.

Use :class:`Pose` / :class:`PoseSet` when you need pose-scoped identity.
:meth:`~deeporigin.drug_discovery.Docking.get_results` and
:meth:`~deeporigin.drug_discovery.Docking.get_poses` return :class:`PoseSet`.
Expand Down
8 changes: 6 additions & 2 deletions src/drug_discovery/protein_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@
from deeporigin.drug_discovery.notebook_watch_mixin import NotebookWatchMixin
from deeporigin.drug_discovery.structures.ligand import Ligand
from deeporigin.drug_discovery.structures.pocket import Pocket
from deeporigin.drug_discovery.structures.pose import Pose, PoseSet
from deeporigin.drug_discovery.structures.pose import (
Pose,
PoseSet,
normalize_pose_origin,
)
from deeporigin.drug_discovery.structures.protein import Protein
from deeporigin.exceptions import DeepOriginException
from deeporigin.platform.client import DeepOriginClient
Expand Down Expand Up @@ -733,7 +737,7 @@ def _crystal_pose_output_rows(rows: list[Any]) -> list[dict[str, Any]]:
for row in rows:
if not isinstance(row, dict):
continue
origin = str(row.get("origin") or "").strip()
origin = normalize_pose_origin(row.get("origin"))
if origin != _PROTEIN_PREP_CRYSTAL_POSE_ORIGIN:
continue
filtered.append(row)
Expand Down
33 changes: 31 additions & 2 deletions src/drug_discovery/structures/pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
from deeporigin.exceptions import DeepOriginException
from deeporigin.platform.client import DeepOriginClient

PoseOrigin = Literal["cocrystal", "docked", "registered"]
PoseOrigin = Literal["cocrystal", "docked", "registered", "manual"]

_LEGACY_POSE_ORIGIN_CRYSTAL_EXTRACT = "crystal_extract"
_CANONICAL_POSE_ORIGIN_COCRYSTAL = "cocrystal"

_POSE_RESULT_ID_POLL_SECONDS = 3.0
_POSE_RESULT_ID_POLL_INTERVAL = 0.5
Expand Down Expand Up @@ -94,6 +97,21 @@ def _strip_nonempty_str(value: Any) -> str | None:
return None


def normalize_pose_origin(value: PoseOrigin | str | None) -> PoseOrigin | str | None:
"""Return canonical pose provenance for client-facing :class:`Pose` objects.

Maps the legacy indexed alias ``crystal_extract`` to ``cocrystal``. Known and
unknown origin strings otherwise remain stripped and unchanged.
"""

text = _strip_nonempty_str(value)
if text is None:
return None
if text == _LEGACY_POSE_ORIGIN_CRYSTAL_EXTRACT:
return _CANONICAL_POSE_ORIGIN_COCRYSTAL
return text


def _path_points_to_existing_local_file(path: str) -> bool:
"""Return True if ``path`` refers to an existing regular file on disk."""

Expand Down Expand Up @@ -289,7 +307,9 @@ class Pose(Entity):
pose_score: Docking pose score when present.
binding_energy: Docking binding energy when present.
best_pose: Whether this row is the best pose for its ligand in a run.
origin: Platform pose provenance (``cocrystal``, ``docked``, ``registered``).
origin: Platform pose provenance (``cocrystal``, ``docked``, ``registered``,
``manual``). Legacy rows may store ``crystal_extract``; the client
presents that as ``cocrystal``.
component_id: Protein Prep Selection component id for cocrystal poses.
props: Additional metadata from the platform row.
"""
Expand All @@ -311,6 +331,15 @@ class Pose(Entity):
_preferred_ext: ClassVar[str] = ".sdf"
_project_name: str | None = field(default=None, repr=False, compare=False)

def __post_init__(self) -> None:
if self.origin is not None:
object.__setattr__(self, "origin", normalize_pose_origin(self.origin))

def __setattr__(self, name: str, value: object) -> None:
if name == "origin" and value is not None:
value = normalize_pose_origin(value) # type: ignore[arg-type]
super().__setattr__(name, value)

@property
def mol(self) -> Chem.Mol | None:
"""Return the RDKit molecule, loading from disk when needed."""
Expand Down
39 changes: 39 additions & 0 deletions tests/test_pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_optional_bool,
_optional_float,
_pose_row_from_registration_execution,
normalize_pose_origin,
)
from deeporigin.platform.client import DeepOriginClient
from deeporigin.platform.constants import TOOL_KEYS_AND_VERSIONS
Expand Down Expand Up @@ -57,6 +58,44 @@ def test_pose_from_json_remote_path_only_row() -> None:
assert pose.mol is None


@pytest.mark.parametrize(
("raw_origin", "expected"),
[
("crystal_extract", "cocrystal"),
("cocrystal", "cocrystal"),
("docked", "docked"),
("registered", "registered"),
("manual", "manual"),
("future_origin", "future_origin"),
],
)
def test_pose_origin_normalization_direct_and_json(
raw_origin: str,
expected: str,
) -> None:
"""Legacy crystal_extract and unknown origins follow the client contract."""
pose = Pose(ligand_id="L1", origin=raw_origin)
assert pose.origin == expected

hydrated = Pose.from_json(
[
{
"ligand_id": "L1",
"remote_path": "entities/poses/p1.sdf",
"origin": raw_origin,
}
]
)[0]
assert hydrated.origin == expected

pose.origin = raw_origin
assert pose.origin == expected


def test_normalize_pose_origin_none() -> None:
assert normalize_pose_origin(None) is None


def test_pose_from_json_coerces_metadata_fields() -> None:
"""Numeric and boolean platform fields are coerced on hydration."""
pose = Pose.from_json(
Expand Down
20 changes: 19 additions & 1 deletion tests/test_protein_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,24 @@ def test_crystal_poses_from_output_rows_hydrates_metadata(
assert poses[0].remote_path == "entities/ligands/extracted/exec/lig.sdf"


def test_crystal_poses_from_output_rows_accepts_legacy_crystal_extract_origin(
client: DeepOriginClient,
) -> None:
"""Legacy crystal_extract rows surface as canonical cocrystal poses."""
rows = [
{
"component_id": "ligand:LIG:A:100",
"file_path": "entities/ligands/extracted/exec/lig.sdf",
"ligand_id": "08LIGAND0001",
"origin": "crystal_extract",
"protein_id": "09PREPAREDPROTEIN",
}
]
poses = _crystal_poses_from_output_rows(rows, client=client)
assert len(poses) == 1
assert poses[0].origin == "cocrystal"


def test_crystal_poses_from_output_rows_rejects_non_cocrystal_origin(
client: DeepOriginClient,
) -> None:
Expand All @@ -1455,7 +1473,7 @@ def test_crystal_poses_from_output_rows_rejects_non_cocrystal_origin(
"component_id": "ligand:LIG:A:100",
"file_path": "entities/ligands/extracted/exec/lig.sdf",
"ligand_id": "08LIGAND0001",
"origin": "crystal_extract",
"origin": "docked",
"protein_id": "09PREPAREDPROTEIN",
}
]
Expand Down
Loading