Laya
Safetensors
English
biology
dna
protein
File size: 22,275 Bytes
d35dd87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#!/usr/bin/env python3
"""Prepare deterministic Laya train, selection-dev, calibration, and test views.

Standard library only. Input JSONL files are read-only. Only train+val labels
participate in conflict removal and stratification. Test labels are serialized
as canonical indices but never counted, compared, scored, or used for decisions.
DNA reverse-complement grouping is partition hygiene, not semantic relabeling.
"""
from __future__ import annotations

import argparse
from collections import Counter, defaultdict
from datetime import datetime, timezone
import hashlib
import itertools
import json
from pathlib import Path
import re

from audit_laya_tasks import TASKS

SOURCE_SPLITS = ("train", "val", "test")
FINAL_SPLITS = ("train", "selection_dev", "calibration", "test")
COMPLEMENT = str.maketrans("ACGTRYSWKMBDHVN-", "TGCAYRSWMKVHDBN-")
SCHEMA = ["id", "task", "modality", "context", "sequence", "choices", "label",
          "source_split", "split", "sequence_sha256", "group_id"]
POLICY = [
    "Normalize the final nonempty user-message line with strip and uppercase; preserve sequence orientation.",
    "Validate all rows and stop on a schema/alphabet error rather than silently dropping malformed rows.",
    "Primary identity is SHA-256 of the normalized exact sequence.",
    "For DNA only, a partition group is SHA-256 of min(sequence, reverse_complement(sequence)); this does not imply label equivalence.",
    "Using train+val only, exclude every exact sequence with conflicting labels and every DNA RC group with conflicting labels; never majority-vote or relabel.",
    "Within each source split retain the first exact-sequence occurrence in source-line order; distinct RC sequences with consistent train+val labels can coexist in one group.",
    "Retain train first, then exclude validation records whose exact/RC partition group is in retained train.",
    "Split retained val at group level, stratified by canonical label, into approximately 50% selection_dev and 50% calibration using a fixed seed; each class appears in both.",
    "Test is processed only after training and validation membership is fixed. Deduplicate test by exact sequence and exclude test group overlap with retained train/val; no test labels affect any exclusion or partition.",
    "Test labels are converted to canonical indices only for an eventual locked evaluation; no test label histogram, conflict analysis, model evaluation, or performance access is performed here.",
    "Raw files are never modified. Source provenance and historical clustering summaries are documented; no homology-independence claim is made.",
]


