Skip to content

Commit 6c201ee

Browse files
committed
fix(esmfold2): reuse native CIF occupancy-compatible reader
1 parent a700e96 commit 6c201ee

2 files changed

Lines changed: 93 additions & 8 deletions

File tree

‎platform/api/services/core_protein_result_contract.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,20 @@ def _structure_confidence(content: bytes, path: str):
8686
from io import StringIO
8787
import math
8888
from pathlib import Path
89-
from Bio.PDB import MMCIFParser, PDBParser
89+
from Bio.PDB import PDBParser
9090

9191
try:
9292
suffix = Path(path).suffix.lower()
9393
if suffix == '.pdb':
94-
parser = PDBParser(PERMISSIVE=False, QUIET=True)
94+
structure = PDBParser(PERMISSIVE=False, QUIET=True).get_structure(
95+
'candidate', StringIO(content.decode('utf-8')))
9596
elif suffix in {'.cif', '.mmcif'}:
96-
parser = MMCIFParser(QUIET=True)
97+
# Reuse the native predictor compatibility reader: absent optional
98+
# occupancy is supplied only in memory, never in the hashed bytes.
99+
from services.md.starting_structures import _parse_mmcif_structure
100+
structure = _parse_mmcif_structure(content)
97101
else:
98102
raise ValueError('unsupported structure format')
99-
structure = parser.get_structure('candidate', StringIO(content.decode('utf-8')))
100103
atoms = [atom for model in structure for chain in model
101104
for residue in chain.get_unpacked_list() for atom in residue.get_unpacked_list()]
102105
if not atoms:

