MitoInteract / recovery /scripts /cluster_proteins.py
Ethan Troy
feat: add Kd-only benchmark recovery track
38bce11
Raw
History Blame Contribute Delete
4.62 kB
#!/usr/bin/env python3
"""Create a sequence-similarity-disjoint protein split using MMseqs2."""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
from collections import Counter
from pathlib import Path
from mitointeract_recovery.splits import assign_grouped_splits, assert_group_disjoint
def read_rows(path: Path) -> list[dict]:
with path.open() as handle:
return [json.loads(line) for line in handle if line.strip()]
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--data-dir", type=Path, default=Path("artifacts/dev-10k"))
parser.add_argument("--min-seq-id", type=float, default=0.5)
parser.add_argument("--coverage", type=float, default=0.8)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--threads", type=int, default=8)
args = parser.parse_args()
if not shutil.which("mmseqs"):
raise RuntimeError("MMseqs2 is required; install the mmseqs2 package")
rows = read_rows(args.data_dir / "sample.jsonl")
audit_path = args.data_dir / "audit.json"
audit = json.loads(audit_path.read_text())
split_ratios = audit.get(
"split_ratios", {"train": 0.8, "validation": 0.1, "test": 0.1}
)
proteins = sorted({row["protein_id"]: row["sequence"] for row in rows}.items())
suffix = (
f"identity-{int(args.min_seq_id * 100)}-coverage-{int(args.coverage * 100)}"
)
work = args.data_dir / f"mmseqs-{suffix}"
work.mkdir(parents=True, exist_ok=True)
fasta = work / "proteins.fasta"
with fasta.open("w") as handle:
for protein_id, sequence in proteins:
handle.write(f">{protein_id}\n{sequence}\n")
prefix = work / "clusters"
tmp = work / "tmp"
command = [
"mmseqs",
"easy-cluster",
str(fasta),
str(prefix),
str(tmp),
"--min-seq-id",
str(args.min_seq_id),
"-c",
str(args.coverage),
"--cov-mode",
"0",
"--alignment-mode",
"3",
"--threads",
str(args.threads),
]
cluster_tsv = prefix.with_name(prefix.name + "_cluster.tsv")
if not cluster_tsv.exists():
subprocess.run(command, check=True)
protein_to_cluster: dict[str, str] = {}
with cluster_tsv.open() as handle:
for line in handle:
representative, member = line.rstrip("\n").split("\t")
protein_to_cluster[member] = representative
missing = {protein_id for protein_id, _ in proteins} - protein_to_cluster.keys()
if missing:
raise AssertionError(f"MMseqs2 omitted {len(missing)} proteins")
enriched = [
dict(row, protein_similarity_cluster=protein_to_cluster[row["protein_id"]])
for row in rows
]
assignments = assign_grouped_splits(
enriched,
group_key="protein_similarity_cluster",
seed=args.seed,
ratios=split_ratios,
)
assert_group_disjoint(enriched, assignments, group_key="protein_similarity_cluster")
manifest_name = f"split-protein_similarity_{int(args.min_seq_id * 100)}.jsonl"
manifest = args.data_dir / manifest_name
with manifest.open("w") as handle:
for row in rows:
handle.write(
json.dumps(
{"pair_id": row["pair_id"], "split": assignments[row["pair_id"]]}
)
+ "\n"
)
cluster_sizes = Counter(protein_to_cluster.values())
row_counts = Counter(assignments.values())
report = {
"tool": "MMseqs2",
"command": command,
"min_sequence_identity": args.min_seq_id,
"minimum_alignment_coverage": args.coverage,
"coverage_mode": "both query and target",
"alignment_mode": "actual identical residues / aligned columns",
"unique_proteins": len(proteins),
"clusters": len(cluster_sizes),
"largest_cluster_proteins": max(cluster_sizes.values()),
"split_rows": dict(row_counts),
"manifest": manifest_name,
"protein_cluster_overlap": 0,
}
(args.data_dir / f"protein-clusters-{suffix}.json").write_text(
json.dumps(report, indent=2) + "\n"
)
audit.setdefault("split_manifests", {})[
f"protein_similarity_{int(args.min_seq_id * 100)}"
] = manifest_name
audit.setdefault("external_split_audits", {})[
f"protein_similarity_{int(args.min_seq_id * 100)}"
] = report
audit_path.write_text(json.dumps(audit, indent=2) + "\n")
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()