kierandidi commited on
Commit
77832f5
·
verified ·
1 Parent(s): 38584dd

Type glycans and non-canonical residues, and drop the deprecated AtomWorks call

Browse files

TokenType carried only five members while the RFD4 repository has seven, and nothing ever assigned GLYCAN or NCAA. A glycan or non-canonical residue was therefore typed as protein, unknown or atomized, which hands the decoder the wrong per-token-type residue restriction - on a release whose point is that it handles them. Ports the two assignments from datapipes/bonds.py along with the 13 glycan and 100 NCAA residue names.

Replaces get_annotation_2d_categories() with get_annotation_categories(n_body=2) at the two call sites that still used it, matching main and silencing the DeprecationWarning AtomWorks 3.0.3 emits on every run.

check_vendored.py now compares vendored content against the source by default, rather than only comparing the recorded commit against the current one. The pin check cannot catch a header that was updated without the content being reconciled, which is how the above slipped through.

Output-neutral on the nine example inputs: all 720 tensors bit-identical. Glycan and NCAA typing is newly reachable but is not exercised by those examples.

src/rfd4_proteina_prep/_cif/datapipes_bonds.py CHANGED
@@ -27,6 +27,33 @@ from .datapipes_schema_proteina import TokenType
27
 
28
  logger = logging.getLogger(__name__)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  _UNKNOWN_RES_BY_TOKEN_TYPE = {TokenType.PROTEIN: UNKNOWN_AA, TokenType.RNA: UNKNOWN_RNA, TokenType.DNA: UNKNOWN_DNA}
31
 
