Skip to content

Commit 14a1209

Browse files
committed
Add: concise __repr__ to data model classes and Thermodynamics
The frozen dataclasses in models.py (PoseResult, BindingModeResult, DockingResult) relied on auto-generated __repr__ which dumped all fields including the full remarks dict and Path objects, making them unreadable in interactive Python sessions. Added concise __repr__ methods showing only key identifiers and scores. Also added __repr__ to Thermodynamics in thermodynamics.py for consistency with the existing repr style in docking.py classes. https://claude.ai/code/session_01LFSRF3YrvmxjcGBBJbvgLv
1 parent af69aee commit 14a1209

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

python/flexaidds/models.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from typing import Any, Dict, List, Optional, Union
2323

2424

25-
@dataclass(frozen=True)
25+
@dataclass(frozen=True, repr=False)
2626
class PoseResult:
2727
"""A single docked pose read from one FlexAID∆S output PDB file.
2828
@@ -62,8 +62,17 @@ class PoseResult:
6262
temperature: Optional[float] = None
6363
remarks: Dict[str, Any] = field(default_factory=dict)
6464

65+
def __repr__(self) -> str:
66+
parts = [f"mode={self.mode_id}", f"rank={self.pose_rank}"]
67+
if self.cf is not None:
68+
parts.append(f"cf={self.cf:.3f}")
69+
if self.free_energy is not None:
70+
parts.append(f"F={self.free_energy:.3f}")
71+
parts.append(f"path='{self.path.name}'")
72+
return f"<PoseResult {' '.join(parts)}>"
6573

66-
@dataclass(frozen=True)
74+
75+
@dataclass(frozen=True, repr=False)
6776
class BindingModeResult:
6877
"""A cluster of docked poses that share a common binding geometry.
6978
@@ -129,8 +138,17 @@ def best_pose(self) -> Optional[PoseResult]:
129138
return min(scored, key=lambda p: p.cf_app)
130139
return self.poses[0] if self.poses else None
131140

141+
def __repr__(self) -> str:
142+
parts = [f"id={self.mode_id}", f"rank={self.rank}",
143+
f"n_poses={self.n_poses}"]
144+
if self.free_energy is not None:
145+
parts.append(f"F={self.free_energy:.3f}")
146+
if self.best_cf is not None:
147+
parts.append(f"best_cf={self.best_cf:.3f}")
148+
return f"<BindingModeResult {' '.join(parts)}>"
149+
132150

133-
@dataclass(frozen=True)
151+
@dataclass(frozen=True, repr=False)
134152
class DockingResult:
135153
"""Top-level container for a complete FlexAID∆S docking run.
136154
@@ -174,6 +192,13 @@ def top_mode(self) -> Optional[BindingModeResult]:
174192
return min(free_modes, key=lambda m: m.free_energy)
175193
return min(self.binding_modes, key=lambda m: m.rank)
176194

195+
def __repr__(self) -> str:
196+
parts = [f"n_modes={self.n_modes}"]
197+
if self.temperature is not None:
198+
parts.append(f"T={self.temperature:.1f}K")
199+
parts.append(f"source='{self.source_dir.name}'")
200+
return f"<DockingResult {' '.join(parts)}>"
201+
177202
def to_records(self) -> List[Dict[str, Any]]:
178203
"""Serialise all binding modes to a list of flat dictionaries.
179204

python/flexaidds/thermodynamics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ def entropy_term(self) -> float:
5959
"""Entropic contribution to free energy: TΔS (kcal/mol)."""
6060
return self.temperature * self.entropy
6161