‎platform/api/tests/test_esmfold2_native_confidence.py‎

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,99 @@
5050
'''
5151

5252

53+
# Exact pinned writer's 19-column layout; deliberately inert coordinates and
54+
# confidence, not a captured prediction. Occupancy is the only omitted column.
55+
NATIVE_CIF = '''data_fixture
56+
#
57+
loop_
58+
_atom_site.group_PDB
59+
_atom_site.type_symbol
60+
_atom_site.label_atom_id
61+
_atom_site.label_alt_id
62+
_atom_site.label_comp_id
63+
_atom_site.label_asym_id
64+
_atom_site.label_entity_id
65+
_atom_site.label_seq_id
66+
_atom_site.pdbx_PDB_ins_code
67+
_atom_site.auth_seq_id
68+
_atom_site.auth_comp_id
69+
_atom_site.auth_asym_id
70+
_atom_site.auth_atom_id
71+
_atom_site.B_iso_or_equiv
72+
_atom_site.Cartn_x
73+
_atom_site.Cartn_y
74+
_atom_site.Cartn_z
75+
_atom_site.pdbx_PDB_model_num
76+
_atom_site.id
77+
ATOM C CA . ALA X 1 7 A 42 ALA A CA 0.5 1 2 3 1 1
78+
ATOM C CA . ALA Y 1 1 ? 42 ALA B CA 80 2 3 4 1 2
79+
HETATM C C1 . LIG Z 2 . ? 1 LIG L C1 40 3 4 5 1 3
80+
HETATM O O1 . LIG Z 2 . ? 1 LIG L O1 40 4 5 6 1 4
81+
#
82+
'''
83+
84+
85+
@pytest.mark.parametrize('suffix', ['.cif', '.mmcif'])
86+
def test_native_cif_compatibility_preserves_geometry_identity_and_confidence(suffix):
87+
from services.core_protein_result_contract import _structure_confidence
88+
from services.md.starting_structures import _parse_mmcif_structure
89+
90+
data = NATIVE_CIF.encode()
91+
original = bytes(data)
92+
atoms = list(_parse_mmcif_structure(data).get_atoms())
93+
assert [atom.occupancy for atom in atoms] == [1.0] * 4
94+
np.testing.assert_array_equal([atom.coord for atom in atoms],
95+
[[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]])
96+
assert [atom.get_parent().get_parent().id for atom in atoms] == ['A', 'B', 'L', 'L']
97+
assert [atom.name for atom in atoms] == ['CA', 'CA', 'C1', 'O1']
98+
assert atoms[0].get_parent().id == (' ', 42, 'A')
99+
assert [atom.bfactor for atom in atoms] == [.5, 80, 40, 40]
100+
assert _structure_confidence(data, 'native' + suffix) == (40.25, [.5, 80])
101+
assert data == original and b'_atom_site.occupancy' not in data
102+
103+
104+
@pytest.mark.parametrize('damage', ['nonnumeric_coordinate', 'nonfinite_coordinate',
105+
'missing_coordinate', 'missing_atom_identity',
106+
'missing_chain_identity', 'missing_residue_identity',
107+
'invalid_present_occupancy'])
108+
def test_native_occupancy_compatibility_does_not_repair_other_damage(damage):
109+
from services.core_protein_result_contract import CandidateIntegrityError, _structure_confidence
110+
111+
content = NATIVE_CIF
112+
if damage in {'nonnumeric_coordinate', 'nonfinite_coordinate'}:
113+
value = 'broken' if damage == 'nonnumeric_coordinate' else 'nan'
114+
content = content.replace('0.5 1 2 3', f'0.5 {value} 2 3')
115+
elif damage == 'invalid_present_occupancy':
116+
content = content.replace('_atom_site.id\n', '_atom_site.id\n_atom_site.occupancy\n')
117+
content = '\n'.join(line + ' broken' if line.startswith(('ATOM ', 'HETATM ')) else line
118+
for line in content.split('\n'))
119+
else:
120+
column = {'missing_coordinate': 'Cartn_x', 'missing_atom_identity': 'label_atom_id',
121+
'missing_chain_identity': 'auth_asym_id',
122+
'missing_residue_identity': 'label_comp_id'}[damage]
123+
lines = content.splitlines()
124+
headers = [line for line in lines if line.startswith('_atom_site.')]
125+
index = headers.index('_atom_site.' + column)
126+
content = '\n'.join(' '.join(line.split()[:index] + line.split()[index + 1:])
127+
if line.startswith(('ATOM ', 'HETATM ')) else line
128+
for line in lines if line != '_atom_site.' + column) + '\n'
129+
with pytest.raises(CandidateIntegrityError) as error:
130+
_structure_confidence(content.encode(), 'native.cif')
131+
assert error.value.reason['code'] == 'candidate_structure_invalid'
132+
133+
53134
@pytest.fixture(autouse=True)
54135
def isolated_artifacts(monkeypatch, tmp_path):
55136
monkeypatch.setenv('BMS_SCIENTIFIC_ARTIFACT_ROOT', str(tmp_path / 'scientific_artifacts'))
56137

57138

58-
def produce(tmp_path, monkeypatch, *, confidence=True, pae=True):
139+
def produce(tmp_path, monkeypatch, *, confidence=True, pae=True, cif=CIF):
59140
"""Run the real CLI writer against an explicitly inert fold result."""
60141
import sys
61142
output = tmp_path / 'esmfold2_results'
62143
output.mkdir()
63144
matrix = np.arange(25, dtype=np.float32).reshape(5, 5) / 2
64-
samples = [SimpleNamespace(complex=SimpleNamespace(to_mmcif=lambda: CIF),
145+
samples = [SimpleNamespace(complex=SimpleNamespace(to_mmcif=lambda: cif),
65146
plddt=np.array([0, .2, .6, .8, 1], dtype=np.float32), ptm=.7, iptm=0,
66147
pae=matrix + index if pae else None,
67148
pair_chains_iptm=np.array([[.2, .4], [.8, .6]]),
@@ -120,8 +201,9 @@ async def session():
120201

121202
@pytest.mark.asyncio
122203
@pytest.mark.parametrize('model_id', ['esmfold2', 'esmfold2_experimental'])
123-
async def test_producer_ingest_fresh_api_sample_binding(tmp_path, monkeypatch, model_id):
124-
root, samples = produce(tmp_path, monkeypatch)
204+
@pytest.mark.parametrize('cif', [CIF, NATIVE_CIF], ids=['occupancy_present', 'native_occupancy_absent'])
205+
async def test_producer_ingest_fresh_api_sample_binding(tmp_path, monkeypatch, model_id, cif):
206+
root, samples = produce(tmp_path, monkeypatch, cif=cif)
125207
manifest = json.loads((root / 'manifest.json').read_text())
126208
assert manifest['sample_count'] == 2
127209
for entry, sample in zip(manifest['samples'], samples):

0 commit comments

Comments
 (0)