32
  _AROMATIC_BOND_TYPES = (
@@ -54,6 +81,8 @@ def _token_type_at(atom_array: AtomArray, atom_idxs: np.ndarray) -> np.ndarray:
54
  types[atom_array.get_annotation("is_standard_unknown_or_masked_dna")[atom_idxs]] = TokenType.DNA
55
  types[atom_array.get_annotation("is_standard_unknown_or_masked_rna")[atom_idxs]] = TokenType.RNA
56
  types[atom_array.get_annotation("is_standard_unknown_or_masked_aa")[atom_idxs]] = TokenType.PROTEIN
 
 
57
  types[atom_array.atomize[atom_idxs]] = TokenType.ATOMIZED
58
  return types
59
 
 
27
 
28
  logger = logging.getLogger(__name__)
29
 
30
+ # Residue names the unified vocabulary tokenizes as glycans and non-canonical amino acids,
31
+ # from the RFD4 repository's RESIDUES_BY_TOKEN_TYPE. A residue missing from these is typed as
32
+ # protein/RNA/DNA or atomized instead, which gives the decoder the wrong residue restriction.
33
+ GLYCAN_NAMES = frozenset(
34
+ (
35
+ "A2G", "BGC", "BMA", "FUC", "GAL", "GLA", "GLC", "MAN",
36
+ "NAG", "NDG", "NGA", "SIA", "XYP",
37
+ )
38
+ ) # fmt: skip
39
+ NCAA_NAMES = frozenset(
40
+ (
41
+ "004", "0AF", "0TD", "2MR", "3FG", "4BF", "4D4", "4HH",
42
+ "5CT", "5OH", "ABA", "AGM", "AIB", "ALC", "ALN", "ALO",
43
+ "ALY", "API", "ASA", "CAF", "CAS", "CCS", "CGU", "CIR",
44
+ "CME", "CSD", "CSO", "CSS", "CSU", "CSX", "CYG", "D2T",
45
+ "DAB", "DAH", "DDE", "DDZ", "DPP", "EEP", "FTR", "GLZ",
46
+ "HIC", "HIP", "HRG", "HY3", "HYP", "IIL", "ILX", "IYR",
47
+ "KCR", "KCX", "KPI", "LLP", "LYR", "M3L", "MEN", "MEQ",
48
+ "MGN", "MHO", "MHS", "MIS", "MK8", "ML3", "MLY", "MLZ",
49
+ "MP8", "NEP", "NIY", "NLE", "NMM", "OAS", "OCS", "OMT",
50
+ "OMY", "ORN", "P1L", "PBF", "PHD", "PHI", "PPN", "PTR",
51
+ "SCH", "SCY", "SEB", "SEC", "SEP", "SLL", "SMC", "SME",
52
+ "SNC", "TBG", "TOX", "TPO", "TPQ", "TRQ", "TRX", "TYI",
53
+ "TYQ", "TYS", "V5N", "YCM",
54
+ )
55
+ ) # fmt: skip
56
+
57
  _UNKNOWN_RES_BY_TOKEN_TYPE = {TokenType.PROTEIN: UNKNOWN_AA, TokenType.RNA: UNKNOWN_RNA, TokenType.DNA: UNKNOWN_DNA}
58
 
59
  _AROMATIC_BOND_TYPES = (
 
81
  types[atom_array.get_annotation("is_standard_unknown_or_masked_dna")[atom_idxs]] = TokenType.DNA
82
  types[atom_array.get_annotation("is_standard_unknown_or_masked_rna")[atom_idxs]] = TokenType.RNA
83
  types[atom_array.get_annotation("is_standard_unknown_or_masked_aa")[atom_idxs]] = TokenType.PROTEIN
84
+ types[np.isin(atom_array.res_name[atom_idxs], list(NCAA_NAMES))] = TokenType.NCAA
85
+ types[np.isin(atom_array.res_name[atom_idxs], list(GLYCAN_NAMES))] = TokenType.GLYCAN
86
  types[atom_array.atomize[atom_idxs]] = TokenType.ATOMIZED
87
  return types
88
 
src/rfd4_proteina_prep/_cif/datapipes_pipeline.py CHANGED
@@ -510,7 +510,7 @@ class ConvertToAtom37Format(Transform):
510
  na_ss_pair = np.zeros((n_tokens, n_tokens), dtype=np.int16)
511
  na_ss_pair_mask = np.zeros((n_tokens, n_tokens), dtype=bool)
512
 
513
- if C_BP.full_name in atom_array.get_annotation_2d_categories():
514
  pair_annotation = C_BP.annotation(atom_array, default="raise")
515
  atom_to_token = np.full(atom_array.array_length(), -1, dtype=np.int32)
516
  atom_to_token[token_starts] = np.arange(n_tokens, dtype=np.int32)
 
510
  na_ss_pair = np.zeros((n_tokens, n_tokens), dtype=np.int16)
511
  na_ss_pair_mask = np.zeros((n_tokens, n_tokens), dtype=bool)
512
 
513
+ if C_BP.full_name in atom_array.get_annotation_categories(n_body=2):
514
  pair_annotation = C_BP.annotation(atom_array, default="raise")
515
  atom_to_token = np.full(atom_array.array_length(), -1, dtype=np.int32)
516
  atom_to_token[token_starts] = np.arange(n_tokens, dtype=np.int32)
src/rfd4_proteina_prep/_cif/datapipes_schema_proteina.py CHANGED
@@ -42,6 +42,8 @@ class TokenType:
42
  DNA = 2
43
  ATOMIZED = 3
44
  UNKNOWN = 4
 
 
45
 
46
 
47
  SPECS: dict[str, tuple] = {
 
42
  DNA = 2
43
  ATOMIZED = 3
44
  UNKNOWN = 4
45
+ GLYCAN = 5
46
+ NCAA = 6
47
 
48
 
49
  SPECS: dict[str, tuple] = {
src/rfd4_proteina_prep/_cif/transforms_na_geom.py CHANGED
@@ -58,7 +58,7 @@ class FeaturizeNucleicAcidSecondaryStructure(Transform):
58
  token_starts = get_token_starts(atom_array)
59
  n_tokens = len(token_starts)
60
  if (
61
- C_BP.full_name not in atom_array.get_annotation_2d_categories()
62
  or C_NA_SS_STATE.full_name not in atom_array.get_annotation_categories()
63
  ):
64
  pair_annotation = AnnotationList2D(
 
58
  token_starts = get_token_starts(atom_array)
59
  n_tokens = len(token_starts)
60
  if (
61
+ C_BP.full_name not in atom_array.get_annotation_categories(n_body=2)
62
  or C_NA_SS_STATE.full_name not in atom_array.get_annotation_categories()
63
  ):
64
  pair_annotation = AnnotationList2D(
tools/check_vendored.py CHANGED
@@ -1,13 +1,23 @@
1
  #!/usr/bin/env python3
2
  """Report drift between the vendored `_cif` modules and the RFD4 repository they came from.
3
 
4
- Every vendored file records the commit it was adapted from. This re-reads those sources at the
5
- recorded commit, and again at the checkout's current commit, and reports which ones moved. It
6
- does not rewrite anything: the vendored copies deliberately omit training-only code, so the
7
- diffs need a human. The point is that drift shows up as a diff rather than as a silent
8
- behavioural difference months later.
 
 
 
 
 
 
 
 
9
 
10
  python tools/check_vendored.py --rfd4 /path/to/RFD4-Proteina-dev
 
 
11
  """
12
 
13
  from __future__ import annotations
@@ -43,18 +53,76 @@ SOURCES = {
43
  }
44
 
45
 
46
- def show(repo: Path, commit: str, path: str) -> list[str] | None:
47
  result = subprocess.run(
48
  ["git", "-C", str(repo), "show", f"{commit}:{path}"], capture_output=True, text=True
49
  )
50
  return result.stdout.splitlines() if result.returncode == 0 else None
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def main() -> int:
54
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
55
  parser.add_argument("--rfd4", type=Path, required=True, help="Checkout of baker-laboratory/RFD4-Proteina-dev.")
56
  parser.add_argument("--against", default="HEAD", help="Commit to compare against (default: HEAD).")
57
- parser.add_argument("--show-diff", action="store_true", help="Print the diff, not just the line count.")
 
 
 
 
 
 
 
 
58
  args = parser.parse_args()
59
 
60
  vendored = Path(__file__).resolve().parent.parent / "src" / "rfd4_proteina_prep" / "_cif"
@@ -65,6 +133,9 @@ def main() -> int:
65
  print(f"error: {args.rfd4} is not a readable git checkout", file=sys.stderr)
66
  return 2
67
 
 
 
 
68
  pins, moved, unpinned = set(), [], []
69
  for name, source in sorted(SOURCES.items()):
70
  local = vendored / name
@@ -75,7 +146,7 @@ def main() -> int:
75
  unpinned.append(name)
76
  continue
77
  pins.add(match.group(1))
78
- was, now = show(args.rfd4, match.group(1), source), show(args.rfd4, current, source)
79
  if was is None or now is None:
80
  moved.append((name, source, -1))
81
  continue
 
1
  #!/usr/bin/env python3
2
  """Report drift between the vendored `_cif` modules and the RFD4 repository they came from.
3
 
4
+ Two checks, because they catch different things:
5
+
6
+ * `--mode pin` re-reads each source at the commit the vendored file records and at the
7
+ checkout's current commit, and reports which sources moved upstream. This answers "has main
8
+ changed since someone last looked", and it is only as truthful as the recorded commit.
9
+ * `--mode content` (the default) compares the vendored file against the source itself, and
10
+ reports lines that exist in both but differ. This answers "does the copy actually match",
11
+ and unlike the pin check it cannot be defeated by a header that was updated without the
12
+ content being reconciled. That mistake has already been made here once.
13
+
14
+ Neither rewrites anything. The vendored copies deliberately omit training-only code, so removals
15
+ are expected and are not reported; only lines that correspond and disagree are. Some of those are
16
+ legitimate inference-only adaptations, so the output is a triage list, not a defect list.
17
 
18
  python tools/check_vendored.py --rfd4 /path/to/RFD4-Proteina-dev
19
+ python tools/check_vendored.py --rfd4 ../RFD4-Proteina-dev --mode pin
20
+ python tools/check_vendored.py --rfd4 ../RFD4-Proteina-dev --only transforms_na_geom.py --show-diff
21
  """
22
 
23
  from __future__ import annotations
 
53
  }
54
 
55
 
56
+ def show_file(repo: Path, commit: str, path: str) -> list[str] | None:
57
  result = subprocess.run(
58
  ["git", "-C", str(repo), "show", f"{commit}:{path}"], capture_output=True, text=True
59
  )
60
  return result.stdout.splitlines() if result.returncode == 0 else None
61
 
62
 
63
+ def content_drift(vendored: Path, repo: Path, commit: str, only: str | None, show: bool) -> int:
64
+ """Compare each vendored copy against its source, reporting lines that correspond but differ.
65
+
66
+ Removals are skipped: the vendored copies drop training-only code on purpose, so a missing
67
+ block is expected. What matters is a line that exists on both sides and says something
68
+ different — a renamed call, a changed constant, a deprecated API the copy still uses.
69
+ """
70
+ noise = re.compile(r"^\s*(#|from\s|import\s|\"\"\"|\'\'\')")
71
+ total, reported = 0, 0
72
+ print(f"comparing vendored copies against {commit[:12]}\n")
73
+ for name, source in sorted(SOURCES.items()):
74
+ if only and name != only:
75
+ continue
76
+ local = vendored / name
77
+ if not local.is_file():
78
+ continue
79
+ theirs = show_file(repo, commit, source)
80
+ if theirs is None:
81
+ print(f" {name:<40} source unreadable at {commit[:12]} <- {source}")
82
+ continue
83
+ ours = local.read_text().splitlines()
84
+ differing = []
85
+ matcher = difflib.SequenceMatcher(None, ours, theirs, autojunk=False)
86
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
87
+ if tag != "replace":
88
+ continue
89
+ for mine, theirs_line in zip(ours[i1:i2], theirs[j1:j2]):
90
+ left, right = mine.strip(), theirs_line.strip()
91
+ if not left or not right or left == right:
92
+ continue
93
+ if noise.match(mine) and noise.match(theirs_line):
94
+ continue
95
+ differing.append((left, right))
96
+ if differing:
97
+ reported += 1
98
+ total += len(differing)
99
+ print(f" {name:<40} {len(differing):>4} differing line(s) <- {source}")
100
+ if show:
101
+ for left, right in differing:
102
+ print(f" copy: {left}")
103
+ print(f" main: {right}")
104
+ if not total:
105
+ print("\nno differing lines: every vendored copy matches its source.")
106
+ return 0
107
+ print(f"\n{total} differing line(s) across {reported} file(s).")
108
+ print("Some are deliberate inference-only adaptations. Re-run with --show-diff to triage,")
109
+ print("and only update a file's recorded commit once its content has actually been reconciled.")
110
+ return 1
111
+
112
+
113
  def main() -> int:
114
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
115
  parser.add_argument("--rfd4", type=Path, required=True, help="Checkout of baker-laboratory/RFD4-Proteina-dev.")
116
  parser.add_argument("--against", default="HEAD", help="Commit to compare against (default: HEAD).")
117
+ parser.add_argument("--show-diff", action="store_true", help="Print the differing lines, not just a count.")
118
+ parser.add_argument(
119
+ "--mode",
120
+ choices=("content", "pin"),
121
+ default="content",
122
+ help="content: compare the vendored copy against the source. pin: compare the source at the recorded "
123
+ "commit against the current one. Default: content.",
124
+ )
125
+ parser.add_argument("--only", help="Restrict to one vendored file name.")
126
  args = parser.parse_args()
127
 
128
  vendored = Path(__file__).resolve().parent.parent / "src" / "rfd4_proteina_prep" / "_cif"
 
133
  print(f"error: {args.rfd4} is not a readable git checkout", file=sys.stderr)
134
  return 2
135
 
136
+ if args.mode == "content":
137
+ return content_drift(vendored, args.rfd4, current, args.only, args.show_diff)
138
+
139
  pins, moved, unpinned = set(), [], []
140
  for name, source in sorted(SOURCES.items()):
141
  local = vendored / name
 
146
  unpinned.append(name)
147
  continue
148
  pins.add(match.group(1))
149
+ was, now = show_file(args.rfd4, match.group(1), source), show_file(args.rfd4, current, source)
150
  if was is None or now is None:
151
  moved.append((name, source, -1))
152
  continue