kdidi99 Claude Opus 5 commited on
Commit
33517d4
·
1 Parent(s): 040b0b2

Document and test RFD4's batching contract for several designs

Browse files

No change to how write_cif indexes anything. RFD4 replicates at collate time --
datapipes/collate.py does `[sample for sample in samples for _ in
range(num_replicates)]` -- so every per-row quantity already has one row per
design by the time anything reads it, and the export loop in proteina.py reads
row i of each. write_cif already did the same thing, and keeping it that way is
the point: this package indexes the way RFD4 indexes.

What was missing was saying so, and testing it. The README example replicated
with `repeat`, which tiles; an input's designs sit together, so it must be
`repeat_interleave`, and the difference is invisible for one input and wrong
for more than one. It now also shows `num_designs_per_prompt` alongside the
replication, since a pipeline repeating its input internally still leaves the
caller to replicate the features it passes back in.

Tests cover one and three designs with replicated features, that unreplicated
features raise IndexError on the second design rather than quietly reading
another row, and that a row past the end raises.

An earlier draft of this added a helper deriving the design count from the
feature and output shapes, so that unreplicated features would work. That was
invention: it is not what RFD4 does, it would have been one more thing to keep
in step with upstream, and the first version of it silently read the wrong
input's mask for batches of more than one input. Following the reference is
cheaper than being clever next to it.

Verified against the Diffusers pipeline on branch rfd4-torch: prepare_cif's
features pass its check_inputs with num_designs_per_prompt=3, three designs
write from replicated features, and the unreplicated case raises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +19 -5
  2. tests/test_write_cif_designs.py +118 -0
README.md CHANGED
@@ -202,11 +202,25 @@ provide is the rest of a training run: the dataset loader, the task configuratio
202
 
203
  Source attribution and third-party licenses: [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt).
204
 
205
- To generate several designs of one input in a batch, repeat the features and write each design with `row`:
 
 
206
 
207
  ```python
208
- batch = {name: value.repeat(8, *[1] * (value.ndim - 1)) for name, value in features.items()}
209
- output = pipe(batch, generator=[torch.Generator("cuda:0").manual_seed(seed) for seed in range(8)])
210
- for row in range(8):
211
- write_cif(output, batch, atoms, f"design_{row}.cif", row=row)
 
 
 
 
 
 
 
 
212
  ```
 
 
 
 
 
202
 
203
  Source attribution and third-party licenses: [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt).
204
 
205
+ To generate several designs of one input, ask the pipeline for them and replicate the features to match, which is
206
+ what RFD4 itself does: its collate step repeats each sample `num_replicates` times before batching, so every
207
+ per-row quantity has one row per design and `row` indexes them all alike.
208
 
209
  ```python
210
+ designs = 8
211
+ output = pipe(
212
+ features,
213
+ num_designs_per_prompt=designs,
214
+ generator=[torch.Generator("cuda:0").manual_seed(seed) for seed in range(designs)],
215
+ )
216
+ replicated = {
217
+ name: value.repeat_interleave(designs, dim=0) if torch.is_tensor(value) else value
218
+ for name, value in features.items()
219
+ }
220
+ for row in range(designs):
221
+ write_cif(output, replicated, atoms, f"design_{row}.cif", row=row)
222
  ```
