File size: 5,335 Bytes
ad9fbbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Run MVGNN-PPIS five-fold ensemble inference."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import pandas as pd
import torch


PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from model import MVGNN, Seed_everything, model_test  # noqa: E402


def project_path(value: str | Path) -> Path:
    """Resolve package-relative paths while preserving explicit absolute paths."""
    path = Path(value).expanduser()
    return path if path.is_absolute() else PROJECT_ROOT / path


def load_config(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as handle:
        return json.load(handle)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Predict protein-protein interaction sites with MVGNN-PPIS."
    )
    parser.add_argument(
        "--config",
        default="conf/config.json",
        help="Package-relative JSON configuration file.",
    )
    parser.add_argument("--dataset", help="Override the evaluation CSV path.")
    parser.add_argument("--feature-path", help="Override the precomputed feature directory.")
    parser.add_argument("--weight-path", help="Override the checkpoint directory.")
    parser.add_argument("--output-path", help="Override the prediction output directory.")
    parser.add_argument("--num-workers", type=int, help="DataLoader worker count.")
    parser.add_argument("--seed", type=int, help="Random seed.")
    parser.add_argument(
        "--device",
        help="PyTorch device, for example 'cuda', 'cuda:0', or 'cpu'.",
    )
    return parser.parse_args()


def required_feature_files(feature_dir: Path, protein_ids: list[str]) -> list[Path]:
    suffixes = ("X", "node_feature", "mask", "label", "adj")
    return [
        feature_dir / f"{protein_id}_{suffix}.tensor"
        for protein_id in protein_ids
        for suffix in suffixes
    ]


def main() -> None:
    args = parse_args()
    config = load_config(project_path(args.config))

    paths = config["paths"]
    dataset_path = project_path(args.dataset or paths["dataset"])
    feature_dir = project_path(args.feature_path or paths["feature_dir"])
    weight_dir = project_path(args.weight_path or paths["weight_dir"])
    output_dir = project_path(args.output_path or paths["output_dir"])

    runtime = config["runtime"]
    seed = args.seed if args.seed is not None else runtime["seed"]
    num_workers = (
        args.num_workers if args.num_workers is not None else runtime["num_workers"]
    )
    requested_device = args.device or runtime.get("device", "auto")
    if requested_device == "auto":
        requested_device = "cuda" if torch.cuda.is_available() else "cpu"
    device = torch.device(requested_device)
    if device.type == "cuda" and not torch.cuda.is_available():
        raise RuntimeError("A CUDA/HIP device was requested but torch.cuda.is_available() is False.")

    test_df = pd.read_csv(
        dataset_path,
        dtype={"ID": str, "sequence": str, "label": str},
        keep_default_na=False,
    )
    required_columns = {"ID", "sequence", "label"}
    missing_columns = required_columns.difference(test_df.columns)
    if missing_columns:
        raise ValueError(
            f"Dataset {dataset_path} is missing columns: {sorted(missing_columns)}"
        )

    test_df["ID"] = test_df["ID"].str.strip()
    protein_ids = test_df["ID"].str.strip().drop_duplicates().tolist()
    missing_features = [
        path for path in required_feature_files(feature_dir, protein_ids) if not path.is_file()
    ]
    if missing_features:
        preview = "\n".join(f"  - {path}" for path in missing_features[:10])
        raise FileNotFoundError(
            f"Missing {len(missing_features)} precomputed feature files.\n{preview}"
        )

    folds = config["model"]["folds"]
    missing_weights = [
        weight_dir / f"fold{fold}.ckpt"
        for fold in range(folds)
        if not (weight_dir / f"fold{fold}.ckpt").is_file()
    ]
    if missing_weights:
        formatted = "\n".join(f"  - {path}" for path in missing_weights)
        raise FileNotFoundError(f"Missing model checkpoints:\n{formatted}")

    all_protein_data = {}
    for protein_id in protein_ids:
        all_protein_data[protein_id] = tuple(
            torch.load(
                feature_dir / f"{protein_id}_{suffix}.tensor",
                map_location="cpu",
                weights_only=True,
            )
            for suffix in ("X", "node_feature", "mask", "label", "adj")
        )

    Seed_everything(seed)
    model_config = dict(config["model"])
    model_config.update(
        {
            "seed": seed,
            "id_name": "ID",
            "remark": "PRO binding site prediction",
        }
    )
    print(f"Project root: {PROJECT_ROOT}")
    print(f"Dataset: {dataset_path}")
    print(f"Proteins: {len(test_df)}")
    print(f"Device: {device}")
    print(f"Checkpoints: {folds}")

    model_test(
        test_df,
        all_protein_data,
        MVGNN,
        model_config,
        weight_dir=weight_dir,
        output_dir=output_dir,
        logit=True,
        device=device,
        num_workers=num_workers,
    )


if __name__ == "__main__":
    main()