62+
def __repr__(self) -> str:
63+
return (
64+
f"<Thermodynamics T={self.temperature:.1f}K "
65+
f"F={self.free_energy:.3f} H={self.mean_energy:.3f} "
66+
f"S={self.entropy:.6f} Cv={self.heat_capacity:.6f}>"
67+
)
68+
6269
def to_dict(self) -> dict:
6370
"""Convert to dictionary for serialization."""
6471
return {

python/tests/test_results_loader_models.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pathlib import Path
22

3+
from flexaidds.models import BindingModeResult, DockingResult, PoseResult
34
from flexaidds.results import load_results
45

56

@@ -89,3 +90,72 @@ def test_load_results_promotes_frequency_metadata_to_mode_level(tmp_path: Path)
8990
assert mode.n_poses == 2
9091
assert mode.frequency == 7
9192
assert mode.metadata["frequency"] == 7
93+
94+
95+
# ── __repr__ tests ───────────────────────────────────────────────────────────
96+
97+
98+
class TestPoseResultRepr:
99+
def test_repr_with_all_scores(self):
100+
pose = PoseResult(
101+
path=Path("/tmp/mode1_pose1.pdb"),
102+
mode_id=1, pose_rank=2, cf=-42.5, free_energy=-41.0,
103+
)
104+
r = repr(pose)
105+
assert r.startswith("<PoseResult")
106+
assert "mode=1" in r
107+
assert "rank=2" in r
108+
assert "cf=-42.500" in r
109+
assert "F=-41.000" in r
110+
assert "mode1_pose1.pdb" in r
111+
112+
def test_repr_without_optional_scores(self):
113+
pose = PoseResult(path=Path("/tmp/test.pdb"), mode_id=3, pose_rank=1)
114+
r = repr(pose)
115+
assert "mode=3" in r
116+
assert "cf=" not in r
117+
assert "F=" not in r
118+
assert "test.pdb" in r
119+
120+
121+
class TestBindingModeResultRepr:
122+
def test_repr_with_free_energy(self):
123+
pose = PoseResult(path=Path("/tmp/p.pdb"), mode_id=1, pose_rank=1)
124+
mode = BindingModeResult(
125+
mode_id=1, rank=2, poses=[pose],
126+
free_energy=-41.0, best_cf=-42.5,
127+
)
128+
r = repr(mode)
129+
assert "id=1" in r
130+
assert "rank=2" in r
131+
assert "n_poses=1" in r
132+
assert "F=-41.000" in r
133+
assert "best_cf=-42.500" in r
134+
135+
def test_repr_without_optional_fields(self):
136+
mode = BindingModeResult(mode_id=5, rank=1, poses=[])
137+
r = repr(mode)
138+
assert "id=5" in r
139+
assert "n_poses=0" in r
140+
assert "F=" not in r
141+
142+
143+
class TestDockingResultRepr:
144+
def test_repr_with_temperature(self):
145+
result = DockingResult(
146+
source_dir=Path("/tmp/docking_run"),
147+
binding_modes=[], temperature=300.0,
148+
)
149+
r = repr(result)
150+
assert "n_modes=0" in r
151+
assert "T=300.0K" in r
152+
assert "docking_run" in r
153+
154+
def test_repr_without_temperature(self):
155+
result = DockingResult(
156+
source_dir=Path("/data/results"), binding_modes=[],
157+
)
158+
r = repr(result)
159+
assert "n_modes=0" in r
160+
assert "T=" not in r
161+
assert "results" in r

python/tests/test_thermodynamics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ def test_thermodynamics_dataclass_legacy():
133133
assert d["entropy_kcal_mol_K"] == 0.001667
134134

135135

136+
def test_thermodynamics_repr():
137+
"""Thermodynamics __repr__ includes key thermodynamic quantities."""
138+
from flexaidds.thermodynamics import Thermodynamics
139+
140+
thermo = Thermodynamics(
141+
temperature=300.0, log_Z=10.0, free_energy=-5.0,
142+
mean_energy=-4.5, mean_energy_sq=20.5,
143+
heat_capacity=0.1, entropy=0.001667, std_energy=0.3,
144+
)
145+
r = repr(thermo)
146+
assert r.startswith("<Thermodynamics")
147+
assert "T=300.0K" in r
148+
assert "F=-5.000" in r
149+
assert "H=-4.500" in r
150+
assert "S=0.001667" in r
151+
assert "Cv=0.100000" in r
152+
153+
136154
# ─────────────────────────────────────────────────────────────────────────────
137155
# Tests that require the compiled C++ extension
138156
# ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)