def digest(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def file_digest(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def parse_record(raw: dict, task: dict, line: int) -> dict:
    split = raw.get("split")
    if split not in SOURCE_SPLITS:
        raise ValueError(f"line {line}: unknown split {split!r}")
    if raw.get("task_id") != task["task_id"]:
        raise ValueError(f"line {line}: unexpected task_id")
    if task["modality"] not in raw.get("modality", []):
        raise ValueError(f"line {line}: wrong modality")
    if not isinstance(raw.get("id"), str) or not raw["id"]:
        raise ValueError(f"line {line}: missing id")
    if raw.get("choices") != task["choices"]:
        raise ValueError(f"line {line}: unexpected choices or candidate order")
    if raw.get("answer_short") not in task["choices"]:
        raise ValueError(f"line {line}: unknown canonical label")
    users = [m.get("content") for m in raw.get("messages", [])
             if isinstance(m, dict) and m.get("role") == "user"]
    if len(users) != 1 or not isinstance(users[0], str):
        raise ValueError(f"line {line}: expected one string user message")
    lines = [part.strip() for part in users[0].splitlines() if part.strip()]
    if len(lines) < 2:
        raise ValueError(f"line {line}: cannot identify context and sequence")
    sequence = lines[-1].upper()
    if not sequence or not set(sequence) <= set(task["allowed"]):
        raise ValueError(f"line {line}: invalid sequence alphabet")
    context = re.split(r",?\s*The result will be one of the following\s*:",
                       "\n".join(lines[:-1]), maxsplit=1, flags=re.IGNORECASE)[0].strip(" ,")
    if not context:
        raise ValueError(f"line {line}: empty context")
    if split != "test":
        assistants = [m.get("content") for m in raw.get("messages", [])
                      if isinstance(m, dict) and m.get("role") == "assistant"]
        if assistants != [raw["answer_short"]]:
            raise ValueError(f"line {line}: training/validation assistant target mismatch")
    seq_hash = digest(sequence)
    group_sequence = min(sequence, sequence.translate(COMPLEMENT)[::-1]) if task["modality"] == "dna" else sequence
    return {"id": raw["id"], "task": task["task_id"], "modality": task["modality"],
            "context": context, "sequence": sequence, "choices": list(task["choices"]),
            "label": task["choices"].index(raw["answer_short"]), "source_split": split,
            "split": split, "sequence_sha256": seq_hash,
            "group_id": f'{task["task_id"]}:{digest(group_sequence)}', "_line": line}


def label_counts(rows: list[dict]) -> dict[str, int]:
    if any(row["source_split"] == "test" for row in rows):
        raise ValueError("Test label aggregation is forbidden in split preparation")
    return {str(k): v for k, v in sorted(Counter(row["label"] for row in rows).items())}


def group_membership(rows: list[dict], key: str) -> dict[str, set[str]]:
    return {split: {row[key] for row in rows if row["source_split"] == split} for split in SOURCE_SPLITS}


def overlaps(rows: list[dict], key: str) -> dict[str, int]:
    members = group_membership(rows, key)
    return {f"{a}__{b}": len(members[a] & members[b]) for a, b in itertools.combinations(SOURCE_SPLITS, 2)}


def split_validation(rows: list[dict], task_id: str, n_labels: int, seed: int):
    groups = defaultdict(list)
    for row in rows:
        groups[row["group_id"]].append(row)
    by_label = defaultdict(list)
    for gid, members in groups.items():
        labels = {row["label"] for row in members}
        if len(labels) != 1:
            raise ValueError("Conflicting validation group survived cleaning")
        by_label[next(iter(labels))].append((gid, members))
    dev_ids, details = set(), {}
    for label in range(n_labels):
        items = sorted(by_label[label], key=lambda x: digest(f"{seed}:{task_id}:{label}:{x[0]}"))
        if len(items) < 2:
            raise ValueError(f"{task_id} class {label}: fewer than two validation groups")
        total = sum(len(members) for _, members in items)
        target = total // 2
        # Exact subset-sum up to floor(total/2). Record each reachable sum once;
        # deterministic hashed group order determines equally good solutions.
        reachable, mask, previous = 1, (1 << (target + 1)) - 1, {}
        for gid, members in items:
            weight = len(members)
            added = ((reachable << weight) & mask) & ~reachable
            while added:
                low = added & -added
                amount = low.bit_length() - 1
                previous[amount] = (amount - weight, gid)
                added ^= low
            reachable |= (reachable << weight) & mask
        amount = reachable.bit_length() - 1
        if amount == 0:
            raise ValueError(f"{task_id} class {label}: cannot preserve coverage in both halves")
        selected_count = amount
        while amount:
            amount, gid = previous[amount]
            dev_ids.add(gid)
        details[str(label)] = {"source_val_rows": total, "group_count": len(items),
                               "target_dev_rows": target, "selection_dev_rows": selected_count,
                               "calibration_rows": total - selected_count}
    dev, calibration = [], []
    for row in rows:
        split = "selection_dev" if row["group_id"] in dev_ids else "calibration"
        copy = dict(row, split=split)
        (dev if split == "selection_dev" else calibration).append(copy)
    assert set(label_counts(dev)) == set(label_counts(calibration)) == {str(i) for i in range(n_labels)}
    assert {r["group_id"] for r in dev}.isdisjoint(r["group_id"] for r in calibration)
    return dev, calibration, details


def write_jsonl(path: Path, rows: list[dict], *, compact_records: bool = True):
    with path.open("w", encoding="utf-8", newline="\n") as handle:
        for row in rows:
            obj = {key: row[key] for key in SCHEMA} if compact_records else row
            handle.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + "\n")


def historical_metadata(root: Path, name: str, current_rows: int):
    path = root / "data/03_sft_biopaws2/docs/leakage_stats.json"
    out = {"row_level_group_ids_found": False, "homology_independence_verified": False}
    if path.exists():
        historical = json.loads(path.read_text(encoding="utf-8")).get(name)
        out.update({"historical_summary_path": str(path.relative_to(root)),
                    "historical_summary_sha256": file_digest(path),
                    "historical_task_summary": historical,
                    "historical_row_count_matches": bool(historical and historical.get("n_rows") == current_rows),
                    "historical_summary_current_sequence_hashes_verified": False,
                    "scope": "Historical aggregate only; no row-level membership or per-input hash binding. Not used for partitioning, model choices, or claims of homology independence."})
    return out


def prepare_task(root: Path, outdir: Path, name: str, task: dict, seed: int):
    source_path = root / "data/03_sft_biopaws2/jsonl" / f"{name}.jsonl"
    input_hash = file_digest(source_path)
    rows, field_counts, source_counts, source_by_split = [], Counter(), Counter(), defaultdict(Counter)
    seen_ids = set()
    with source_path.open(encoding="utf-8") as handle:
        for line, raw_line in enumerate(handle, 1):
            if not raw_line.strip():
                continue
            raw = json.loads(raw_line)
            row = parse_record(raw, task, line)
            if row["id"] in seen_ids:
                raise ValueError(f"Duplicate source ID: {row['id']}")
            seen_ids.add(row["id"])
            rows.append(row)
            # Count field presence without concatenating string values into a
            # giant Counter payload (Counter.update(mapping) treats values as
            # counts).  This is provenance metadata only.
            field_counts.update(raw.keys())
            source_counts[str(raw.get("source"))] += 1
            source_by_split[row["source_split"]][str(raw.get("source"))] += 1
    trainval = [row for row in rows if row["source_split"] != "test"]
    exact_labels, group_labels, group_sequences = defaultdict(set), defaultdict(set), defaultdict(set)
    for row in trainval:
        exact_labels[row["sequence_sha256"]].add(row["label"])
        group_labels[row["group_id"]].add(row["label"])
    for row in rows:
        group_sequences[row["group_id"]].add(row["sequence_sha256"])
    exact_conflicts = {key for key, labels in exact_labels.items() if len(labels) > 1}
    group_conflicts = {key for key, labels in group_labels.items() if len(labels) > 1}
    retained, exclusions = {}, []
    for split in SOURCE_SPLITS:
        selected, seen_sequences = [], set()
        earlier_groups = set()
        if split == "val":
            earlier_groups = {row["group_id"] for row in retained["train"]}
        elif split == "test":
            earlier_groups = {row["group_id"] for earlier in ("train", "val") for row in retained[earlier]}
        for row in rows:
            if row["source_split"] != split:
                continue
            reason = None
            if split != "test" and row["sequence_sha256"] in exact_conflicts:
                reason = "train_val_exact_label_conflict"
            elif split != "test" and row["group_id"] in group_conflicts:
                reason = "train_val_rc_group_label_conflict"
            elif row["group_id"] in earlier_groups:
                reason = "group_overlap_with_retained_train" if split == "val" else "group_overlap_with_retained_train_val"
            elif row["sequence_sha256"] in seen_sequences:
                reason = "same_split_exact_duplicate"
            if reason:
                exclusions.append({"id": row["id"], "source_split": split,
                                   "source_line": row["_line"], "sequence_sha256": row["sequence_sha256"],
                                   "group_id": row["group_id"], "reason": reason})
            else:
                selected.append(row)
                seen_sequences.add(row["sequence_sha256"])
        retained[split] = selected
    n_labels = len(task["choices"])
    assert set(label_counts(retained["train"])) == {str(i) for i in range(n_labels)}
    dev, calibration, stratification = split_validation(retained["val"], task["task_id"], n_labels, seed)
    outputs = {"train": retained["train"], "selection_dev": dev, "calibration": calibration, "test": retained["test"]}
    files = {}
    for split, records in outputs.items():
        path = outdir / f"{task['task_id']}_{split}.jsonl"
        write_jsonl(path, records)
        files[split] = {"path": str(path.relative_to(root)), "sha256": file_digest(path), "rows": len(records),
                        "unique_exact_sequences": len({row["sequence_sha256"] for row in records}),
                        "partition_groups": len({row["group_id"] for row in records}),
                        "ordered_ids_sha256": digest("\n".join(row["id"] for row in records)),
                        "sorted_sequence_hashes_sha256": digest("\n".join(sorted(row["sequence_sha256"] for row in records)))}
        if split != "test":
            files[split]["label_counts"] = label_counts(records)
    output_groups = {split: {row["group_id"] for row in records} for split, records in outputs.items()}
    final_overlaps = {f"{a}__{b}": len(output_groups[a] & output_groups[b])
                      for a, b in itertools.combinations(FINAL_SPLITS, 2)}
    assert not any(final_overlaps.values())
    exclusion_path = outdir / f"{task['task_id']}_exclusions.jsonl"
    write_jsonl(exclusion_path, exclusions, compact_records=False)
    assert file_digest(source_path) == input_hash, "Raw data unexpectedly changed"
    return {"task": task["task_id"], "modality": task["modality"], "choices": task["choices"],
            "input": {"path": str(source_path.relative_to(root)), "sha256": input_hash, "rows": len(rows),
                      "split_counts": dict(sorted(Counter(r["source_split"] for r in rows).items()))},
            "source_metadata": {"field_counts": dict(sorted(field_counts.items())),
                                "source_counts": dict(sorted(source_counts.items())),
                                "source_by_split": {s: dict(sorted(v.items())) for s, v in source_by_split.items()},
                                **historical_metadata(root, name, len(rows))},
            "audit": {"raw_cross_split_exact_overlap": overlaps(rows, "sequence_sha256"),
                      "raw_cross_split_partition_group_overlap": overlaps(rows, "group_id"),
                      "raw_groups_with_distinct_rc_sequences": sum(len(v) > 1 for v in group_sequences.values()) if task["modality"] == "dna" else None,
                      "train_val_exact_label_conflict_hashes": sorted(exact_conflicts),
                      "train_val_partition_label_conflict_group_ids": sorted(group_conflicts),
                      "train_val_additional_conflicting_rc_group_ids": sorted(gid for gid in group_conflicts if not (group_sequences[gid] & exact_conflicts)),
                      "final_cross_split_partition_group_overlap": final_overlaps,
                      "test_label_statistics_computed": False, "test_used_to_filter_train_val": False},
            "validation_stratification": stratification, "files": files,
            "exclusions": {"path": str(exclusion_path.relative_to(root)), "sha256": file_digest(exclusion_path),
                           "counts_by_source_split": {split: dict(sorted(Counter(row["reason"] for row in exclusions if row["source_split"] == split).items())) for split in SOURCE_SPLITS}}}


def markdown_report(manifest: dict) -> str:
    lines = ["# Laya 正式实验数据视图", "", f"生成时间:{manifest['generated_at_utc']};固定划分 seed:`{manifest['seed']}`。", "",
             "两个原始 JSONL 保持不变。脚本仅使用 Python 标准库;本阶段不运行模型、不计算 test 性能。", "",
             "| 任务 | 原始 train / val / test | train | selection_dev | calibration | test view |", "|---|---|---:|---:|---:|---:|"]
    for task in manifest["tasks"].values():
        inp, files = task["input"], task["files"]
        lines.append(f"| {task['task']} | " + " / ".join(str(inp['split_counts'][s]) for s in SOURCE_SPLITS) + " | " + " | ".join(str(files[s]['rows']) for s in FINAL_SPLITS) + " |")
    lines += ["", "## 冻结规则", ""] + [f"{i}. {text}" for i, text in enumerate(POLICY, 1)]
    lines += ["", "## 清洗、反向互补与分层", ""]
    for task in manifest["tasks"].values():
        au = task["audit"]
        lines += [f"### {task['task']}", "", f"- train+val exact 冲突组:{len(au['train_val_exact_label_conflict_hashes'])};额外 DNA RC 冲突组:{len(au['train_val_additional_conflicting_rc_group_ids'])}。",
                  f"- 原始 exact 跨 split 重叠:`{json.dumps(au['raw_cross_split_exact_overlap'], ensure_ascii=False)}`。",
                  f"- 原始 partition group 跨 split 重叠:`{json.dumps(au['raw_cross_split_partition_group_overlap'], ensure_ascii=False)}`。",
                  f"- 删除计数:`{json.dumps(task['exclusions']['counts_by_source_split'], ensure_ascii=False)}`。",
                  "- 最终 train / selection_dev / calibration / test 之间 partition group 重叠全部为 0。",
                  "", "| canonical label | 类名 | train | selection_dev | calibration |", "|---:|---|---:|---:|---:|"]
        for i, choice in enumerate(task["choices"]):
            lines.append(f"| {i} | {choice} | " + " | ".join(str(task['files'][s]['label_counts'].get(str(i), 0)) for s in FINAL_SPLITS[:-1]) + " |")
        if task["modality"] == "dna":
            lines += ["", f"DNA 中包含两个不同方向序列的 RC group 为 {au['raw_groups_with_distinct_rc_sequences']};当前 RC 检查没有引入额外删除。"]
    lines += ["", "selection_dev 和 calibration 按类别采用固定 seed 的组哈希顺序及确定性子集和划分,尽量达到每类 50/50;奇数类多出的一条归 calibration。每个训练类别在两个验证子集都保留覆盖。", "",
              "## 来源分组元数据与限制", "",
              "当前两个 JSONL 只有任务、source、id、split 等字段,没有可复用的 donor/species/homology cluster ID。`source` 的 dna_train/dna_eva、prot_train/prot_eva 是来源批次标签,不是独立生物实体分组。", "",
              "仓库带有历史 `data/03_sft_biopaws2/docs/leakage_stats.json` 和 `leakage_report.md`,记录了 MMseqs2 min-seq-id 0.5 / coverage 0.5 的汇总。该文件没有将每个当前输入哈希绑定到 cluster 的成员表,本次没有重跑或验证这些聚类。历史记录提示相似序列问题可能存在,不能把 exact/RC 无重叠写成同源独立。", "",
              "## 文件与复现", "", "`artifacts/laya_formal_data/manifest.json` 包含原始文件和输出 JSONL 的 SHA-256、行数、train/dev/calibration 类别计数、删除原因、来源统计和最终 overlap 断言。test 标签只被编码保存在 test view 中,没有类别直方图、冲突分析或性能访问。", "",
              "每个 compact JSONL 记录字段:`" + "`, `".join(SCHEMA) + "`。`label` 是该行 `choices` 的 0-based 索引;`context` 保留任务指令并移除重复候选列表。`group_id` 带 task 前缀,DNA 使用 RC canonical SHA-256,蛋白使用 exact SHA-256。", "",
              "```bash", "python scripts/prepare_laya_splits.py --seed 20260922", "```", ""]
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
    parser.add_argument("--output-dir", type=Path, default=Path("artifacts/laya_formal_data"))
    parser.add_argument("--seed", type=int, default=20260922)
    args = parser.parse_args()
    root = args.root.resolve()
    outdir = args.output_dir if args.output_dir.is_absolute() else root / args.output_dir
    outdir.mkdir(parents=True, exist_ok=True)
    manifest = {"schema_version": 1, "generated_at_utc": datetime.now(timezone.utc).isoformat(),
                "seed": args.seed, "validation_fraction_selection_dev": 0.5,
                "schema": SCHEMA, "policy": POLICY,
                "script_sha256": file_digest(Path(__file__).resolve()),
                "tasks": {task["task_id"]: prepare_task(root, outdir, name, task, args.seed) for name, task in TASKS.items()}}
    (outdir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    report_path = root / "research/laya_formal_data.md"
    report_path.parent.mkdir(parents=True, exist_ok=True)
    report_path.write_text(markdown_report(manifest), encoding="utf-8")
    for name, task in manifest["tasks"].items():
        print(json.dumps({"task": name, "rows": {s: task["files"][s]["rows"] for s in FINAL_SPLITS},
                          "exclusions": task["exclusions"]["counts_by_source_split"],
                          "final_partition_overlap": task["audit"]["final_cross_split_partition_group_overlap"]}))
    print(f"Wrote {outdir / 'manifest.json'} and {report_path}")


if __name__ == "__main__":
    main()