223
+
224
+ Use `repeat_interleave`, not `repeat`: an input's designs sit together, so output row `r` belongs to input
225
+ `r // designs`. Passing the features unreplicated raises `IndexError` on the second design rather than writing the
226
+ wrong row.
tests/test_write_cif_designs.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """`write_cif` across several designs, following RFD4's own batching contract.
2
+
3
+ RFD4 replicates at collate time -- `datapipes/collate.py` does
4
+ `[sample for sample in samples for _ in range(num_replicates)]` -- so by the time anything is
5
+ indexed, every per-row quantity already has one row per design, and the export loop in
6
+ `proteina.py` reads row `i` of each. `write_cif` does the same: `row` indexes both the output and
7
+ the features, so the caller passes as many feature rows as the output has rows.
8
+
9
+ These cover that contract, and that passing too few rows fails loudly rather than reading some
10
+ other row's mask.
11
+ """
12
+
13
+ import tempfile
14
+ import types
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ ROOT = Path(__file__).resolve().parents[1]
20
+ INPUT = ROOT / "examples/inputs/inverse_folding.cif"
21
+
22
+
23
+ def _prepared():
24
+ pytest.importorskip("atomworks")
25
+ import torch
26
+
27
+ from rfd4_proteina_prep import prepare_cif
28
+
29
+ if not INPUT.is_file():
30
+ pytest.skip(f"{INPUT.name} is not present")
31
+ return prepare_cif(INPUT, seed=0, device=torch.device("cpu"))
32
+
33
+
34
+ def _replicate(features, n_designs: int):
35
+ """What RFD4's collate does, and what a pipeline repeating its input leaves the caller to do.
36
+
37
+ `repeat_interleave` rather than `repeat`: an input's designs sit together, so output row `r`
38
+ belongs to input `r // n_designs`. Tiling would pair the rows differently, and for more than
39
+ one input, wrongly.
40
+ """
41
+ return {
42
+ name: value.repeat_interleave(n_designs, dim=0) if hasattr(value, "repeat_interleave") else value
43
+ for name, value in features.items()
44
+ }
45
+
46
+
47
+ def _output_of(features, n_designs: int):
48
+ """The shape a pipeline returns for `n_designs` designs of one input.
49
+
50
+ Every design carries the input's own coordinates, so the written files must agree; what is
51
+ under test is which row is read, not what a model would have produced.
52
+ """
53
+ import torch
54
+
55
+ tokens = features["coords"].shape[1]
56
+ repeat = lambda tensor: tensor.repeat_interleave(n_designs, dim=0) # noqa: E731 - matches the pipeline
57
+ return types.SimpleNamespace(
58
+ coords_nm=repeat(features["coords_nm"]),
59
+ residue_type=repeat(features["residue_type"]),
60
+ atom_mask=repeat(features["atom37_mask"]),
61
+ insertion_logits=torch.zeros((n_designs, tokens, tokens)),
62
+ )
63
+
64
+
65
+ @pytest.mark.parametrize("n_designs", [1, 3])
66
+ def test_every_design_is_written(n_designs):
67
+ """Features replicated to match the output, one file per design."""
68
+ from rfd4_proteina_prep import write_cif
69
+
70
+ features, atoms = _prepared()
71
+ assert features["padding_mask"].shape[0] == 1, "prepare_cif returns a single row"
72
+ replicated = _replicate(features, n_designs)
73
+ assert replicated["padding_mask"].shape[0] == n_designs
74
+ output = _output_of(features, n_designs)
75
+
76
+ with tempfile.TemporaryDirectory() as directory:
77
+ written = [
78
+ Path(write_cif(output, replicated, atoms, Path(directory) / f"design_{row}.cif", row=row))
79
+ for row in range(n_designs)
80
+ ]
81
+ assert all(path.is_file() for path in written)
82
+ # Same coordinates in every design, so the files agree apart from the lines the writer
83
+ # derives from the output path and the clock.
84
+ skip = ("_entry.time", "_entry.id", "data_")
85
+ bodies = {
86
+ tuple(line for line in path.read_text().splitlines() if not line.startswith(skip)) for path in written
87
+ }
88
+ assert len(bodies) == 1, "designs of one input differ beyond their file-derived lines"
89
+
90
+
91
+ def test_unreplicated_features_fail_loudly():
92
+ """Too few feature rows raises rather than quietly reading another row.
93
+
94
+ This is what a caller sees when the pipeline repeated its input internally and they passed the
95
+ features straight through. Row 0 happens to work, because one feature row is the row it wants;
96
+ every later row must fail rather than succeed on the wrong data.
97
+ """
98
+ from rfd4_proteina_prep import write_cif
99
+
100
+ features, atoms = _prepared()
101
+ output = _output_of(features, 3)
102
+ with tempfile.TemporaryDirectory() as directory:
103
+ write_cif(output, features, atoms, Path(directory) / "design_0.cif", row=0)
104
+ with pytest.raises(IndexError):
105
+ write_cif(output, features, atoms, Path(directory) / "design_1.cif", row=1)
106
+
107
+
108
+ def test_row_beyond_the_output_is_an_error():
109
+ """A row past the designs that exist fails rather than writing design 0 again."""
110
+ from rfd4_proteina_prep import write_cif
111
+
112
+ features, atoms = _prepared()
113
+ n_designs = 2
114
+ output = _output_of(features, n_designs)
115
+ replicated = _replicate(features, n_designs)
116
+ with tempfile.TemporaryDirectory() as directory:
117
+ with pytest.raises(IndexError):
118
+ write_cif(output, replicated, atoms, Path(directory) / "design_9.cif", row=9)