Skip to content

Commit 3f9ad20

Browse files
DimaMolodclaude
andcommitted
Explain empty interfaces.csv instead of writing it silently
When no interface row is produced for a run, process() wrote a zero-byte interfaces.csv (no header) and still logged "wrote .../interfaces.csv" with no error, making a genuine "no interface" result indistinguishable from a parse/load failure (issue #17). The common case for heterodimers is that AlphaFold placed the chains with no inter-chain contact within --contact_thresh (default 8 A, Cb-Cb), so no interface is detected and rows is empty. Track lightweight per-run diagnostics (models processed, interfaces found, interfaces dropped by --pae_filter) and emit a WARNING explaining why the CSV is empty and which knob to relax. File-write behavior is unchanged (still an empty file) for backward compatibility; only the logging improves. Add a regression test that runs the AF3 fixture with a sub-Angstrom contact_thresh and asserts the empty CSV plus the explanatory warning. Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent a1b022e commit 3f9ad20

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

src/alphajudge/runner.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,16 @@ def process(
7070
job = d.resolve().name
7171

7272
rows: list[dict] = []
73+
# Diagnostics so an empty CSV can be explained rather than written silently.
74+
models_processed = 0
75+
total_interfaces = 0
76+
dropped_by_pae = 0
7377
for m in models:
7478
try:
7579
structure, confidence = run.load_model(m)
7680
comp = Complex(structure, confidence, contact_thresh, pae_filter, ipsae_pae_cutoff)
81+
models_processed += 1
82+
total_interfaces += len(comp.interfaces)
7783

7884
global_score = (
7985
comp.mpDockQ
@@ -85,6 +91,7 @@ def process(
8591
if iface.num_intf_residues == 0:
8692
continue
8793
if iface.average_interface_pae > pae_filter:
94+
dropped_by_pae += 1
8895
continue
8996
pd2, _ = iface.pDockQ2()
9097
label = (
@@ -161,6 +168,29 @@ def process(
161168

162169
out = d / per_run_csv_name
163170
out.parent.mkdir(parents=True, exist_ok=True)
171+
172+
if not rows:
173+
# Explain *why* the CSV is empty instead of writing a silent zero-byte file
174+
# (see https://github.com/KosinskiLab/AlphaJudge/issues/17). The common case
175+
# for heterodimers is that AlphaFold placed the chains without any inter-chain
176+
# contact within --contact_thresh, so no interface is detected.
177+
if models_processed == 0:
178+
reason = "no model could be loaded/processed"
179+
elif total_interfaces == 0:
180+
reason = (
181+
f"no inter-chain contacts within contact_thresh={contact_thresh} A "
182+
f"(chains have no detectable interface); try a larger --contact_thresh "
183+
f"or check that the model is actually a complex"
184+
)
185+
elif dropped_by_pae:
186+
reason = (
187+
f"all {dropped_by_pae} detected interface(s) were filtered out by "
188+
f"pae_filter={pae_filter}; try a larger --pae_filter"
189+
)
190+
else:
191+
reason = "all detected interfaces had zero interface residues"
192+
logger.warning(f"no interface rows for {job}: {reason}; writing empty {out}")
193+
164194
with out.open("w", newline="") as f:
165195
if rows:
166196
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))

test/test_parsers_and_runner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,25 @@ def test_af3_runner_outputs_have_expected_scores(tmp_path: Path, af3_dir_src: Pa
519519
assert nearly_equal(got_iptm_ptm, float(exp_iptm_ptm)), f"AF3 iptm_ptm mismatch for {m}"
520520

521521

522+
def test_af3_empty_csv_is_explained_when_no_contacts(
523+
tmp_path: Path, af3_dir_src: Path, caplog: pytest.LogCaptureFixture
524+
):
525+
"""Regression for issue #17: when no inter-chain contact is within contact_thresh,
526+
the CSV is empty (no header) but the log must say *why* instead of being silent."""
527+
af3_dir = copy_run_dir(af3_dir_src, tmp_path)
528+
529+
caplog.set_level(logging.WARNING, logger="alphajudge.runner")
530+
# A sub-Angstrom contact threshold guarantees no inter-chain contacts -> no interfaces.
531+
process(str(af3_dir), 0.01, 100.0, "best", 10.0)
532+
533+
out = af3_dir / "interfaces.csv"
534+
assert out.exists(), "an (empty) interfaces.csv should still be written"
535+
assert out.stat().st_size == 0, "no contacts -> empty CSV (no header)"
536+
537+
assert "no interface rows" in caplog.text
538+
assert "contact_thresh" in caplog.text
539+
540+
522541
def test_af3_parser_accepts_official_prefixed_layout(tmp_path: Path, af3_dir_src: Path):
523542
af3_dir = make_official_af3_layout(af3_dir_src, tmp_path, job_name="hello_fold")
524543

0 commit comments

Comments
 (0)