File size: 6,225 Bytes
62a01bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""Add example sequence(s) to examples.py from a raw-data Excel file.

The training-data Excel files (e.g. LLPSense/data/raw/nonPSPex/ASCL1.xlsx)
carry a "Protein name" column and a sequence column (spelled either
"Sequence" or "Seqence" across files) alongside condition columns this demo
doesn't need. Some files hold one protein across several condition rows
(e.g. ASCL1.xlsx); others hold several distinct proteins, one per row, each
with its own name/sequence (e.g. UBQLN4_mut.xlsx's UBQLN_mut10/20/30). This
script groups rows by protein name and appends one EXAMPLES entry per
distinct (name, sequence) pair found in the file.

Requires pandas + openpyxl (not part of requirements.txt β€” those are only
needed to run this offline script, not the deployed app):
    pip install pandas openpyxl

Usage:
    python preprocess/add_example.py /path/to/ASCL1.xlsx
    python preprocess/add_example.py /path/to/ASCL1.xlsx --id ascl1 --name "ASCL1"
    python preprocess/add_example.py /path/to/UBQLN4_mut.xlsx   # adds all 3 mutants

--id/--name only apply when the file contains a single protein.

After adding, run preprocess/extract_example_feat.py to cache the T5 feature(s).
"""

import argparse
import re
import sys
from pathlib import Path

import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from examples import EXAMPLES  # noqa: E402

EXAMPLES_FILE = Path(__file__).resolve().parent.parent / "examples.py"
VALID_AA = set("ACDEFGHIKLMNPQRSTVWY")
ALLOW_AA = VALID_AA | set("XBJOUZ")


def _find_col(df, *keywords):
    for col in df.columns:
        low = col.lower()
        if all(keyword in low for keyword in keywords):
            return col
    return None


def slugify(name: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "_", name.strip().lower()).strip("_")
    if not slug:
        raise ValueError(f"Could not derive an id from name: {name!r}")
    return slug


def read_proteins(xlsx_path: Path) -> list[tuple[str, str]]:
    """Return one (name, sequence) pair per distinct protein in the file.

    Rows are grouped by protein name β€” a file may repeat the same protein
    across several condition rows (ASCL1.xlsx: 1 protein, several conditions)
    or list several distinct proteins one per row (UBQLN4_mut.xlsx: 3
    mutants). Either shape yields one entry per unique protein name here.
    """
    df = pd.read_excel(xlsx_path, engine="openpyxl")

    name_col = _find_col(df, "protein", "name")
    seq_col = _find_col(df, "seq")
    if name_col is None or seq_col is None:
        raise ValueError(
            f"{xlsx_path} is missing a protein-name or sequence column "
            f"(found columns: {df.columns.tolist()})"
        )

    df = df[[name_col, seq_col]].dropna()
    if df.empty:
        raise ValueError(f"{xlsx_path} has no rows with both a name and a sequence")

    proteins = []
    for name, group in df.groupby(name_col, sort=False):
        seqs = group[seq_col].astype(str).str.strip().str.upper().unique()
        if len(seqs) != 1:
            raise ValueError(
                f"Protein '{name}' in {xlsx_path} has {len(seqs)} distinct "
                f"sequences across its rows β€” expected exactly one"
            )
        proteins.append((str(name).strip(), seqs[0]))
    return proteins


def format_seq_literal(seq: str, width: int = 60) -> str:
    lines = [seq[i:i + width] for i in range(0, len(seq), width)]
    body = "\n".join(f'            "{line}"' for line in lines)
    return f"(\n{body}\n        )"


def append_example(entry: dict) -> None:
    text = EXAMPLES_FILE.read_text()
    match = re.search(r"(EXAMPLES\s*=\s*\[)(.*?)(\n\])", text, flags=re.DOTALL)
    if not match:
        raise RuntimeError(f"Could not find EXAMPLES list in {EXAMPLES_FILE}")

    # The existing last entry may or may not have a trailing comma (both
    # forms occur in practice) β€” normalize to one before appending.
    body = match.group(2).rstrip()
    if body and not body.endswith(","):
        body += ","

    new_block = (
        "\n    {\n"
        f'        "id": {entry["id"]!r},\n'
        f'        "name": {entry["name"]!r},\n'
        f'        "seq": {format_seq_literal(entry["seq"])},\n'
        "    },"
    )
    updated = text[:match.start(2)] + body + new_block + text[match.end(2):]
    EXAMPLES_FILE.write_text(updated)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("xlsx_path", type=Path)
    parser.add_argument(
        "--id", type=str, default=None,
        help="override the generated example id (only valid for single-protein files)",
    )
    parser.add_argument(
        "--name", type=str, default=None,
        help="override the display name (only valid for single-protein files)",
    )
    args = parser.parse_args()

    proteins = read_proteins(args.xlsx_path)

    if (args.id or args.name) and len(proteins) > 1:
        parser.error(
            f"{args.xlsx_path} contains {len(proteins)} distinct proteins β€” "
            "--id/--name can only be used with a single-protein file"
        )

    # Track ids seen so far in this run too, so two proteins in the same
    # file that happen to slugify to the same id are still caught.
    known_ids = {example["id"] for example in EXAMPLES}
    added = 0

    for name, seq in proteins:
        example_id = args.id or slugify(name)
        display_name = args.name or name

        invalid = set(seq) - ALLOW_AA
        if invalid:
            print(
                f"Warning: '{name}' sequence contains unexpected characters: "
                f"{''.join(sorted(invalid))}",
                file=sys.stderr,
            )

        if example_id in known_ids:
            print(f"'{example_id}' already exists in EXAMPLES β€” skipping.")
            continue

        append_example({"id": example_id, "name": display_name, "seq": seq})
        known_ids.add(example_id)
        added += 1
        print(f"Added '{example_id}' ({display_name}, {len(seq)} aa) to {EXAMPLES_FILE}")

    if added:
        print("Run `python preprocess/extract_example_feat.py` to cache the T5 feature(s).")
    else:
        print("No new examples added.")


if __name__ == "__main__":
    main()