supanthadey1 commited on
Commit
cc1df70
·
verified ·
1 Parent(s): c1e2e6b

Publish revised BERTose and BERToseBind release

Browse files

Add the verified versioned scientific release and reader-facing documentation; preserve legacy files and history.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. releases/2026-10-02/.gitattributes +2 -0
  2. releases/2026-10-02/.gitignore +5 -0
  3. releases/2026-10-02/LICENSE +176 -0
  4. releases/2026-10-02/NOTICE +18 -0
  5. releases/2026-10-02/README.md +51 -0
  6. releases/2026-10-02/SHA256SUMS +577 -0
  7. releases/2026-10-02/affinose/a1111_encoder_adapter.py +144 -0
  8. releases/2026-10-02/affinose/a5_tokenizer_shim.py +72 -0
  9. releases/2026-10-02/affinose/bertint_v8.py +969 -0
  10. releases/2026-10-02/affinose/dataset_v8.py +351 -0
  11. releases/2026-10-02/affinose/training_v8.py +1038 -0
  12. releases/2026-10-02/benchmark_exclusion/MANIFEST_SHA256.tsv +11 -0
  13. releases/2026-10-02/benchmark_exclusion/README.md +60 -0
  14. releases/2026-10-02/benchmark_exclusion/SHA256SUMS +11 -0
  15. releases/2026-10-02/benchmark_exclusion/code_inventory.tsv +7 -0
  16. releases/2026-10-02/benchmark_exclusion/required_successor_inputs.json +203 -0
  17. releases/2026-10-02/benchmark_exclusion/source_verification.json +124 -0
  18. releases/2026-10-02/benchmark_exclusion/writing/review/glytoucan/scripts/build_connection_level_support_audit.py +521 -0
  19. releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py +969 -0
  20. releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py +380 -0
  21. releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py +770 -0
  22. releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py +339 -0
  23. releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py +203 -0
  24. releases/2026-10-02/bertose.py +132 -0
  25. releases/2026-10-02/bertose_source/bert_training_v4/downstream_tasks/utils/wurcs_bpe_tokenizer.py +740 -0
  26. releases/2026-10-02/bertose_source/bert_training_v4/model/__init__.py +0 -0
  27. releases/2026-10-02/bertose_source/bert_training_v4/model/glycan_bert.py +303 -0
  28. releases/2026-10-02/bertose_source/bert_training_v4/model/multimodal_glycan_bert_v3.py +1108 -0
  29. releases/2026-10-02/bertose_source/data/bpe_vocabulary.json +2612 -0
  30. releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_authority.json +511 -0
  31. releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_independent_certificate.json +466 -0
  32. releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_receipt.json +150 -0
  33. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_bpe_vocabulary.json +0 -0
  34. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_sequence_tree_runtime.py +287 -0
  35. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_topology.py +180 -0
  36. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_wurcs_bpe.py +368 -0
  37. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_receipt.json +149 -0
  38. releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_summary.json +186 -0
  39. releases/2026-10-02/bertose_source/model/glycan_bert.py +303 -0
  40. releases/2026-10-02/bertose_source/model/multimodal_glycan_bert_v3.py +1084 -0
  41. releases/2026-10-02/bertose_source/training/a5_wurcs_bpe.py +368 -0
  42. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_submission_readback_11767377/review_a5_native_eight_cuda_resume_diagnostic_r5_11767377_terminal_certification_receipt.json +135 -0
  43. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_a5_native_eight_cuda_resume_diagnostic_receipt_r5.json +0 -0
  44. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_checkpoints_r1/review_checkpoint_index.json +1 -0
  45. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r1/review_a5_native_eight_data_adapter_receipt_r1.json +95 -0
  46. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r2/review_a5_native_eight_data_adapter_receipt_r2.json +1 -0
  47. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_receipt_r3.json +1 -0
  48. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_verification_receipt_r1.json +1 -0
  49. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r4/review_a5_native_eight_hotpath_r4_integrity_receipt.json +115 -0
  50. releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r5/review_a5_native_eight_hotpath_r5_integrity_receipt.json +98 -0
releases/2026-10-02/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.npz filter=lfs diff=lfs merge=lfs -text
releases/2026-10-02/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ outputs/
5
+ weights/
releases/2026-10-02/LICENSE ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
releases/2026-10-02/NOTICE ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BERTose and BERToseBind
2
+ Copyright 2026 Supantha Dey and contributors.
3
+
4
+ Original code is licensed under Apache License 2.0. See LICENSE. Existing source notices and attribution are preserved.
5
+
6
+ The Python dependencies are distributed separately under their own licenses.
7
+ The bundled WURCSFramework and MolWURCS Java archives are third-party software.
8
+ Their bytes, embedded LICENSE/NOTICE files and dependency POMs are preserved;
9
+ the original-code Apache declaration does not replace those terms. Archive
10
+ hashes and embedded notice locations are listed in
11
+ provenance/third_party_archives.json.
12
+ This code license does not grant rights to source databases, spectra, molecular
13
+ structures, simulations, third-party model weights, or ESM-C-derived features.
14
+ Those assets require the terms documented with each separate release.
15
+
16
+ The preserved implementation files retain their historical filenames and
17
+ internal model identifiers so that source and checkpoint hashes remain traceable.
18
+ Public model names are BERTose and BERToseBind.
releases/2026-10-02/README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BERTose workbench
2
+
3
+ Code for [supanthadey1/bertose-affinose-training-code](https://huggingface.co/supanthadey1/bertose-affinose-training-code), under `releases/2026-10-02/`. The model assets retain their `releases/2026-09-28/` paths.
4
+
5
+ ## Install
6
+
7
+ Run these commands from the code directory with Python 3.12:
8
+
9
+ ```bash
10
+ python3.12 -m venv .venv
11
+ source .venv/bin/activate
12
+ python -m pip install -r requirements-workbench.txt
13
+ ```
14
+
15
+ The base requirements use PyTorch 2.9.1 and NumPy 1.26.4. The workbench adds Hugging Face downloads, plotting and a notebook kernel.
16
+
17
+ ## Embed a glycan
18
+
19
+ ```python
20
+ from bertose_workbench import Encoder
21
+
22
+ encoder = Encoder.from_pretrained(variant="3", device="cpu")
23
+ wurcs = "WURCS=2.0/1,2,1/[a2122h-1a_1-5]/1-1/a4-b1"
24
+ vectors = encoder.encode([wurcs])
25
+ print(vectors.shape) # (1, 768)
26
+ ```
27
+
28
+ Variant 3 is the BERTose encoder used in the manuscript comparison. The five variants have different pretraining loss weights. Input must be WURCS 2.0 within the trained 256-token limit. Use `encode_with_diagnostics` to inspect tokenizer diagnostics alongside the vectors.
29
+
30
+ Open [BERTose_workbench.ipynb](notebooks/BERTose_workbench.ipynb) for batch embeddings, similarity searches and PCA plots. [The quickstart](docs/QUICKSTART.md) gives the same examples as scripts, CSV export, downloads and API reuse in a website backend. Embedding similarity and PCA explore the representations; they do not measure chemical validity, binding affinity or completion accuracy.
31
+
32
+ ## Binding and IAR
33
+
34
+ The package includes the nine fitted BERToseBind models' loader and the fitted iterative ambiguity resolution (IAR) linkage readout. Model checkpoints and feature data are separate downloads.
35
+
36
+ - Binding prediction accepts pairs represented in the supplied glycan and protein feature cache. It does not generate features for new glycans or proteins. Scores are model regression outputs, not calibrated probabilities or affinity constants.
37
+ - IAR inference runs only the two supplied single-linkage cases with the full reference checkpoint and saved readout. It does not accept arbitrary new masking cases.
38
+
39
+ [The quickstart](docs/QUICKSTART.md#binding-from-cached-inputs) shows both commands and their required downloads. These interfaces use saved models without training or refitting.
40
+
41
+ For selective model/data downloads, see [DATA_ACCESS.md](docs/DATA_ACCESS.md). Your website collaborator can follow [WEBSITE_INTEGRATION.md](docs/WEBSITE_INTEGRATION.md) to reuse the Python interface and check the deployed model identity.
42
+
43
+ ## Training and manuscript code
44
+
45
+ Training, evaluation, structure/MD processing, and analysis and figure sources are included. See [TRAINING.md](docs/TRAINING.md) for the entry points and required inputs, and [reproduction.md](docs/reproduction.md) for the original implementation details. The workbench examples are inference examples, not a rerun of the full training or benchmark campaign.
46
+
47
+ ## Licenses and provenance
48
+
49
+ Original code uses [Apache-2.0](LICENSE); [NOTICE](NOTICE) records third-party exceptions. Revised model weights use Apache-2.0. Newly generated contributor-owned data use CC-BY-4.0; upstream data and external model assets retain their own terms. ESM-C weights are not bundled.
50
+
51
+ [The execution summary](provenance/execution_summary.json), [sequence variant records](provenance/sequence_variants.json) and [release manifest](provenance/release_manifest.json) preserve the source and model references. Historical execution paths describe the original runs; follow the quickstart for local paths.
releases/2026-10-02/SHA256SUMS ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 87c07b5f72ac341f0eb42c040e0617c9512c4ec9f89a20790580d01d473f1b99 .gitattributes
2
+ 6f8f72cd77c10018fd9f9666bad581556097a04a67d11e3e831ef21bb250f25b .gitignore
3
+ a6cba85bc92e0cff7a450b1d873c0eaa2e9fc96bf472df0247a26bec77bf3ff9 LICENSE
4
+ b6c446fc86540a7a2c432368a5987ed15d622820a0b1bdf45c7cd7273b9bcad0 NOTICE
5
+ 5b60c2b1da93ffe81348a456f7f43ba0909855355347f1ea18436c8acb613843 README.md
6
+ 52c790c24e10061b5890260aad39cefdb6c2c615b8b9c167cc23d86393e41d98 affinose/a1111_encoder_adapter.py
7
+ c4bbe5c1a0286b319f0b71c87669b4f9cfe781051019fb02a98b5d7e208b8b2b affinose/a5_tokenizer_shim.py
8
+ 7fa8d6fe95db3ccecefdd396931d6354442800cd5284cabf519e3425cc388883 affinose/bertint_v8.py
9
+ 27e7fe6820357551e9e09a135db4a9d2cff5c4382f5396912dced12c54aaa288 affinose/dataset_v8.py
10
+ 82d1d55ed231740fff88065cff7eb9bc45907d69f7c96beebf7eb5fa6e1ae52d affinose/training_v8.py
11
+ 8452e3ac521af1d3ea24bb5a99e02a930d913fc96394e5610fe8b86e19ea89ec benchmark_exclusion/MANIFEST_SHA256.tsv
12
+ 96567c3b9c3e254bceb6bd82e43c1f252b987ced58dad41d3b4406b9f58a76ec benchmark_exclusion/README.md
13
+ 3d37d993b1cd4135ca9fe90feca856b73907d725516ea652425ca24060769698 benchmark_exclusion/SHA256SUMS
14
+ e2e0dfa7d133362146de10ba3dd774e9a3e186088f976189b0aae9d26b442bc4 benchmark_exclusion/code_inventory.tsv
15
+ 050c0af96f133ded3139baf6dfea594f91c6659f411dc25fc06d5847951b2f0c benchmark_exclusion/required_successor_inputs.json
16
+ 0507537ca8ee05b249f9ab3f3fa09de94b7acc633b91235f2778ae07659096bd benchmark_exclusion/source_verification.json
17
+ 3d598a249db90c928f8a317923ba5d37eebbc1f63383a2c39e0541f20c31ccac benchmark_exclusion/writing/review/glytoucan/scripts/build_connection_level_support_audit.py
18
+ a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c benchmark_exclusion/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py
19
+ 0f88f53e22d0c068a87a09508a9d286e4982eddcb4e2154e63b2ca25339ece69 benchmark_exclusion/writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py
20
+ 2c8821586df1caa53556b4cc14e6cd7507af4830aa8a149657571e0455eb6853 benchmark_exclusion/writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py
21
+ cd86a0a0b228c32e51ec73490711e37da3e6c9fdf9e64e980b7222b5312484aa benchmark_exclusion/writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py
22
+ 966889118fa567729e1959750b1601018ba117aed0bfad3508005ba6b1896a08 benchmark_exclusion/writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py
23
+ 92a9fb830e7adfad3d6999bd3b0cd73f2d5414934c65521a3ee7c6f59003a66d bertose.py
24
+ 0bc54399362945601bcfd403441fc80968d173200dd0561f57568b2053a94839 bertose_source/bert_training_v4/downstream_tasks/utils/wurcs_bpe_tokenizer.py
25
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bertose_source/bert_training_v4/model/__init__.py
26
+ b69f14c9976951325e3a0a4e8107a16126e67d410e966650f513f1f538a732bb bertose_source/bert_training_v4/model/glycan_bert.py
27
+ 83fde1c18cd6547a6ea026017296b5c94d209ac8f0e125057f03d507b3b65190 bertose_source/bert_training_v4/model/multimodal_glycan_bert_v3.py
28
+ 8a7f8569ff3a21513723d3a15e969120f0fdcf352943dbbadfdc54afd382149e bertose_source/data/bpe_vocabulary.json
29
+ 48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4 bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_authority.json
30
+ 53098620a5c22dafdc951ba743515b4b10b1673c02f0aa4d881db669584902fa bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_independent_certificate.json
31
+ f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4 bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_receipt.json
32
+ 918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_bpe_vocabulary.json
33
+ 10ca4b121d2f22d7a64208e4f05172e7a756b653d0828c9dcc49dd8ea1a5ad04 bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_sequence_tree_runtime.py
34
+ f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7 bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_topology.py
35
+ 6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4 bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_wurcs_bpe.py
36
+ 399df3de2b6c4d5e88aa442c8326214ab9cff2e8f0411e6921762bbade1475ba bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_receipt.json
37
+ 93ed219d807d4a7735be96bf7cf6a59a0bd407619925ee194aed6d473048cdf0 bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_summary.json
38
+ b69f14c9976951325e3a0a4e8107a16126e67d410e966650f513f1f538a732bb bertose_source/model/glycan_bert.py
39
+ 8e06c32f4e844c3c66f146fd15a075472808a0720c892d0b9683dfd32f15ef0c bertose_source/model/multimodal_glycan_bert_v3.py
40
+ 6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4 bertose_source/training/a5_wurcs_bpe.py
41
+ 2a3b28bd980b144eb42f9688f411adfbced054b9e7bad9fac4de37c6180064f8 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_submission_readback_11767377/review_a5_native_eight_cuda_resume_diagnostic_r5_11767377_terminal_certification_receipt.json
42
+ 6a1bc65c568a17dc45f1102cd122ac43c62a8ee3b28e06ec855577857cc41ce0 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_a5_native_eight_cuda_resume_diagnostic_receipt_r5.json
43
+ 9292c31195a7eddfe9f987a8cfe8cda843b283bb10a1bc8fb1b05d88f82a92d9 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_checkpoints_r1/review_checkpoint_index.json
44
+ cacec838740d64a493c2e56fd1efa8a85ba7d11fce3ec26e40905c33db05e58d bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r1/review_a5_native_eight_data_adapter_receipt_r1.json
45
+ 3b2118e128a5550ba4918b4f4f2c6d8045e319b7cff37b76d74dce6b3220c9f0 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r2/review_a5_native_eight_data_adapter_receipt_r2.json
46
+ e29df7a91d0c30238abe9155e600fb1b77b0d3d3481f6599d2ca0d2185371b9f bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_receipt_r3.json
47
+ a92f4b951476e84af006015c97698f07d5e03e7a182c54c82b7d7c5c0e68ffb3 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_verification_receipt_r1.json
48
+ 9aa5860bfc032afd82561a6db6f8df1439245b73aa3f7575b2b88836e384a1f9 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r4/review_a5_native_eight_hotpath_r4_integrity_receipt.json
49
+ 59a2fd53e9e75f6f8ec258d76e103adb61c908ab0ace6718a348739f5b70d8bd bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r5/review_a5_native_eight_hotpath_r5_integrity_receipt.json
50
+ 0c078ce52e2eb6d6feaeb1a1c2af1794ef33aef56aca7959f76a93f39d152692 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r6/review_a5_native_eight_hotpath_r6_integrity_receipt.json
51
+ 912bc1391ff66c3fd704e61266de508adcf9ed587d7e136932508029b77cb956 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260727_r7_cuda_rng_cpu_normalization/review_a5_native_eight_hotpath_r7_integrity_receipt.json
52
+ d8feaf25a7266a919c42d3a4fd1ad2d105ab088ae79c024edc665dfa29cf758a bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_production_bundle_20260728_r24_stable_null_jobside/review_a5_native_eight_final_binding_r22.json
53
+ 30082379d47318f5d9e17fafad4aa018e7e9f0f0bf1aac559178d46784fe4b9f bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_production_bundle_20260728_r24_stable_null_jobside/review_a5_native_eight_launch_config_r22.json
54
+ 826f3e8167fdd459cff7677b5b69d0f3caf9dababaaa71d9eded6070aa5d93a7 bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_production_bundle_20260728_r24_stable_null_jobside/run_review_a5_native_eight_r22.sbatch
55
+ 1238f76e5da5184d16d33077bf3e5342033918fadb4eba84242c47cb1b1322cc bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_scientific_boundary_20260721_r1/review_a5_native_eight_scientific_boundary_child_authority_r1.json
56
+ fb65dddf0bc61b4c73b5dc5ec796967e65263e9d7bdc4882e07684e882280fdb bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_scientific_boundary_20260721_r1/review_a5_native_eight_scientific_boundary_child_receipt_r1.json
57
+ 64eaf9174f523ae909c84987049d071bf3869ca8e83919f14f0572ef77b0530e bertose_source/writing/review/multimodal/outputs/review_a5_official_ragged_sequence_topology_runtime_20260721_r3/review_a5_topology_loss_v2.py
58
+ 654f5b9728c3dfd2588c4b570b0e3a1bcab8feb366af28eeead36f9beb37d921 bertose_source/writing/review/multimodal/outputs/review_a5_official_ragged_sequence_topology_runtime_20260721_r4/review_a5_official_ragged_sequence_topology_runtime_receipt_r4.json
59
+ ccd4b81d3afac8ae789c538e0af332f74f2468d403da1d243fefefd4df013538 bertose_source/writing/review/multimodal/outputs/review_a5_official_ragged_sequence_topology_runtime_20260721_r4/review_a5_official_ragged_sequence_topology_runtime_summary_r4.json
60
+ b10556643abf560b9e8622e8ae67936d2d3ca8867433db485e785f31d049a8fb bertose_source/writing/review/multimodal/outputs/review_a5_sequence_official_array_train_only_null_20260721_r1/review_a5_official_sequence_train_only_null.json
61
+ f4eb239f56e472d57dd3a3848c1a6c142e193cb326320ac8bf134b96ac35102a bertose_source/writing/review/multimodal/outputs/review_a5_sequence_official_array_train_only_null_20260721_r1/review_a5_official_sequence_train_only_null_receipt.json
62
+ 831bebd2dcf6a8308d01ec62eb6e1132fe7edbbc666c1c83787335acc576b19c bertose_source/writing/review/multimodal/outputs/review_a5_sequence_official_array_train_only_null_20260721_r1/review_frozen_sequence_masking_runtime.py
63
+ eae6b3772cd08c65cdc26bdf7ec525742452aad4a35b4d5aa9a7a5936dc3bf73 bertose_source/writing/review/multimodal/outputs/review_a5_sequence_topology_bfs_replay_all_20260722_r1/review_a5_sequence_topology_bfs_replay_all_receipt_r1.json
64
+ da7814d20c81c0d072544cfc5761e8fae0292def176d63b863083389c9caa0ff bertose_source/writing/review/multimodal/outputs/review_a5_sequence_topology_bfs_replay_all_20260722_r1/review_a5_sequence_topology_bfs_replay_all_summary_r1.json
65
+ ba9591529f3747706fbc169fe8262e23ca31a810473d10c284fb01d9190ebe42 bertose_source/writing/review/multimodal/outputs/review_a5_sequence_v4_two_run_reproduction_20260722_r1/review_a5_sequence_v4_two_run_reproduction_receipt.json
66
+ d6b3dad73d8c9cc78a7c721e7969280d0ff6500773402545bc2ff143b125f56c bertose_source/writing/review/multimodal/outputs/review_a5_sequence_v4_two_run_reproduction_20260722_r1/review_a5_sequence_v4_two_run_reproduction_summary.json
67
+ 8ff3181d22567728c0a10580c1e775da0714f17561ace81034fc673813f54af6 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_epoch_boundary_continuation_11762446_r1.json
68
+ 163374c5c6b049908d935f0d92d8f1a1adda761550761db9b2cb423bfb450ee1 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_epoch_boundary_continuation_11762534_r1.json
69
+ 85711141c9d0e84dee228df84d02df7d4693fb4b85b1ae8163466e208f5e9fd6 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_scheduler_memory_amendment_11762446_r2.json
70
+ 0d96b7867eeec85dbf14eb8f55b745b7c354edb5d16ac702d17b97bf850c79af bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_scheduler_memory_amendment_11762534_r3.json
71
+ f7adbf64e17c3a2afc677b7af9f05e50e37b4d681f38f0fc192634ca0447e4f4 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_scheduler_memory_rightsizing_applied_11762446_r2.json
72
+ 6796f0b163dcf21af5dfe3048b53e8d2660fe10290e22222232a6c7f6668396c bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_epoch_boundary_resume_20260725_r1/review_a5_static_v2_scheduler_memory_rightsizing_applied_11762534_r3.json
73
+ be55a87986dd6d80942dcd0b17dd75994145ff4a383596314d5b4ee51972a443 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_postfit_cpu_finalization_20260728_r21_stable_null_mass_adapter/review_a5_static_v2_postfit_cpu_finalization_execution_r21.json
74
+ 77ba176a9fab70e75b3c2c423c857d1e266f2021875aea01e15bac3f11f327d1 bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_postfit_direct_publication_20260728_r21_stable_null_mass_adapter/review_a5_static_v2_postfit_publication_receipt_r4.json
75
+ 661d04d76fcf36d116feb117154eac01e6569e377d4245c07653b28696e1227b bertose_source/writing/review/multimodal/outputs/review_a5_static_v2_postfit_direct_publication_20260728_r21_stable_null_mass_adapter/review_a5_static_v2_train_only_null_r4.json
76
+ 333ee6f34e69f89e26b810cc565415b89d6874615ea4ac57850371fb159c1b71 bertose_source/writing/review/multimodal/outputs/review_a5_topology_official_ragged_later_role_20260721_r1/review_a5_official_ragged_topology_receipt.json
77
+ a752ffd350fbe38aa0e6f6b42b76137f4f72d76fa9a7f5d3609bc47c65c552de bertose_source/writing/review/multimodal/outputs/review_a5_topology_official_ragged_train_only_null_20260721_r1/review_a5_official_topology_train_only_null.json
78
+ dace4cb5df71912f5aa7531bb0ce87db3b63d726b2a12aa41facabf9d54b5c35 bertose_source/writing/review/multimodal/outputs/review_a5_topology_official_ragged_train_only_null_20260721_r1/review_a5_official_topology_train_only_null_receipt.json
79
+ 68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6 bertose_source/writing/review/multimodal/runtime/a5_ms_acquisition_top30_v3_20260721_r3/review_a5_ms_acquisition_top30_runtime_v3.py
80
+ 9f8f167148aa234df413e4c3d4e1846881781c8e5a19bd91b0b178b260a3a537 bertose_source/writing/review/multimodal/runtime/a5_native_eight_data_adapter_20260721_r2/review_a5_native_eight_data_adapter_r2.py
81
+ a76c24cb0a3b904ae95bfda21bdeb2ba96e36293368fe44a218429be2b10d43e bertose_source/writing/review/multimodal/runtime/a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_r3.py
82
+ c71ba654568a24993bc1733b26b6863b11e5c198c53871a6183b485ab96540f0 bertose_source/writing/review/multimodal/runtime/a5_native_eight_direct_static_bridge_20260724_r1/review_a5_native_eight_direct_static_bridge_r1.py
83
+ b6266553c075e9b85d221a6278d1ef3ab1d26fbce5de70dbcd4569db42653990 bertose_source/writing/review/multimodal/runtime/a5_native_eight_finalization_controller_20260727_r19_external_release_runtime_binding/review_a5_native_eight_finalization_runtime_r19.py
84
+ 28d482fbae230c999d84e64a5abd6a1260ae26fb555c92b7d3176afb37cb260a bertose_source/writing/review/multimodal/runtime/a5_native_eight_finalization_controller_20260727_r19_external_release_runtime_binding/run_review_a5_native_eight_training_r19.py
85
+ 62f5355ef06cdc56f85214ac251bfa0ed4b19b41a19ab6acac51d644678f0a62 bertose_source/writing/review/multimodal/runtime/a5_native_eight_finalization_controller_20260728_r24_stable_null_jobside/review_a5_native_eight_finalization_runtime_r22.py
86
+ cb9a583091912c59f515f46074b04c90dfbe0ebf0fc28d241db980851b180335 bertose_source/writing/review/multimodal/runtime/a5_native_eight_finalization_controller_20260728_r24_stable_null_jobside/run_review_a5_native_eight_training_r22.py
87
+ 3b9ada847b1ea3ecf1f12a6566b55fe1a0218b10aed9d5dabf40190cf0e70c4f bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260725_r1/review_a5_native_eight_hotpath_r1.py
88
+ 101559df116319422762f0d7293fe55478b56defcbc1724661696163bb4fc9ae bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260725_r2/load_review_a5_native_eight_hotpath_r2.py
89
+ 941f08e0db8bbf2fa61852d4fc218f592fdd63c12f2dafdb70ed3b18e6aedf98 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260725_r2/review_a5_native_eight_hotpath_r2.py
90
+ e99f4556a8e3b3721d1f9155738d2bf4e9765e43a0e9f7734259e33533ff4b30 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r3/load_review_a5_native_eight_hotpath_r3.py
91
+ a81dd58c727a28b1a383477338e41922a2d746dac5cb33f351b4501ee8c5d0dd bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r3/review_a5_native_eight_hotpath_r3.py
92
+ 662d0766a8d68638480e9c44ca4070387481d43e1c666a20363025cf5b9bc958 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r4/load_review_a5_native_eight_hotpath_r4.py
93
+ 600af4b6b0fab63b7780bb1e6be8233eb28bd0808ff390c3096369afec9c6649 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r4/review_a5_native_eight_hotpath_r4.py
94
+ 864a05e86c5ea91318b11373d2cdffcdd2ddd0777f141cb7ef0305f779a42ea6 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r5/load_review_a5_native_eight_hotpath_r5.py
95
+ ce4bad9195229341cab0890ed775b78126c17a23d82f2244757f120c12b346c8 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r5/review_a5_native_eight_hotpath_r5.py
96
+ e2496170942db844e4ebe71d4c098b0d0cf99540d3465ed7e8e9a113469cf7d2 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r6/load_review_a5_native_eight_hotpath_r6.py
97
+ 73b42e5131c64284c6f748ddf81b4d187dcb57f756c88c22240d189f2afc4cef bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260726_r6/review_a5_native_eight_hotpath_r6.py
98
+ fa9538829ff3ccfaf3d3f4346c7e735c70758661eb6bf3da5eece2e7e96e0fb6 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260727_r7_cuda_rng_cpu_normalization/load_review_a5_native_eight_hotpath_r7.py
99
+ c0cb618f7856daf24666dbec07ff51b86e3fbd52c073c58bd1b7151122714f47 bertose_source/writing/review/multimodal/runtime/a5_native_eight_hotpath_20260727_r7_cuda_rng_cpu_normalization/review_a5_native_eight_hotpath_r7.py
100
+ 35c7a13da662f49bec07b63f590c8fb3d9cc4578633a179da42ff2650a2d7865 bertose_source/writing/review/multimodal/runtime/a5_native_eight_submission_20260727_r15_preparation/review_a5_native_eight_submission_contract_r15.py
101
+ 34785bf46f38579e48c3be6326f667a04521cb7d95c1f94c5200d84012b577bf bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260722_r1/review_a5_native_eight_training_runner_r1.py
102
+ 7e93b13b3212b3c97e30144f3e393a86a13411d5e82fe19bd55d43bc58587e30 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260722_r2/review_a5_native_eight_training_runner_r2.py
103
+ 195dbedfde65c061eb0fca5684270ff95a2bec16732683b1df7f40d91909cc7f bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260722_r2/run_review_a5_native_eight_training_r2.py
104
+ 550588ca033718273de2e35825b8b9faffe4a5fb747ff8775fe17bb31050d9be bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260722_r3/review_a5_native_eight_training_runner_r3.py
105
+ bba69e736a44fbfa6b380089d50b945e93b45d5202fa265b5869ffd378149040 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260722_r3/run_review_a5_native_eight_training_r3.py
106
+ cbc9fc0179ecb1e7c449fa37a574f38ff3e912e13db5c984d87e05e3e347a0e1 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260724_r8/review_a5_native_eight_training_runner_r8.py
107
+ 213afce295408dda11cf3ede98ebff191735a77e1db09b6252cae964caca39e2 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260724_r8/run_review_a5_native_eight_training_r8.py
108
+ 4430560565a18da0e68700d60526d3dec15fabd79854111d1e85deb59e2381c8 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260724_r9_isolated_path/review_a5_native_eight_training_runner_r9.py
109
+ b2bae85c879d2d266ac1e39869c661625958e21c1c3b7299713c8a0b46827ec5 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260724_r9_isolated_path/run_review_a5_native_eight_training_r9.py
110
+ ce7626823c2980bc21b46a7eec6c1961a87dc5512525e553c49f2c7d57bf0430 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260725_r11_certification_gate_candidate/review_a5_native_eight_certification_gate_r11.py
111
+ a8cfaca102f7c3d0c7936d244ce1e9ed75099e70c0ca3a27943be7ce5587b6e0 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260725_r12_r11_integrated/review_a5_native_eight_final_bindings_r12.py
112
+ 47e973c7c4ed5bc6686e5d7795b00d3eea5c2da8c2a15fc534a7cc24c1744ee1 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260725_r12_r11_integrated/review_a5_native_eight_training_runner_r12.py
113
+ 66270933d1ff23ffe50b800267a4705c905fc423e8e2c929bf642cb5c6c37def bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260725_r12_r11_integrated/run_review_a5_native_eight_training_r12.py
114
+ 2feb9a90db66ca277fb9e23d079ce8b089a31060e5670961a13b0e5146ed39a8 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r13_final_candidate/review_a5_native_eight_direct_submit_controller_r13.py
115
+ c30b6b03dae852a4b10c6d01596e977d8a071e738dc3074f1ca0592a50778dd3 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r13_final_candidate/review_a5_native_eight_final_bindings_r13.py
116
+ 17cf8ab216b354ec1de7574ebebd7bfe02b5935da605b4131e45c7aa13982d5b bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r13_final_candidate/review_a5_native_eight_training_runner_r13.py
117
+ a2a0a0d9056c7cd360913a792ff5e3390a190b692d7663c3a6d48a3ee0f180d5 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r13_final_candidate/run_review_a5_native_eight_training_r13.py
118
+ 737b1caa9086f8ac1556c05625d1b5391b8cae58e9d98a7755fa72f44e94c7ba bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r14_r6_strict_candidate/__init__.py
119
+ bd6c704239e2bb4898dddda5c86274f41cd5b304b35d584ddfae3884e8bc8503 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r14_r6_strict_candidate/review_a5_native_eight_final_bindings_r14.py
120
+ eda983e1b0c29357eb1b6e2f555f402a83b7176fb857488e9be5f5faed490b2d bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260726_r14_r6_strict_candidate/review_a5_native_eight_strict_successor_r14.py
121
+ 7ef11d9a891d32b0c1bb3ee24016700acd14251ac49448f004a701cdea79ca53 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260727_r15_current_static_terminal_gate_preparation/__init__.py
122
+ 1f21427486260e8b56795f7137f0f7322ae768d80645db6636414e5961aa3224 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260727_r15_current_static_terminal_gate_preparation/review_a5_native_eight_current_terminal_bindings_r15.py
123
+ 1556d6e2643edfc80b6b6782164ab47560e259ff55791c50dcffdd9b9afc001f bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260727_r15_current_static_terminal_gate_preparation/review_a5_native_eight_current_terminal_gate_r15.py
124
+ 1287db8472580db69e38d60be8c13beeddb4fdde7958a5b302d072e1226f4de3 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260727_r16_r7_cuda_terminal_preparation/review_a5_native_eight_final_bindings_r16.py
125
+ 689db8aaf51d15288f01b196c5ad2fe8ee3ca8aab3ff525695abaa9f25060ff6 bertose_source/writing/review/multimodal/runtime/a5_native_eight_training_20260727_r16_r7_cuda_terminal_preparation/review_a5_native_eight_release_successor_r16.py
126
+ 2c985edfe747bae712c41689da8be2abb5096bd97a8771691b3676d3a8af49fd bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r4/review_a5_native_eight_balanced_planner_r4.py
127
+ 52a311636e7631b97711aab63aec28e2ada47a446d1aecc1a47921d584f69f9d bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r4/review_a5_native_eight_composite_r4.py
128
+ 1b9b05c812603db7b216aa989214a3c3715f80032b3a8b82577ae2b9e48f048d bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r5/review_a5_native_eight_balanced_planner_r5.py
129
+ 24c35257fc61e1c9c77630d2b0075775a7f46b6500ecb5f175ef8f42bd214e0e bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r5/review_a5_native_eight_composite_r5.py
130
+ a482a70a2edcb7c9936b091a86a0f7d98714b5bab09a0bdc80cd927f015a46f2 bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r5/review_a5_native_eight_executor_r5.py
131
+ b5fbdff168de23676208484ccccc3cc32f613ea411d193e6d8930456e8edeeef bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r5/review_a5_native_eight_r4_lineage_receipt_r1.json
132
+ e47e6e9810db0e8ec9d37ce5533f762e287b162f811bfadc347666f5ad78db82 bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r6/review_a5_native_eight_executor_r6.py
133
+ e068a3c233fc3fd94fe3b8618edb9bd8c87fd81b97dff2666206fb870f0f898a bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r7/review_a5_native_eight_executor_r7.py
134
+ 60ab4de6bc1ee151e9da2c312c41079638e11469502e8136ede51be032eac630 bertose_source/writing/review/multimodal/runtime/a5_pretraining_failclosed_integration_20260721_r8/review_a5_native_eight_executor_r8.py
135
+ ee97bf4e68ddfbf280b7e0f01ce3409c4d4692feafbc9382613bbc5698913fa7 bertose_source/writing/review/multimodal/runtime/a5_static_failure_release_chain_20260727_r1_preparation/review_a5_native_static_failure_release_gate_r1.py
136
+ f420c84258cdf4247fdd766a9e8470bad55bd1f845ebd6640ec70b97b3953e6e bertose_source/writing/review/multimodal/runtime/a5_static_v2_epoch_boundary_resume_20260725_r1/run_review_a5_static_v2_epoch_boundary_resume_r1_scavenger_v100.sbatch
137
+ f2e558c52bf166d6e7f42ecdd92995f2b70c8a174eab8d6814875399fb69732e bertose_source/writing/review/multimodal/runtime/a5_static_v2_ops_20260721_r2/review_a5_static_v2_checkpoint_store_r2.py
138
+ 8e57c17fd43f3894f94f980cf70a0d6d7f183cf04c26780c3fe30c1932bb7abb bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260724_r2/review_a5_static_v2_postfit_direct_publication_r2.py
139
+ c46a7d4644899a5e7551e2dd5eb30666e01e0f36e2f65a2cd821b8c5565258dc bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260724_r2/run_review_a5_static_v2_postfit_direct_publication_r2.py
140
+ 3b9632e840265e1a5c1d226f3fe7fb8cd4c448f810f3826c59e19c25132dd04f bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r4_terminal_certification/review_a5_static_v2_terminal_certification_r4.py
141
+ 9429deca8cfb6cb25ef6f79a5b0ee0b91d47657e7f21f61627afd655d08fe7d3 bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r5_terminal_certification/__init__.py
142
+ 04d724ee84f67565e2f0229db99006c8f64d2492564643f6e60af25930cf12a1 bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r5_terminal_certification/review_a5_static_v2_terminal_certification_r5.py
143
+ bbdce8deb776917ade3704eb5d08b064917f52c76f3f39ee345b3b2938992add bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r6_terminal_certification/__init__.py
144
+ 195d41bba2282fdfde77a88f8b0a70eb64b0207d1a9efd42ddaeb873f8ccaeed bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r6_terminal_certification/review_a5_static_v2_production_module_roster_r6.json
145
+ bdb0fa37804531f6446cee9411c9730fde792eb9674054e56b883156f36da4cb bertose_source/writing/review/multimodal/runtime/a5_static_v2_postfit_direct_publication_20260725_r6_terminal_certification/review_a5_static_v2_terminal_certification_r6.py
146
+ 30b64143eb15688fa1e1c8aeaf3b84eb08a2a65730fa1c996c9ef6f12679d80f bertose_source/writing/review/multimodal/runtime/a5_wa345_continuation_20260819_r1_preparation/a5_wa345_continuation_manifest_20260819_r1.json
147
+ f517b5c99d93103d1212b2153c26be1cc58267c7b2d9bfa747f6d512f0fdadf2 bertose_source/writing/review/multimodal/runtime/a5_wa345_continuation_20260819_r1_preparation/submit_a5_wa345_continuation_20260819_r1.sbatch
148
+ eeebc9c7e2cfc701f3ad47e62696ae2247c8caec93d8b034d5f573b701eab2a1 bertose_source/writing/review/multimodal/runtime/a5_wa345_pretraining_20260817_r1_preparation/a5_wa345_manifest_20260817_r1.json
149
+ 5159414381025cf3e2d5e742422c84c292481168af42801c773df4ba12797fb4 bertose_source/writing/review/multimodal/runtime/a5_wa345_pretraining_20260817_r1_preparation/run_a5_wa345_shim_20260817_r1.py
150
+ a090f111df7cc0db09e61ea0a8067882b7cd9bf21d91c53da4115549560b08e7 bertose_source/writing/review/multimodal/runtime/a5_wa345_pretraining_20260817_r1_preparation/submit_a5_wa345_20260817_r1.sbatch
151
+ 600144a7f5b893958d629083a3a9266eec980765218408d2d51db72b73c1e506 bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_a1111_wa2_s424344_20260811_r2_log_route_preparation/review_md_temporal_20x6_a1111_wa2_manifest_20260811_r1.json
152
+ dd74253863f39150af4ddc7e8d48c0a366e1f327c30aa50e0748078330b97b85 bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_a1111_wa2_s424344_20260811_r2_log_route_preparation/run_review_md_temporal_20x6_a1111_wa2_s424344_20260811_r1.py
153
+ 6ffd2f97706bdf0001b90ca2f8af7ec565b298cc8585444ba6027194d0e378d2 bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_a1111_wa2_s424344_20260811_r2_log_route_preparation/submit_review_md_temporal_20x6_a1111_wa2_20260811_r1.sbatch
154
+ aa37d1e86bd3a58262f9824ba80993c0f0f280db4d0f4d704ca469dc12ba3df7 bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_end_to_end_20260802_r3_native_intersection/__init__.py
155
+ 3e96d8092239c2c21f217ad5bae4ee7e07dbe0f7630b213bad468d6b08d3b1c1 bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_end_to_end_20260802_r3_native_intersection/review_md_temporal_20x6_core_r3.py
156
+ 1e446e7e89a39ef01132b9f2a4deb817161f7b376a0fa04c2b11529004f53e8a bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_end_to_end_20260802_r3_native_intersection/run_review_md_temporal_20x6_training_r3.py
157
+ a6a180ae103eaaf68458b298cd4180920a0e88c1724a6ba9a94ef94559e75c5f bertose_source/writing/review/multimodal/runtime/md_temporal_20x6_end_to_end_20260805_r4_dev_memory/run_review_md_temporal_20x6_training_r4.py
158
+ 32285cc5fedd421032bd98b97214f899ccacda9b211c1b2a97a3fb1513f1ad70 bertose_workbench.py
159
+ 3d3077181b519b0106d9201ee8514a41905e32057237d8fdb48343e10cfa97b3 bertosebind_inference/README.md
160
+ 81f3da9b814892c3f12b2f7a535284b89f0be07b22bce50a374c8303d6db6c72 bertosebind_inference/__init__.py
161
+ b7cfcdaf1c902664252f3d2022e290a803892a31618c1290c09c174131036541 bertosebind_inference/check_real_examples.py
162
+ 07282f0fec34ef2cf9f832c3662227ca89064b52e3fdbe9fd3ad11b5f4e01154 bertosebind_inference/config.json
163
+ d748e004c483597c86cc214ecd43f5c902e3d3ced41d1e9223b555dfa3b9ed58 bertosebind_inference/examples/doubly_cold.csv
164
+ c50e124eba39a8bb5f08e4d75e54b0f168ff74ee886d7f28e3156b3beba6ad96 bertosebind_inference/examples/glycan_cold.csv
165
+ 6d70ea272cb716c7bf001675493496bfe7491add0185c010f83417a8ed607784 bertosebind_inference/examples/protein_cold.csv
166
+ 3de9b7e6b9c6035c0e07504b1dadb2c1c430151b7c889c33825a437a5c70e659 bertosebind_inference/examples/reference.json
167
+ 0c074c3901ac55b737e10a2b316db3c00678e569d151fc3e2192b46bd1662ce6 bertosebind_inference/loader.py
168
+ e06c540874abbfd72b72b6ee41aa2d35a6d35bf11d26e10714f382d031765b3a bertosebind_inference/local_checks.json
169
+ fe3f559157a6bc3dd4b59803fb010054c8987f402b6790f488e6cce51bc95d4b bertosebind_inference/predict.py
170
+ b18eed4c3ad4f0cbdd700e41d6ff7af1f520d26386f76f4573e82332f3e1ea28 bertosebind_new_runs/README.md
171
+ fc4ffac8de965da6250297ee24fa801be2619f1afcc29471204c7e5aead03012 bertosebind_new_runs/audit_pretraining_exposure.py
172
+ 2b8be4fcd7ebc98f49e9532415644f457930a1e7700d5c17fdc58a0abd0dd271 bertosebind_new_runs/check_package.py
173
+ 45a366b40a523671427950b83ac2c8a243c96f1bce8527b0af2337a375ff2b15 bertosebind_new_runs/executed/local/audit_current_pretraining_exposure.py
174
+ bdb19bf323c94eacc1943b4345b5b2485dfa50b7ac00a755e8a1dde2f8a4ed74 bertosebind_new_runs/executed/local/collect_current_glycan_sets.py
175
+ 9c9b90f077709c105dd16b706ebcd434dd793177c274eca17b10bc3241f0138b bertosebind_new_runs/executed/local/dc_anatomy.py
176
+ 33d41f680cf7a5ae3b98fef4d1ef604f6e12a6a856ec6e90df8bb5a88f336bbe bertosebind_new_runs/executed/remote/aff_attribution_motif.py
177
+ 2b89020895eebac8c99366036eebb07dd43c687fe47274dcd1de47d1017548ff bertosebind_new_runs/executed/remote/aff_attribution_motif.sbatch
178
+ d7f21b8037172e6d42fb65c8169d37b19c385d68dc9cbebadbafe9e930843074 bertosebind_new_runs/executed/remote/affinose_gifflar_macro_A_glycan_gifflarESM.json
179
+ eca94bfbe82bfba95c2086c9c4ffe14091d442017356394ec2e75173cb66810f bertosebind_new_runs/executed/remote/affinose_gifflar_macro_A_glycan_sweetnetESM.json
180
+ 529296c1388651b4451f35829d0caa63e692a96fc88b74ee9b8dc8c1d09136da bertosebind_new_runs/executed/remote/affinose_gifflar_macro_B_protein_gifflarESM.json
181
+ 6f0098947aa4c962f991d1e17acc17707d7cb2eb5e9d6338182d6cfa65d28a2d bertosebind_new_runs/executed/remote/affinose_gifflar_macro_B_protein_sweetnetESM.json
182
+ bf720edacbad09cd04c42df7cf347dd1fe9846a22f307f7be1c38f06e89faa44 bertosebind_new_runs/executed/remote/affinose_gifflar_macro_C_doubly_gifflarESM.json
183
+ 1884d744232b56c5edc76e655cf9bd117e43d8d749c98300b528ea6a5e55b75d bertosebind_new_runs/executed/remote/affinose_gifflar_macro_C_doubly_sweetnetESM.json
184
+ 0d4458462cd10dae219a9f15eb6dc8129d200a09891bd0b6bf0297cd85c4dae2 bertosebind_new_runs/executed/remote/affinose_glycan_cold_contamination.json
185
+ 61a5ef32482365e37e58bd4aee49ff5dabc2fafa04f4e063f3a0be7c294c84c7 bertosebind_new_runs/executed/remote/b4_baselines_perrow.py
186
+ a381978d36492bb21cdb7c2a3007c51414b7630dd88016c7a74eb19c48080925 bertosebind_new_runs/executed/remote/b4_gif_repeat.sbatch
187
+ 7fe6a80de1e7662a916e84b0a679c5579ab7fdcaaace70e1855f7d8face51ca3 bertosebind_new_runs/executed/remote/b4_gif_repeat_compare.py
188
+ 1d0433763d7ad49088e396b435a42f2a12141436db05592778a79938c88a953b bertosebind_new_runs/executed/remote/b4_lo_perrow_ckpt.py
189
+ c82d7fe71d178d78d52ee47d809cd12d0286c48b1375c0c7b8b80fa222858e61 bertosebind_new_runs/executed/remote/b4_step1_structure.py
190
+ ab7560abe237da435464a984a4e23925e7ec48d802949f57ef9600b829e6b37d bertosebind_new_runs/executed/remote/b4_step3_bootstrap.py
191
+ 6dbbbeb14bb860f6344a93bb65754f540dee8da4ceb3fdc526133c7133dd426c bertosebind_new_runs/executed/remote/b4_step3_bootstrap_r2.py
192
+ 50049bf5658f27700205e91d07f40512b8d297a7af11f51fcfbd514df9b59478 bertosebind_new_runs/executed/remote/diagnostics/affinose_b4_gif_repeat_20260927.json
193
+ 7f9edc64528d5b43d2c464f8f5d4db91c2a133f54364993c7798a2a606bd3427 bertosebind_new_runs/executed/remote/diagnostics/affinose_b4_step1_structure_20260926.json
194
+ 17ab513ec2d9dec6f1e8c9445dfd7500190f99f0e8758b21fbb435b026324f3b bertosebind_new_runs/executed/remote/diagnostics/affinose_b4_step3_bootstrap_20260926.json
195
+ 6a87e6a8abc926f41ca13b1bb6c81cbf9ea1d04b06e8f39bfdaf847ce55ba235 bertosebind_new_runs/executed/remote/diagnostics/affinose_b4_step3_bootstrap_r2_20260927.json
196
+ 1ab7793ddb0daa247dc1f75b53356663d2c99e2e279a33207c9dd82f24288d17 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_A_glycan.yaml
197
+ cda3d3e0df11dcbffff287c985039a4abe486a39bb97fe1554851992cbca127e bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_A_glycan_s43_gifflar.yaml
198
+ d11d6ca73935a664a9afe9330d07e51b7e95e89e044a1012065ef1780d820381 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_A_glycan_s43_sweetnet.yaml
199
+ dab891f502cb328f8313a9d2971e5d8134472b059fa776409f97f5f59e5d470b bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_A_glycan_s44_gifflar.yaml
200
+ 875883d0ec38fca1a4995ea76c00db81ba84357fb2ed9a7c0921dbb6c7cf6492 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_A_glycan_s44_sweetnet.yaml
201
+ 529c3940cfce81ab3d135f25b722d7c302cd83e8540646f041a5d6ba36baebf3 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_B_protein.yaml
202
+ 908379483dc5b2d0655aa7b0a13b33c8b70ba0379852ffc2d848e354ebd9945e bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_B_protein_s43_gifflar.yaml
203
+ 57cbf88ddf620ffbec7c4a8167a271ec38da0f94b1d3a4e9fe9769a9a19b8eea bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_B_protein_s43_sweetnet.yaml
204
+ 5e518f2db88c03663eb263a5e4a44e93a3bc43e51c4af6c626cd0da416a18d81 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_B_protein_s44_gifflar.yaml
205
+ 50c68416d3d4d72445e61811474179cb552e60d9f8a87f6cc0ed1da32d8b55e8 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_B_protein_s44_sweetnet.yaml
206
+ 034ba8bdefc3410b987d7bc380bd24a469a01c912349fef1e35436df2a24c36e bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly.yaml
207
+ ed244565f2f81e6c554613718398af97683ad375eb4debb5a61d25855c444368 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly_s43_gifflar.yaml
208
+ d8ba3e8d83871cdd92548bc8bab1493225cdcbd536c0004ff6b40b48e4e62e40 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly_s43_sweetnet.yaml
209
+ 2faa1986c043cf5d8b869d316afa0a043d071400c4f2018ba83551332a143f25 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly_s44_gifflar.yaml
210
+ 5fd2cdec3ee4d49f7d7441a037e0a2fbe428b1c0586de88beaf17a2ab67580f1 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly_s44_sweetnet.yaml
211
+ e8fafb98627685ededa4392c5310e149a7dbbe907e37b43da7d20d228961b181 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/configs/lgi/ours_C_doubly_sweetonly.yaml
212
+ ab257b93c23a522dcd99f37b2b802e67c8e3b4ba46b938db6229a6311a77cd39 bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/gifflar/data/utils.py
213
+ ab79451e7b51e27d985904a4445fc09b756d759413bb2ebaf8755d177bb3e8ad bertosebind_new_runs/executed/remote/external/work/ratul1/supantha/glycan-SD-VS/bert_training_v3/v3.1_cluster_training/bench/models/GIFFLAR/gifflar/train.py
214
+ 56bf1e5b11a386d0920b9f50da6288eff222ee1b7f25d772a8a666a2387b2a9d bertosebind_new_runs/executed/remote/gif_seeds.sbatch
215
+ 241d3ca6045610a70ccad8307448b4d57e47810c945f127fef768357e56cdef2 bertosebind_new_runs/executed/remote/gifflar_launch.py
216
+ e391584ec821c1e86916c6f8c14a6571a550034fd246e1dc0e7d7b3b6d4dbd7e bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s43_gifflar/LGI_gifflarESM/version_0/hparams.yaml
217
+ 35d0ef06a1b9725233ed1e58c74101b7e6a555da48165ed57c9bd71bb5e77315 bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s43_gifflar/LGI_gifflarESM/version_0/metrics.csv
218
+ 0dae829566b87d0dd00613343bc5a0c49fb1107c0f4834327d8cca71b8932389 bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s43_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
219
+ fd6f39dd3bef9326bc1f583bf07a1f9b8f02f846630a0c58c66c7bae70456d59 bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s43_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
220
+ fb07ac7cfcb63c4dd77c7493f4a518e11d268fd5e8d4cf6a849c31552e48369a bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s44_gifflar/LGI_gifflarESM/version_0/hparams.yaml
221
+ 1d9e1b8c6fff12f8603ce8f7e58189da0ae8580e72d3c5bf5ff3893a4f9bce2f bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s44_gifflar/LGI_gifflarESM/version_0/metrics.csv
222
+ 4902e83edaa137f55f5fe4f32f32068e94a3f4832630b284048c22b8a099dca6 bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s44_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
223
+ b911e06e11968f9d8655ab69619f33a8878759e497eb44498432c341dc6fbdd1 bertosebind_new_runs/executed/remote/gifflar_lgi_A_glycan/logs_s44_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
224
+ 75ffe300584ac7f56ecc28e85be309a3f7c9f506a615d1edc8e77aac4f3d4b7b bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_0/hparams.yaml
225
+ 346d480fa44e31a5a486bfa99c136cc5ddfee27a197194b7fac289de9ba55665 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_0/metrics.csv
226
+ 75ffe300584ac7f56ecc28e85be309a3f7c9f506a615d1edc8e77aac4f3d4b7b bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_1/hparams.yaml
227
+ 6a42d5cf20bf64a7b027660beca902ec84a80a1beb73e8aec1e4eb26fbb7c913 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_1/metrics.csv
228
+ 75ffe300584ac7f56ecc28e85be309a3f7c9f506a615d1edc8e77aac4f3d4b7b bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_2/hparams.yaml
229
+ a71483c34b9d9cd904a6fe241f88af3e30bb3e100a15d666def5b06e4a5c0431 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_gifflar/LGI_gifflarESM/version_2/metrics.csv
230
+ 2213baffb7e7ada5a1f384d75380098d53bc92f2c3134e94d35750ab9e419c16 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
231
+ faee86ceadc7e1dbe6835d44aef8c5d4f810a2cbe224e13202154b52dab92be3 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
232
+ 2213baffb7e7ada5a1f384d75380098d53bc92f2c3134e94d35750ab9e419c16 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_1/hparams.yaml
233
+ ed66bf0654eeaabf3b4f5f92e1295abc53a0155095160092ffb47826ff385d1a bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_1/metrics.csv
234
+ 2213baffb7e7ada5a1f384d75380098d53bc92f2c3134e94d35750ab9e419c16 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_2/hparams.yaml
235
+ 127b095cc67faf26378a247dc7345de56d331537dad375990ed319f684fbde27 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_2/metrics.csv
236
+ 2213baffb7e7ada5a1f384d75380098d53bc92f2c3134e94d35750ab9e419c16 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_3/hparams.yaml
237
+ eee99377078178660db1f12c2700b0c46c9d8120db5f445a8d71d3f584cc9fcd bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s43_sweetnet/LGI_sweetnetESM/version_3/metrics.csv
238
+ 0c1b007e6c5b0146ed2927ff60720f7c9fba81286f4dc4b866f896e18c878d35 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_0/hparams.yaml
239
+ 47b51d96993782f3c9e6117722176941bd2c6d571200d160e28aa654c97c6356 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_0/metrics.csv
240
+ 0c1b007e6c5b0146ed2927ff60720f7c9fba81286f4dc4b866f896e18c878d35 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_1/hparams.yaml
241
+ 89d056126636c1fd1fa44d27ecfd57040a9bfc9eec2a76104da0ae07d01680b3 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_1/metrics.csv
242
+ 0c1b007e6c5b0146ed2927ff60720f7c9fba81286f4dc4b866f896e18c878d35 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_2/hparams.yaml
243
+ c066cdbf7827bdec2f87c15bf95a42e0a23b511bf03338482e4d2b20c149336b bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_gifflar/LGI_gifflarESM/version_2/metrics.csv
244
+ 1192dfb103f69ed8970d6bb263fa292648b6c673b3067a33b46d26940d0231ef bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
245
+ fe5d987579ac234e8f21bb70e642177e9eccb69493564b7c5895e6abd51f4dc2 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
246
+ 1192dfb103f69ed8970d6bb263fa292648b6c673b3067a33b46d26940d0231ef bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_1/hparams.yaml
247
+ 0ecfee6421a63cee0f0537fdb311e44b87227421f637816a0a3ccfd132763d16 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_1/metrics.csv
248
+ 1192dfb103f69ed8970d6bb263fa292648b6c673b3067a33b46d26940d0231ef bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_2/hparams.yaml
249
+ 0d313e8557cb13577b34c8e94e0f2c887c11cfa6da6ca36ab2aa5f9d1cc10f22 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_2/metrics.csv
250
+ 1192dfb103f69ed8970d6bb263fa292648b6c673b3067a33b46d26940d0231ef bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_3/hparams.yaml
251
+ 5ef2b9552f452e4218b9728c0ed57f7a14ec5068d065c7223e2c6e566e5dafd2 bertosebind_new_runs/executed/remote/gifflar_lgi_B_protein/logs_s44_sweetnet/LGI_sweetnetESM/version_3/metrics.csv
252
+ 781862ff668231f3501e892c7be567e4e3a16709f6b433e97edeadd60ec1e8bf bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_gifflar/LGI_gifflarESM/version_0/hparams.yaml
253
+ e92266239d6445a0de886e5081e433632c6719719f6d536ad31e98045160fcea bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_gifflar/LGI_gifflarESM/version_0/metrics.csv
254
+ 781862ff668231f3501e892c7be567e4e3a16709f6b433e97edeadd60ec1e8bf bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_gifflar/LGI_gifflarESM/version_1/hparams.yaml
255
+ 929a3396f429527836db8368e7908024bf69766e0567636b1f24df8c1c06ea42 bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_gifflar/LGI_gifflarESM/version_1/metrics.csv
256
+ f7852d23330d3b55e46eec193b3fe411306ea59349de6fa53273294d893dce1c bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
257
+ 1e68c5af1f9c7fe0f1c85d9f3d49fa1104bf3d4c768014154316ee27b94a3cc3 bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s43_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
258
+ 987afb2ba2360ff7f2a5a94943d064a7a3e5c902b53e445bc59bc291241e9d9a bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_gifflar/LGI_gifflarESM/version_0/hparams.yaml
259
+ e937607ecc6a7058664faab71728c58ef7a9bba8f6985082bca5d565aef6ec7e bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_gifflar/LGI_gifflarESM/version_0/metrics.csv
260
+ 987afb2ba2360ff7f2a5a94943d064a7a3e5c902b53e445bc59bc291241e9d9a bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_gifflar/LGI_gifflarESM/version_1/hparams.yaml
261
+ eb420f0ea03bce2075ad4eac9237862ccbecb2c37c02d162d860516f4739b280 bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_gifflar/LGI_gifflarESM/version_1/metrics.csv
262
+ 22796804655424d5977b04b0b171d4da9fba18896eb4928842c4fad6bb7911dd bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_sweetnet/LGI_sweetnetESM/version_0/hparams.yaml
263
+ 30f0105da3f2c579a8261a30ed6c3fcc8308cf9ebc2068f0e71168567e919922 bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_sweetnet/LGI_sweetnetESM/version_0/metrics.csv
264
+ 22796804655424d5977b04b0b171d4da9fba18896eb4928842c4fad6bb7911dd bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_sweetnet/LGI_sweetnetESM/version_1/hparams.yaml
265
+ 168da3dcbf6ca795bb75ec67b9fc567788637c25875b766cc76b441825e6020c bertosebind_new_runs/executed/remote/gifflar_lgi_C_doubly/logs_s44_sweetnet/LGI_sweetnetESM/version_1/metrics.csv
266
+ ef833104d5eecd273df7d9f7c39bae4a216b015bde15978ea716f1f864874de4 bertosebind_new_runs/executed/remote/gifflar_score_macro.py
267
+ 578635213e1a3eddd61438a6249f14e1a5d2487267286fc459eb77972808e225 bertosebind_new_runs/executed/remote/gifflar_score_macro_seeds.py
268
+ f88bcf303685ea6a075747a12eafa7df041909b9b455d80ea98c2b536aed0de8 bertosebind_new_runs/executed/remote/gifflar_score_perrow.py
269
+ e65e21b8c9c9bd0291a034852f7ab47f826e1f51448d9ea8439b5eb23ed8eb98 bertosebind_new_runs/executed/remote/logs/aff_attr_motif_16596807.out
270
+ dadfe2c408938645b4138bb011719affdaebe7098060f66354df56da71d0a146 bertosebind_new_runs/executed/remote/logs/aff_b4_grep_16604967.out
271
+ 79eb29df46c44942a2a6ac4313789ebc29e1672bec4512c4f965be5ebb739cc9 bertosebind_new_runs/executed/remote/logs/aff_b4_s3_16604914.out
272
+ 5d93f96fb6e537c9b802fd1b2d9fe25df41a19800f85ea860246274d2e897d71 bertosebind_new_runs/executed/remote/logs/aff_b4_s3r2_16605803.out
273
+ 6c486b37a642549a8dcf924eabd52e2660d88a6ada53ba05aecb91937fd98626 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_0.err
274
+ 7832596dfd62832aadeed39c1acca59643f00ec8dbe812730b49bd1b2ca5150b bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_0.out
275
+ bf5e3ff79c6b942dca7f35dcb85d0f32081b03875cb4640022fab29dff833b11 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_1.err
276
+ 134b045efbf1f560d0d41fa702ff53b6369fd38235fcef55ca8aaa4615698fed bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_1.out
277
+ d911a9619dee68a9fc982bb3db253b8e72f12dc1c06d2bc434556211909b2719 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_10.err
278
+ c079926adcc0d2bce5b591f870208dcd2bef2ea3029aee8950bf0ce44c1105ea bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_10.out
279
+ 5024c4160aeeccd212930b0d959ed0bab4dd7bf593b61b26decd78c7513aa638 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_11.err
280
+ 4d110e939761ff4d8c7938c754e182d2c09ede84c8360d10ce18a04280dafea6 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_11.out
281
+ d9383fa7e0469a436e08d24f4f9af31c1a6fb9ebb5e115843b53e2b2ded612f1 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_2.err
282
+ 0bbeab27824eeffbbfe5cf04317d1795b1f8f02d4c5ad9b7dbc23e159a823940 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_2.out
283
+ 735f42f273046b3af0374283927429fa75a1b7a8b66d6b205a20e4e1cfd59eed bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_3.err
284
+ dcdbc088e7945d0f63803ac95aed81f3f8c8d25c97dd760df269921a08bacae7 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_3.out
285
+ 98912251710582e65eaed2ba01ee184d957d177ec31e30969939b4ef0962a2bf bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_4.err
286
+ b1febe3591a887bd63d7bb607536ce17bdab51eb9db9a8b8c3d9d5ff77aca1c2 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_4.out
287
+ cbb38c7fc50100cfa44cdf9b64de3df9a002613c62d8489b9d3db301406712c7 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_5.err
288
+ a1b8f17ba63285c54d42ad49ce61bf8356ac4dfe279de8b65f687304783bdf55 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_5.out
289
+ 8ec18cd103ee2f80b2bacc8660a53588ed6313dbdc2455fe87fb5cdac6d30ef1 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_6.err
290
+ 18050c22347d3a2900b0d15cd7f2b97c1f6028ff27e5e060fc33c8308851b358 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_6.out
291
+ 1a1baefb3f9d8bae9c38975310f96004470e75bed27b32b5273b31a9baca05f5 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_7.err
292
+ 8869a90d89089a6f26c7d88b1a389630930c35adff10016a015bc5bb331308b7 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_7.out
293
+ 01b633582a3ed13728f688fde16693926778896ba0952506cce66072f600f354 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_8.err
294
+ 8f468cb20588f01bc0794c43ea69d5fa78627f4e35ef7ecdb7ffbcd3266c78c3 bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_8.out
295
+ a3ea46d3bd9191b9bbd49627cbfdac0dee312230e5e1ed93016f98f8786b94ba bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_9.err
296
+ 313f8c5e3999dfa75ae508fd57d0d033a00b40cdf2b794cb4c32fb0a901b53bb bertosebind_new_runs/executed/remote/logs/aff_gseed_16597159_9.out
297
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_0.err
298
+ 19bb18892da4f9174e308bd42908a6e6d383001edd75c9ad482eef111b5bbb31 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_0.out
299
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_1.err
300
+ 152af1228aa39534d68470023321d4350bb2ea4b997bfd0bf38efbbeb92bad8b bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_1.out
301
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_10.err
302
+ d842f047adc16782a9724406ecab07aa67fe3ad853f5d0c8bee73cb63d129782 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_10.out
303
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_11.err
304
+ 922dfe72fd457b50a6c468db266827346ab1173381458ad61e21ceb5a0c8736c bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_11.out
305
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_2.err
306
+ 2b008f6ae0f5554b9841582ded12a01558218558d8b7748899e94a0499d8caaf bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_2.out
307
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_3.err
308
+ c1d325125ee0463b230e5ac347c45a117ebc1a1686393d9d7bd63dd7c9a8a8d4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_3.out
309
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_4.err
310
+ 939f30920d3d4a772d8736c945f6e39dc949cde64cc7b9b9e0116e7b45f4f895 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_4.out
311
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_5.err
312
+ 7817d9b5c087d542f2c0659726a66532091a24c9b05edb04196a756291dd5784 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_5.out
313
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_6.err
314
+ 4283aa1d699582d4766ca394cf2d9c4b046ce8247b5cef0f8250c424329138a2 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_6.out
315
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_7.err
316
+ eb0257132cbd79d1a3a84bec543183b7bbdaceaaaefc85c988368d70d4146322 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_7.out
317
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_8.err
318
+ 523b5ac11259832265dab5c4e08d837f3fe7e4780048a5a4af1f513f6b8c3d4a bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_8.out
319
+ 53e0d436715c2b2e61c45d8474df7edff033d7693326a363aa202198e7001ff4 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_9.err
320
+ 40ef3d8f12ba723cfe0e845ec72a960dbd32c8bc5baa3013f570e1c27a6431b6 bertosebind_new_runs/executed/remote/logs/aff_gsmac_16602170_9.out
321
+ 06fbfcd2f820fc4621d35c16dcd6bbece93ea68b96d417d8b7bd2ce3ea104cdb bertosebind_new_runs/executed/remote/manifest.json
322
+ bf602abb05eeb3b0203766d9d1bc462579c181bc3418d762f6d02d5f36e8a230 bertosebind_new_runs/executed/remote/train_lgi.py
323
+ 9d13452309d64f138705657334b74738eaf167c9faff835d024582e0e98c0af8 bertosebind_new_runs/inputs/current_glycan_sets/doubly_cold_exposure_join.csv
324
+ e5eebf2072779d7001dabd3981237025be37eee6f19a5b109b00ce4b3e776e09 bertosebind_new_runs/inputs/current_glycan_sets/doubly_cold_scored_glycans.csv
325
+ 4d101cc92685c63ecca99cfad2c1bc46c73c77b1e565bd686bde14ee0d020886 bertosebind_new_runs/inputs/current_glycan_sets/glycan_cold_exposure_join.csv
326
+ 0faab973a6ee5e5bf712983d51fad9f746e34edcbe5342574d45933bba62f48a bertosebind_new_runs/inputs/current_glycan_sets/glycan_cold_scored_glycans.csv
327
+ 7655d9d080df7cb5dc748b7d9721037fbec67c04cf837e4724f0afba7e7a4d71 bertosebind_new_runs/inputs/current_glycan_sets/manifest.json
328
+ 08ceef347f61012e0baab4daf1472122dc80335c6ea02fe084098c055a3e3e30 bertosebind_new_runs/inputs/current_glycan_sets/protein_cold_exposure_join.csv
329
+ f35d6088903e993f46adee07806e577456599d9814f4f43e40efdf3d8ddf6c46 bertosebind_new_runs/inputs/current_glycan_sets/protein_cold_scored_glycans.csv
330
+ 56d04a5a985d2bd96067e5c1a1e7acd67c5f76ae05c17831bc5949cb6bee26ab bertosebind_new_runs/package_manifest.json
331
+ 924765b90a840277c5ed9dd6594a8ded2d5f7add5b138a9af1adf89bbb46cbcc bertosebind_new_runs/protocol/amendment_excerpts_20260925_26.md
332
+ 04dea299feb3ac0840ff5db7471381ad3f1cf531653bf6aeb9825005236d2f0b bertosebind_new_runs/protocol/original_20260910.md
333
+ 11014b5f0972c23b7225c4202f2254b444109b8105563d968e9c8f4534061ff1 bertosebind_new_runs/provenance/bootstrap_anatomy_audit.json
334
+ d2812e72a7befc1bdb08465189d2fed035ae725142c1cf978a578e0add72e94d bertosebind_new_runs/provenance/bootstrap_code_audit.json
335
+ d6e36605873c587028c6dab2dde8093c6e4e33df53ebc27f1d6e3c6b4de14991 bertosebind_new_runs/provenance/bootstrap_exact_input_manifest.json
336
+ 30a70a1703183b50fa1c10986c065471513981fac1e3e69f9c7ad649aeecb16a bertosebind_new_runs/provenance/current_pretraining_exposure_audit.json
337
+ ff65b38435040ba2f5987e0c78ecc0fafc4604341b9bc094513d47cb343db023 bertosebind_new_runs/provenance/exposure_portability.patch
338
+ 6649cef7a00096ba2981c611a4c6432be93300e3bb439cd543a692cbb40a15af bertosebind_new_runs/provenance/gifflar_scorer_diff.txt
339
+ 7d24cac24a37583fddac7d91c73e22ea6d4a022194ac213ab354f9e9831b1084 bertosebind_new_runs/provenance/protein_gifflar_audit.json
340
+ 9d2e628a789a46a69333c24550d2a150166aebed382880c2d0a67fbd43124eaa bertosebind_new_runs/provenance/protein_gifflar_verified_numbers.json
341
+ 4b49c6f8f6c9953cee46ff2f6c47e1bb68692c9b58984973b3d8666648999f62 bertosebind_new_runs/reproduce_summary.py
342
+ 99acda4c8365d691c16de3e032e225de05cee38cff063a772bf9eaec2067328f bertosebind_new_runs/requirements.txt
343
+ 0dff08537f0cd5a1ad40562537c48546d3d4f3858a75b2ab2d3729b22a5dfe91 bertosebind_new_runs/results/doubly_cold_anatomy.json
344
+ 4186c66c13fd05d62d83a10df2a5761b2e29ded502660c12cb450836f9881496 bertosebind_new_runs/results/doubly_cold_per_assay.csv
345
+ 2c494ac1351d4ff63828139eac004c0047dc3259d62fd10334d5a740cab9182f bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_A_glycan_gifflarESM_s43.json
346
+ 82173016b893cb2913e90e99feea5f6f4ff22cff9e1aff0fe7dc136161c7f627 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_A_glycan_gifflarESM_s44.json
347
+ ac58a83969a0ffb860cf1a33a472f4cb5b3e14f25097c3ba909f7555f65aa589 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_A_glycan_sweetnetESM_s43.json
348
+ d2d2d73c834a2fcca7b8c22fd6ce91907f18866443628c2c54f8b8ec04477803 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_A_glycan_sweetnetESM_s44.json
349
+ 7b0a01a1acc7e2ca80a3de9aaab02941942ead519e6661abec6c44c7fa5fd079 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_B_protein_gifflarESM_s43.json
350
+ f2e40d306b767e6949c02304077a08398dc16ac19d08d94937f7dc49abd7cc24 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_B_protein_gifflarESM_s44.json
351
+ e13c8021d6e94b3bd6cb4ef00d7456c182449f57c4b1356256e011805a6efdbf bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_B_protein_sweetnetESM_s43.json
352
+ d274d35c9a0ba25e4597cdb8c01001f4f7ff7760aabfe7c8a00b84a70b74252f bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_B_protein_sweetnetESM_s44.json
353
+ 2208056d5303fda27c5b541eed1c6f64cb03de1c10abd2868d4ce55d1bdb475c bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_C_doubly_gifflarESM_s43.json
354
+ c831378d73c3c2c9456be5ff9ff05acc34b64815b056c5df21bef2078a415393 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_C_doubly_gifflarESM_s44.json
355
+ 121a30d0de568217e722036cf25ba4ef61128398fde3466fc8b2f60f096722c7 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_C_doubly_sweetnetESM_s43.json
356
+ 3d29ac377491db5d952f6066228fe4099f99db0e2fbaa80063ee429fc28e0a28 bertosebind_new_runs/results/gifflar/affinose_gifflar_macro_C_doubly_sweetnetESM_s44.json
357
+ 4fd80f7396aa9803602fd62bd82ec3e98e1f937b46e1057cb26ffa56ea6c93f0 bertosebind_new_runs/results/protein_probe.json
358
+ f224aac78366d64d60b86841b11a9694d234e955904112d266de9bbbc4d8af3e check_release.py
359
+ 9acda1420a4df8efc4b73432e517c32a55aa26b4d9c7bb3487e2a565565089e0 code_inventory.tsv
360
+ 6d805fd046c5aca89ef531f37950bb73ef15125865c6a841dcc4934ba7932780 docs/DATA_ACCESS.md
361
+ 1fa5b83d57f611207e04c88d2190d6cd364833b4c6ec9c0ca35649937ec5f645 docs/QUICKSTART.md
362
+ b5072e437307aedfd1f789b3dc8024498e6c96ffb6f80d06d0f6a08a805d4b00 docs/TRAINING.md
363
+ 5f19be94138c38e9f35afa27aeedb514f9421a4e4724ee27c76ec0302487eed3 docs/WEBSITE_INTEGRATION.md
364
+ 52263f18a04e465305b49d53a73f9682849fd138fe817d4afb774c48252d869b docs/reproduction.md
365
+ 4008db9ad60a8d97e5bf705cee0da416dd6755eed15ab4882ffa61e04e4299a5 download_assets.py
366
+ b8fe7b4e50ffa4caf7ca9c50742dd279bb4a717b6fad629a24971ceab635b2ab embed.py
367
+ a46c2c31f02fb0829fbe70a29c4c032b16a1cd5208386c2ecbc9f6c3fc2018bc examples/glycans.csv
368
+ b4a8c26479a118943646d4baae370c1d0a267fe30c487fbf816a3999b5a4ed03 examples/predictions.csv
369
+ 903d14d8bafc9ae2aa968a74ad4b9d31396c598624fe05e2acb1814cb60bd60e examples/smoke_affinose.py
370
+ ff95ff2a7707940b849a44259cdf356f66dd5b0351faf4e400feac6666afd9af examples/smoke_models.py
371
+ 89b65f9186f2080eab214f1b7389854cd8477124cdc729a428624312fb7a1453 executed/downstream/review_a5_native_checkpoint.py
372
+ 1c958427bca7991b8ec091b6f0c8d209fc1ddfaf1aa9e1f96ad22ed5c60b839d executed/downstream/run_review_native_md_on_terminal4_s42.py
373
+ 75d530c95c218fafb25e844865dd3884737d11b23c4cfdc121226549923fecdb figures/Figure_style.md
374
+ 83791502b5e689451ea735a3da108185ad5aed4d861a187b68c622e0b4b3fe69 figures/README.md
375
+ 14addd0706d771da818f53b0d17a0248ba496c7e6ceb3850b89cc6573d35ae71 figures/ablation_figure9/README.md
376
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/ablation_figure9/nature-bpg-palette.json
377
+ 179bf3ddb15842c726a90a9cf373c7861409c8f823bae344a0b621fa77ed51f9 figures/ablation_figure9/plot_data.json
378
+ 457cfdc37e557c623d23292cfb221b214b9f158ab9ec611576b5553969ce4469 figures/bootstrap_figure13/README.md
379
+ fbddc62114cd214265d12bb11584004988c169731fe66c8faba20d1c871bb876 figures/bootstrap_figure13/build_figure13.py
380
+ 6a87e6a8abc926f41ca13b1bb6c81cbf9ea1d04b06e8f39bfdaf847ce55ba235 figures/bootstrap_figure13/inputs/affinose_b4_step3_bootstrap_r2_20260927.json
381
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/bootstrap_figure13/inputs/nature-bpg-palette.json
382
+ 7b14add6ccb6e16d72ab066ae902b77fd15cca4baab69c52015a828c5bc32b35 figures/bootstrap_figure13/inputs/review_affinose_gifflar_seed_welch.csv
383
+ 8a5ce609298e83a231895587c64a24bbc535a3887feb157ef2b4934db4355243 figures/bootstrap_figure13/inputs/review_affinose_individual_values.csv
384
+ cd5a299c7b3a21ce8bbd0c445e66b938e497658e9ffe602d562517eb1ce103fd figures/bootstrap_figure13/inputs/review_affinose_results.csv
385
+ c0957bc56e0694a72289183e7424b49383346e6e55c245b92132c2e77ffc6f9d figures/bootstrap_figure13/inputs/source_manifest.json
386
+ d734b8076500af5edbd2bedd26f29004fb066725a45c273e35b49a1a711d26b1 figures/build_bpe_figure3.py
387
+ 0ceff115647c4921165805fb25fb5ec5451e8ac1264a9140b3db8591f30f66d2 figures/build_corpus_composition_figure.py
388
+ 6053ddca0892325752308544a39da61d427de959f6559ab771902b0c30c7c103 figures/build_grey_seal_figure5.py
389
+ 90fd5e292872323cf90b2312903f8320731808e382a4798cc033379ebd57e568 figures/build_submission_figures.py
390
+ f112a826a7b210c2700d810c2a62f91d80c0f2e964b141290f0343f648ce8a81 figures/build_supplement_figure1.py
391
+ 2b33f563431fc64271f84bebfbf99210e057d4f6d8b4b838316c574aa6bf33b8 figures/build_supplement_presentation.py
392
+ ba1cbfccd6a8f0e029849e9188ae212cfbf9afbc5174d1099970911821c740a1 figures/build_supplementary_figure9.py
393
+ 25e648b564e4db77532947bcd432cf82ef15cd08f5676441a21feffadb9eacfa figures/build_taxonomy_figure11.py
394
+ e964dc2d021382d5950008eceef05d001d640de608b0d86ce37701db5de8379e figures/build_token_length_figure2.py
395
+ de51e3d170a28a319aed0eaf8f0ee5c1a3b95c59146d0360a3e228c947178a78 figures/build_training_coverage_figure6.py
396
+ dd7f57d15e39df52dfa3a92a1ba3aab7e85c284fabc926cffbcad7876366b852 figures/compact_modalities/README.md
397
+ 5d2748b98cf4f4ce9b74b71b8617d98c6d52a7cf3b68b567210727659804b363 figures/compact_modalities/builders/build_figures6_7.py
398
+ 000b70258a2b599a326903d9792e11b6bb67adf5ed6730b5fddc416df6cbe7d7 figures/compact_modalities/inputs/figure7_original_provenance.json
399
+ 3e96d8092239c2c21f217ad5bae4ee7e07dbe0f7630b213bad468d6b08d3b1c1 figures/compact_modalities/inputs/review_md_temporal_20x6_core_r3.py
400
+ a0096e0a0cd5ac6ade78c6b0ee4b0b0fffc7d2c8969899f26f03134aa63edd8e figures/compact_modalities/inputs/review_training_modality_coverage.csv
401
+ 2373a8e7e2998023e764a4a69dead5f3f3a5cba87743fd8762e31bfb55e03393 figures/contrastive_figure12/README.md
402
+ ed7446d8ab3b4a0258cc031361b93e1613fab438786a81fc1a775b88f6ae3c67 figures/contrastive_figure12/build_figure12.py
403
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/contrastive_figure12/inputs/nature-bpg-palette.json
404
+ d732e76c28cd3d741fad99636a8937b464f1efa190d26e7005146e6c88b1feb3 figures/contrastive_figure12/inputs/review_contrastive_fit_values.csv
405
+ 5bfd6352e60158fe3a09c2a3228a7fffc4926780aa320b5e205ef276db3e4113 figures/contrastive_figure12/inputs/review_contrastive_paired_summary.csv
406
+ dc90a817bc54ac310a73298f60d03fc30eaccd724f4676eb826301dd85470acc figures/contrastive_figure12/inputs/review_contrastive_taxonomy_summary.csv
407
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/figure3_inputs/nature-bpg-palette.json
408
+ bd530abacb61f3d6cffda8d1e7e6112f9261fd0a761e594fddceb5c615b9497b figures/figure3_inputs/partition_count_flow.csv
409
+ 918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b figures/figure3_inputs/review_bpe_vocabulary.json
410
+ c885944b4ebce9b08c99318ce51c81b7648309c49ba5244ad0345b794429f151 figures/figure3_inputs/source_example.json
411
+ 8bed92feb2c7ff8f8759e4fa287470336e364764ac7b6fead456840b92b5a5db figures/final_iar/README.md
412
+ 014b0949823ae8691f09efccab38919400282c33e0fd9b6994b39a6fb4ad78dd figures/final_iar/build_final_iar_figures.py
413
+ 259fdc68a39b415c4ec114fbd6872ceaccaf09327d1fde958390071a5a94af2a figures/final_iar/inputs/baselines_same_cases.csv
414
+ d5b7fd634677ea22ea27670649ea30636ef6c98daedc8592cd3f5d7b56beb9c0 figures/final_iar/inputs/final_iar_summary.json
415
+ 50906ea3964a4a7c9744d224c8ffe2d356738d83bc0f5ead05b04ae6c558a64a figures/final_iar/inputs/iar_recount_and_wilson_intervals.csv
416
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/final_iar/inputs/nature-bpg-palette.json
417
+ 0aa0dc0fb74388a72750578acdf9beafba917a1976059c813157a90eceae3419 figures/final_iar/inputs/run_manifest.json
418
+ 61136a3d00e0184db8fbf4db47fa480766e7dadbb4f81d278df27b1fd0a0613c figures/model_variants_figure10/README.md
419
+ d88d4db86071e94d9012f0062696920c2c44a9a15a66e3f152826943234aac0c figures/model_variants_figure10/build_model_variants_figure10.py
420
+ 53fdfc11a75e3aaf39b78a9c59d90e206b0726579440d03df2d5cb5708249ebf figures/model_variants_figure10/inputs/review_weight_policy_ranks.csv
421
+ 9b35cb9ab5056c0548eea944f8472906a8cb36e2a7adf5cf3135268c675619b7 figures/model_variants_figure10/inputs/review_weight_policy_sensitivity.csv
422
+ dd96f6ec2247a918a89873dd031be5f738bcd870056e9b98cb7a1e2c85e158e8 figures/taxonomy_figure11/README.md
423
+ a2ad6df46618b2e94ecd46fd8a2ca41c8d43556d46f26fd8066a102f42b1404b figures/taxonomy_figure11/nature-bpg-palette.json
424
+ 04670f06e3459608d7a43da767531544421d4da1a6125376edc3939dba951d1f iar_completion/README.md
425
+ 8f7e1027e67222d49eb9e3c12fcdff083005d182583990c08c54b7ac3c11040e iar_completion/SHA256SUMS
426
+ a03990a02a1737de05a7fa1739164e84fa6a0a4170ebaa65317a9f3c2f16c0b0 iar_completion/assets/linkage_readout.pt
427
+ 37514cb79322719772b5c14a5795f00a78f3275f3315d5b3b674ee97127ca953 iar_completion/check_package.py
428
+ bbf016ce2f1a7dc82940be28397cbe8083da7ad0f5f525b1cf7345fd42a5758d iar_completion/model_metadata.json
429
+ d406620e3b0c070dff4d6fb6e0c57effce848a9ca5b3dc3de705d65c49ac0961 iar_completion/originals/run_all_training.sbatch
430
+ 5acafc45244a0bf9a8489794f8a39f956622cf50594d87eb2eaa04502589fcb5 iar_completion/originals/wa3_final_iar_readout.py
431
+ 72f8583bf32799ba29c09829d138900c0cbb589f73bd278f389da6fa8a786730 iar_completion/originals/wa3_final_iar_readout_all.py
432
+ 679499406106611dc2c70240a3c14c9b94311bc03a7811d3dae807c58aef4ef8 iar_completion/originals/wa3_linkage_diagnosis.py
433
+ 0ebc629778c854621f485ceb4f5f11090e84ac9171d52270fcc90b6b07d6fe7f iar_completion/originals/wa3_linkage_probe.py
434
+ 0da5f24799afc8978b88c85ea7856deb8c641ebae319760e88539c93a4eb3a86 iar_completion/originals/wa3_same_test.py
435
+ c6e9506dbf97752aa248e325e75acf3abf63495054bcac68d48602967379531d iar_completion/originals/wa3_typed_strict_test.py
436
+ 1341b0051fefaf54b4113d7dda5e408540844d12a9275f9a73607c416f35928d iar_completion/originals/wa3_typed_test.py
437
+ 259fdc68a39b415c4ec114fbd6872ceaccaf09327d1fde958390071a5a94af2a iar_completion/provenance/final_run/baselines_same_cases.csv
438
+ d5b7fd634677ea22ea27670649ea30636ef6c98daedc8592cd3f5d7b56beb9c0 iar_completion/provenance/final_run/final_iar_summary.json
439
+ 0aa0dc0fb74388a72750578acdf9beafba917a1976059c813157a90eceae3419 iar_completion/provenance/final_run/run_manifest.json
440
+ a615d9a889c7a62c19d05a4d07d66d9e743fbfb9e37e4d646c889490d297e492 iar_completion/provenance/heldout_masking_wa3_final_iar_readout_20260926_run_manifest.json
441
+ 0aa0dc0fb74388a72750578acdf9beafba917a1976059c813157a90eceae3419 iar_completion/provenance/heldout_masking_wa3_final_iar_readout_all_20260927_run_manifest.json
442
+ 933ef4d07c380bd4322814acff135e207e00db36197c3dc543214b24cb5d554e iar_completion/provenance/heldout_masking_wa3_linkage_diagnosis_20260925_run_manifest.json
443
+ 3f713f4e074cbb068264173a8fb0ec99813665ad1b23fdea507f7389071cb04b iar_completion/provenance/heldout_masking_wa3_same_test_run_manifest.json
444
+ b3cf90dc772f206dc386a3a92d96e43d7a6c1de4147bc7cc501599730d31807f iar_completion/provenance/heldout_masking_wa3_typed_20260924_run_manifest.json
445
+ 4a13b9faecc329257c2aa08fd6734a8b6c02837d0b230b0f659fd00602832c75 iar_completion/provenance/heldout_masking_wa3_typed_strict_20260925_run_manifest.json
446
+ 9c957fe6b76da503c909ac4e3a0d70cf4ea1fd6da34a3aa6573bac4722d4c62d iar_completion/provenance/local_checks.json
447
+ 8af8ed8b6124f26072d6041deb0e6966c13c149926f5bd720b714884cba6801e iar_completion/provenance/required_input_hashes.json
448
+ 2cb3a3f4e8b1d37dd3ad0234996d546f34ab48231bf69247ffb6ace0651a4a69 iar_completion/provenance/source_inventory.json
449
+ 42464d76c743a302bae79ced6b09af9657444c6f977535b5de49dcf59a5a8a90 iar_completion/readout.py
450
+ 9ef74c17ef3ffacde3f0fc862f9844757bacfffa55b3ac9e7e6a6cca43f5843c iar_completion/requirements.txt
451
+ 3392850a98f1739e3eec8434f42c26f3fad2d3da9fc815a17dd2790aa3b0ff98 iar_completion/runtime_dependencies.json
452
+ a791b8f4bbda75317f41afb34d62fe518448f4d5d90db0b852e2b0d0af5e83a6 iar_completion/runtime_snapshot/java/review_A5WurcsArrayTreeAudit.java
453
+ bc38e44e66daed7ec04f7c7907a13509aac125a8a09c97d9972725ab4278edaf iar_completion/runtime_snapshot/java/review_MolWURCS.jar
454
+ 5449f9276a7b952adf087468c874bb13c7295062cce7d1afb13fdea78ace7f37 iar_completion/runtime_snapshot/java/review_wurcsframework-1.3.1.jar
455
+ 0bc54399362945601bcfd403441fc80968d173200dd0561f57568b2053a94839 iar_completion/runtime_snapshot/project/bert_training_v4/downstream_tasks/utils/wurcs_bpe_tokenizer.py
456
+ c68cd003370b2dcdb162f848f766e4e62f2653c6c38d205f8cbe53a9aabe2d74 iar_completion/runtime_snapshot/project/data/bpe_ambiguity_tokens.json
457
+ 6a572afdf53f1494ab96c896876b824ca7ea749777352606aa9f96bf270ceecc iar_completion/runtime_snapshot/project/data/bpe_vocabulary_clean.json
458
+ 22d2aad45b978988c9e26bc9400016014ee825cc6207377385a8cd74f3c88f2b iar_completion/runtime_snapshot/project/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_bpe_ambiguity_tokens.json
459
+ 918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b iar_completion/runtime_snapshot/project/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_bpe_vocabulary.json
460
+ 10ca4b121d2f22d7a64208e4f05172e7a756b653d0828c9dcc49dd8ea1a5ad04 iar_completion/runtime_snapshot/project/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_sequence_tree_runtime.py
461
+ f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7 iar_completion/runtime_snapshot/project/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_topology.py
462
+ 6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4 iar_completion/runtime_snapshot/project/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_wurcs_bpe.py
463
+ de8c3237b858b640ee9fbf4dac34864c54b6dac80bc89f346a44c774254289a4 iar_completion/runtime_snapshot/project/writing/cluster/nature_submission/code/model/__init__.py
464
+ b69f14c9976951325e3a0a4e8107a16126e67d410e966650f513f1f538a732bb iar_completion/runtime_snapshot/project/writing/cluster/nature_submission/code/model/glycan_bert.py
465
+ 8e06c32f4e844c3c66f146fd15a075472808a0720c892d0b9683dfd32f15ef0c iar_completion/runtime_snapshot/project/writing/cluster/nature_submission/code/model/multimodal_glycan_bert_v3.py
466
+ f0c8efda7a12942b4c6359792d6ee9417e6a9ec0eb04623ad17b4772430270b5 iar_completion/runtime_snapshot/project/writing/cluster/nature_submission/code/training/ipa_bpe_distillation.py
467
+ 8d05a4048e200625694ff9a36f50af89b845fd0d38ba8034a97a710e7ad3dce2 iar_completion/runtime_snapshot/project/writing/glycangt_heldout_ipa_validation/cluster/heldout_ipa_eval.py
468
+ cfce20d28291c8b5868d35386990037415090202dd823bdfd73038456cc65557 iar_completion/runtime_snapshot/schema/classify.py
469
+ 704ed130bdcce94a7af7ce3e37654705f709b8526f632907ef794451c6ef7f30 iar_completion/runtime_snapshot/schema/pipelib.py
470
+ be4b51d806c364f6aa88119d17d71674b8313dde8a35c82099ca755a5c46e0b6 iar_inference/README.md
471
+ 1d21cb72f1cc7d5a094a5fb5f0ec11a6072df996f0975cc39569ed9964daee1c iar_inference/SHA256SUMS
472
+ 043a38744f9fd832181d822ae8481488d954604bf615b2eb05c4c8ad29794325 iar_inference/config.json
473
+ a212cfbd49ec2f951e942c92caaac63260687a626eca588cb692d86bb1e52198 iar_inference/fixtures/case0_pre_sequence_head.npy
474
+ 6ffca438526432a33168a85a0fee1158fd2a9c9f23ecb97f587235f204ae50b3 iar_inference/fixtures/case1_pre_sequence_head.npy
475
+ 60ac0c0fd1b6e0ada2d847d1fccff4f6fe7c085aab3146247dc5d7bfebbabc27 iar_inference/fixtures/rebuilt_two_case_inputs.json
476
+ 60dcdb6262457962da84c7b239c194fac343bd8d27db0e05f15906acb9bf5f4b iar_inference/fixtures/reference_smoke_report.json
477
+ 9237b349668ea3fb5266f4696169fd958e9f997e5064ede0ab06e41859b17361 iar_inference/infer.py
478
+ 87fa484e1f6105fa476dff4cf8c7e6aea71a2aac537796131a4858632f54b906 iar_inference/original_inference_helpers.py
479
+ 4e36f9237dc494eb47898ff3ab9a5b1a483f5004aa933a5294af794edd0757ce iar_inference/preparation_validation.json
480
+ 16e89b25a13eabc8586ba2a8ec32890ecf321f652b03f7548c63112d13cb2e62 iar_inference/requirements.txt
481
+ 144e09f44c6cc2c09df0fafefb2c3975c590ae684756500fae9b6a589767574c notebooks/BERTose_workbench.ipynb
482
+ a4b66d2ed429ad6516646256a84173082b2fca6b779baa6e4ff93586a81a5c2b provenance/WA3_checkpoint_index.json
483
+ b8b05510e97f6b94ca5e0d1465eba034724d7ddcc405aa405db09c4c29fc768b provenance/WA3_final_receipt.json
484
+ 670cfcbcfe89e0729bb75f122cff8c7100c24d39ef775c69421d4bb820654056 provenance/affinose_arms_on_test_20260909.json
485
+ a291a8e8f2b77c7955b5346aa52289afc520a0b5181d62eb0dfa2c096bd6bbb8 provenance/affinose_cells/affinose_mo_20260908_mo_A_glycan_s42/receipt.json
486
+ fdd520b70e38d0e6bc112118adad89cf209278614645b47e219341736bf99599 provenance/affinose_cells/affinose_mo_20260908_mo_A_glycan_s42/results.json
487
+ 29b68fb30127700fa55942d5cc4c237f8bf15c2d0c883192b62698d811e71c51 provenance/affinose_cells/affinose_mo_20260908_mo_C_doubly_s42/receipt.json
488
+ 54918a535b86b01590bab11c78ff4c354c106ca78a384446eb79038b0df666af provenance/affinose_cells/affinose_mo_20260908_mo_C_doubly_s42/results.json
489
+ a380534df6f877c212a2c7dbce577511e282537bbff9ad5d7ac3e186f1cc5633 provenance/affinose_cells/affinose_mof_20260909_mof_B_protein_s43/receipt.json
490
+ a732a56ddc81fc41b02dde8f56d852929ec8eefa12524ed9410a09361f267544 provenance/affinose_cells/affinose_mof_20260909_mof_B_protein_s43/results.json
491
+ 9344a727416db1368c45a0ddad962a12130e10f03944a5142874f72ba5311297 provenance/affinose_cells/affinose_mof_20260909_mof_B_protein_s44/receipt.json
492
+ a530b2fac81a72d99674906253897a1e3eecca4ee17e83c3a27ed409a3411877 provenance/affinose_cells/affinose_mof_20260909_mof_B_protein_s44/results.json
493
+ 068bb72cc2fc0231f3a9f729ac78f1a6340a7088ea86a60449ebe63dcfbd794d provenance/affinose_cells/affinose_mof_20260909_mof_C_doubly_s43/receipt.json
494
+ a21307bb48db62270792f990541bac1c2728feb34063f4b2009e2f6c7d8a7e4c provenance/affinose_cells/affinose_mof_20260909_mof_C_doubly_s43/results.json
495
+ 3044b56619b1bb83063070624b02bfaa64c952c1da76a80051bb62bb26098395 provenance/affinose_cells/affinose_mof_20260909_mof_C_doubly_s44/receipt.json
496
+ 42ffeb2f81acbae80df3284300d0213b52918b90d3b186dbbd1f074800af75f0 provenance/affinose_cells/affinose_mof_20260909_mof_C_doubly_s44/results.json
497
+ 07a919272c928f9630f4d70a73cf1aa91a327e2c9597cf582814c5386b6f24cc provenance/affinose_cells/affinose_sa_20260908_mo_B_protein_s42/receipt.json
498
+ ef5a0b42f15f23a473d46198bd39dc17176add75a1bf76cd708d5b595d552b6e provenance/affinose_cells/affinose_sa_20260908_mo_B_protein_s42/results.json
499
+ c3e3028a05cf7f5b58883d9485bcd7a7e0ba5d05265f4503b8bad09e307c1480 provenance/affinose_cells/affinose_sa_20260908_moc_A_glycan_s43/receipt.json
500
+ f62ddffaffae54f91e4f921fa235741df4023e94b47c2c90f3e158b7c4cd9858 provenance/affinose_cells/affinose_sa_20260908_moc_A_glycan_s43/results.json
501
+ 6f5cbc9e19a45b4e9c8d33e7f4a82e41ef610b5040175852d3c05ea172995d4f provenance/affinose_cells/affinose_sa_20260908_moc_A_glycan_s44/receipt.json
502
+ 9f0a6dc5b89f58603fb368e754da90001a987016ed4d9ab425420703aeb44d9e provenance/affinose_cells/affinose_sa_20260908_moc_A_glycan_s44/results.json
503
+ f4fb099a7dbe229450d2ca337d213555203dc69dd67f1e4b2cca227baca3d701 provenance/affinose_input_hashes_moA_glycan_20260908.json
504
+ 00c53175abc949336bf495c796dc9c779af2109227bc6f351433356129d85663 provenance/affinose_input_hashes_moB_protein_20260908.json
505
+ 3d3c6fd5c39fe2f1ce37ce5ba47b842fa193b716c08227678d68cdcc7e996c4e provenance/affinose_input_hashes_moC_doubly_20260908.json
506
+ ebf5e35b60640944bf3d03955f10da2c40cd5075a85bf0e3a4c6210f97ed85a6 provenance/affinose_recipe.json
507
+ 9c14d2981a11bef92ef230e9848f9876d941b290f2c70848e739a2795bb6feea provenance/artifact_locations.tsv
508
+ 4dfa51299e99a23add6cdfe5380403d4504a207e2b6522ea86cc50ba1cd2cb74 provenance/candidate_affinose_input_hashes_moA_glycan_20260908.json
509
+ 48768c773a262864e7a5104732d1e37971be2581058dec7e6f8778d05483965c provenance/candidate_affinose_input_hashes_moB_protein_20260908.json
510
+ efbfbea3df98ff7b20dadae530aaf5e3cdadc7216a60a97495b4344e39ef1934 provenance/candidate_affinose_input_hashes_moC_doubly_20260908.json
511
+ 2ce6f4a60fdb0bc87d69e270bd724fbccbfae6239579f3c17c0c4d8de05b3d94 provenance/candidate_portability.patch
512
+ 16bed602b94a1a3010f07c2feca3e219035160ab5cc05f4ab202faf57ae1c63c provenance/copied_sources.json
513
+ 967d38d95702e0863eec468dac9de27fee7da60312fc53ba0f95c5b88f381595 provenance/execution_summary.json
514
+ f4cd53bb88fb383677e0f85417662c01a351a954db6df93b6162d0aadda495b0 provenance/local_checks.json
515
+ fc689486dc297277e32480d52d9abb69549520c085101bd406ef3ed6c2fad86b provenance/local_test_environment.json
516
+ 7202cdde019556a7babb3459ae04da8ab5ecb5edbdfde2b74c4a675ed1c9ff43 provenance/moconf_cells.tsv
517
+ e6f1be53ab77ad80ea274f80bd11369bb4efe3836b6119c58283ec12af224752 provenance/mofull_cells.tsv
518
+ 67b5ade1b857e789173128939c6baa4687354bb1d640d8fd3b40332b5956ec72 provenance/motif_cells.tsv
519
+ fb75dee7521e1ca381d9b1fd5c7054bb7ba6389dfde61259362ae4d082ba9a7e provenance/portable_execution_20260930/binding_portable_report.json
520
+ 24f61f4291610df9d7ad89a0cc8afb6a472e41b2dab3ef1ca73d852076c073da provenance/portable_execution_20260930/code_snapshot_manifest.json
521
+ 07fee191f75e12c5becd19b3dbe627ff3a9dbcfcdeb0aea770126483bfd0cebb provenance/portable_execution_20260930/execution_receipt.json
522
+ b51a0f8f5c7b5682e4c432587236bce9daa74e6f2389ccba89b34bcc8c0042cd provenance/portable_execution_20260930/iar_portable_report.json
523
+ 7fb7d9274cc72e65b8a8026256841c9fbcdbbdf82747b319b46b008d4ca4700c provenance/release_manifest.json
524
+ dc42f4cdd76cc2c49fedfe6b8b8ef40b5915ee70803c9cec5ec0b0573256f815 provenance/score_arms_on_test_historical.py
525
+ e2bad73cbdcc16a2a44f2d6e8230bc55b5a8a484d7553af29e379a12f541bba9 provenance/sequence_variants.json
526
+ 6fc928f84838ac82af228d744fadb8be670d631afd5ca84ff84d0847b140d8fd provenance/submission_README.md
527
+ cf0e33d454260a8fa949a9c5fea60bac6291b84c3db573e48fb1735f3a2a2772 provenance/submission_SHA256SUMS
528
+ 80aae521be2bdc5ac2337ea733f7a8ade028f788499d7d44ff1b6e7389639c8d provenance/submission_code_inventory.tsv
529
+ 48da67d3a47fb1155389fccdd9022b825b2eeefb60e28260633ec4c2fe00a33f provenance/submission_requirements.txt
530
+ 05832b5f811b977c09418c79545d8e38c689973b4a91e36a96e20389ac3c0e2c provenance/third_party_archives.json
531
+ b7cb8a39e167f2cc9e35476dcdcad56320b02c1898d13db3236a41bbb2ed4852 provenance/training_v8_before_resume_load_fix.py
532
+ be8f4ae30ae2fbd8f91de0e5db98de3044213e24587717775c31df37e2272dbb provenance/wa3_config.json
533
+ 881354fe1ef1eb6ae73565a42da2b175a0426f41feb6b3a1d8897c4e048fa012 provenance/workbench_assets.json
534
+ fcbf7a31abd2f468c589feca4111a1d5b851e587046aed53ca0196deb3f39e74 requirements-analysis.txt
535
+ 2c69dc6c9b8643afe0e9fbd5f2f355662dc55f2b555f2b096b582065baa2db74 requirements-workbench.txt
536
+ 1c89cbdda9d2c1fb7a3df6692f93eb2765f59f64dd3392b8799697f3238a8867 requirements.txt
537
+ 428d6c855cd9a6a1988581f9d71bd85a130ceb7b803e6190b3590f5e40ef426c score_predictions.py
538
+ 1a8459fea8d7d35948bd4315d840a0ec6f60e4fcb4823e18d903294f2c1c20d5 structure_md_processing/README.md
539
+ d1c48e285928b5b4e2b84f7748bfeb3360f0c98c8d66ac1651927f849e5384f6 structure_md_processing/SHA256SUMS
540
+ 39c6691d762b1ac2d0bed3464b1ed31e4fbe72b18e218ba4a553f3f94702a9e2 structure_md_processing/evidence/review_md_parent_temporal_20x6_v2_summary.json
541
+ 823f089df162283c6ed2077d7c8af1cfc49d1f814e58a029b64c2c18a0a075d8 structure_md_processing/extension_checks.json
542
+ 2e644b90c80388a169da946a22557e0d6d0f37876334fa5d450f27fce41aebe1 structure_md_processing/independent_qa_20260930.json
543
+ 1e15e8a160c1af8e31c490fde35e9177fce583d98157751a852846d1390cb43a structure_md_processing/input_closure_20260930.json
544
+ 0f43a293b4b5edaa76da4c4073a7a221582b064c41375a7de982b09dbfe05642 structure_md_processing/prior_snapshot_20260929.json
545
+ 0d967082a8225a1eab98315c2fe7916eff0e863ec04287a43351b757e08f5070 structure_md_processing/project/training/__init__.py
546
+ daae9ee3f3c286e36c7fd7ede37f889eca2a6f9956755717a5bf714645ffa4bb structure_md_processing/project/training/a5_glycam_chemistry.py
547
+ ee6e3d06fce7316b3622f4ad92d900608eb91af09f3872d0200c1b246158605f structure_md_processing/project/training/a5_static_alignment.py
548
+ f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7 structure_md_processing/project/training/a5_topology.py
549
+ d08683664ffe28661becd6bdfd5b6b8169bd430800c3e844218969548d3498a2 structure_md_processing/project/training/masking.py
550
+ ca2be55ece9c15271713f553f619bdd42cf4292c6c9d86e19037db9a73168e28 structure_md_processing/project/training/multimodal_dataset.py
551
+ 21ae467f964ea98486ed7efa3f7887194e05c326e4fa92f124e8d5f5570f030c structure_md_processing/project/training/multimodal_masking.py
552
+ 2e3b0a72767fa5e7ca950ada7923faac63e58db53f6969a030e01938a81254ad structure_md_processing/project/writing/review/glycam_structure/configs/review_md_temporal_20x6_v1_20260801.yaml
553
+ 4d668c45f1f17f12b1d4af022d2e1849cec815e24664e8ad9183569fae7f764f structure_md_processing/project/writing/review/glycam_structure/configs/review_md_temporal_20x9_v1_20260731.yaml
554
+ ddabd88756774a9c75ae93709fafb6b7c2d3cdf0c9b7d5b66fd801d8e2388c76 structure_md_processing/project/writing/review/glycam_structure/scripts/audit_review_md_charge_corrected_exact_roster_20260802_r1.py
555
+ 131499356bc91f07ebd1f2f40166c9f756d7e64b49a66ee69d1efa0fcecf9843 structure_md_processing/project/writing/review/glycam_structure/scripts/audit_review_md_parent_feature_package.py
556
+ dacc54f37dc4c816fdf2fb801b13eb239cf42444883ace1dfed2a92b6fc798da structure_md_processing/project/writing/review/glycam_structure/scripts/audit_review_md_temporal_20x6_r2.py
557
+ 2df5009bdcefb49ed7742391d7d6f5f73cf1574fc7d17cd46bd2ad3098331a06 structure_md_processing/project/writing/review/glycam_structure/scripts/build_review_md_parent_feature_package.py
558
+ 4bad11cfd69c150f2c50b92856a6ab21641772baa2236b45ca505f5637ea5a0b structure_md_processing/project/writing/review/glycam_structure/scripts/build_review_md_temporal_20x6_r2.py
559
+ 2743e322eccd5c0217b44e3eb6c4dc242d35db7eacf91f235d23f7a846b70dbb structure_md_processing/project/writing/review/glycam_structure/scripts/build_review_md_temporal_20x9.py
560
+ 35108afde036b0f1fbf888014a5afebc45568b45dbdea85a5913c2dba8938127 structure_md_processing/project/writing/review/glycam_structure/scripts/freeze_review_md_temporal_feature_layer.py
561
+ 102466ed3bf04ef2cf2ab53b04fdbb9c6e6ea1d16061194f97eed5bedc4e2e2d structure_md_processing/project/writing/review/glycam_structure/scripts/merge_review_md_charge_corrected_temporal_20x9_r1.py
562
+ 47c9d9a448325e59742e30a444b636e888df18cf88f010fdad09e0393f5bf9c9 structure_md_processing/project/writing/review/glycam_structure/scripts/prepare_review_md_charge_corrected_feature_overlay_r1.py
563
+ 80c1e2cbf2f2bbf7a19564a7e88d65e2d9dd4a190c33e6b5405dd107d6e553fe structure_md_processing/project/writing/review/glycam_structure/scripts/review_md_all20_charge_corrected_run_state_r6.py
564
+ dfc5670ad8a146af48abfb381198bacd017ad7dbac4a081bc204d5dbf61cb1cb structure_md_processing/project/writing/review/glycam_structure/scripts/review_md_wiley_pca_corpus.py
565
+ b9f20eef44858866d95338a6f5dedebf253d8e827890d10806ce7d0045b14594 structure_md_processing/project/writing/review/glycam_structure/scripts/review_md_wiley_pca_pilot.py
566
+ 39f9ddfd0e91c3afc1c3b6085689f8bed7bf585446128be634ab97369a79869b structure_md_processing/project/writing/review/glycam_structure/scripts/review_md_wiley_structural_extension.py
567
+ 163c93827dc18dd281d8e5a6ceb8b106a02b00e3366442ee09fd0e1b477262e8 structure_md_processing/project/writing/review/glycam_structure/scripts/review_md_wiley_torsion_pca_corpus.py
568
+ 3cb1b2ea38fef04ba9ba2b01ef1944f69a48fb5dcfd4d8011684de9ddaa10ca7 structure_md_processing/project/writing/review/glycam_structure/scripts/run_review_md_charge_corrected_feature_stage_20260802_r1.sh
569
+ 21ffbc09d34291adc6d2a3535ef142014d1d31ea8a28002fee23af29b88f415a structure_md_processing/project/writing/review/glycam_structure/scripts/slurm_freeze_review_md_temporal_feature_layer_20260801.sh
570
+ bf3ef76494d936044615b59e0941f3b6fb35a935d6743abb5c035995b61a63d5 structure_md_processing/project/writing/review/glycam_structure/scripts/slurm_review_md_parent_feature_package_20260731.sh
571
+ b8e953947b4ac55e1cd9d509c33e1bddf211f046b9124ddc175166a1a763b31a structure_md_processing/project/writing/review/multimodal/outputs/p1_a5_static_role_successor_cascade_20260720_r1/review_a5_static_role_successor_cascade_receipt.json
572
+ ffbbe2126eb6131ab8b64ab5809b79b29e5534785cbe6fcfe1bba04b93e06050 structure_md_processing/project/writing/review/multimodal/outputs/review_a5_static_glycam_template_registry_20260721_r2/review_a5_static_glycam_template_registry_r2.json
573
+ 6bba07e7da9284af46b97f80c25c7e1dc332facf452e0fd2140a975b7c1e496b structure_md_processing/project/writing/review/multimodal/outputs/review_a5_static_glycam_template_registry_20260721_r2/review_a5_static_glycam_template_registry_receipt_r2.json
574
+ da8d6dab478ace650fa5af3e99c72dedaf65a6cbc2bf62373fa2082bef748cea structure_md_processing/project/writing/review/multimodal/runtime/a5_static_v2_20260721_r7/review_a5_static_graph_materializer_r7.py
575
+ 12fd869f52759168e0f3ff3243ac7dcdb36c0b067d83e24931c185c67fc80c26 structure_md_processing/project/writing/review/multimodal/scripts/build_review_a5_static_graph_v2_20260721_r7.py
576
+ 8da3a60c3d205f9a56f3a97842f3c05e647d3f89ad56c91d431eb75ede49c9ff structure_md_processing/source_manifest.json
577
+ 28c3941d1ae7ebc86296a4ac7117c27a1d4bcaa0ec6c56ea8cfd18fade1bb7a2 test_bertose_workbench.py
releases/2026-10-02/affinose/a1111_encoder_adapter.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load an A1111 / WA-lineage BERTose checkpoint as an Affinose glycan encoder.
2
+
3
+ The a5/md20x6 lineage (A1111, WA1-WA5) stores its sequence tower under different
4
+ module names and a different vocabulary from the submitted-era v5b checkpoint that
5
+ `bertint_v8.load_bertose_encoder()` expects. This adapter renames the 209 sequence
6
+ tensors and resizes the two structural embedding tables so the tower loads STRICTLY.
7
+
8
+ Verified 2026-08-19 against
9
+ review_md_temporal_20x6_a1111_recovery_20260806_r1/.../A1111_seed43/
10
+ review_checkpoints/review_md20x6_checkpoint_best.pt
11
+ -> 209 remapped / 209 expected, 0 missing, 0 unexpected, 0 shape mismatch.
12
+
13
+ WHY STRICT MATTERS. The submitted Affinose shipped an encoder that was 95% random:
14
+ `bertint_v8.py` assigned the pretrained tower and then called
15
+ `self.apply(self._init_weights)`, which silently reset it. `load_state_dict(...,
16
+ strict=False)` hid the damage. Every load here is strict, and
17
+ `assert_pretrained()` is the tensor-level gate that would have caught it.
18
+
19
+ NAME MAPPING (both levels verified against the checkpoints, not guessed):
20
+
21
+ sequence_embeddings.* -> seq_embeddings.*
22
+ sequence_layers.* -> seq_layers.*
23
+ convolution_projection -> conv_proj
24
+ convolutions.N -> conv_layers.N
25
+ input_norm (embeddings) -> LayerNorm (pre-conv normalisation)
26
+ output_norm (embeddings) -> conv_norm (post-conv residual merge)
27
+ attention_norm (layers) -> attention_layer_norm
28
+ output_norm (layers) -> output_layer_norm
29
+
30
+ TRAP: `output_norm` means DIFFERENT things at the two levels — `conv_norm` inside
31
+ the embeddings module, `output_layer_norm` inside a transformer layer. The remap is
32
+ therefore prefix-scoped. Mapping it uniformly silently corrupts the encoder.
33
+
34
+ NOT HANDLED HERE — the caller must supply data tokenized under the A1111 vocabulary
35
+ (2,114 tokens; only the 10 special tokens share IDs with Affinose's 2,090-token
36
+ vocabulary), with branch depths in [0, 203) and linkage types in [0, 10).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import math
42
+ import torch
43
+ import torch.nn as nn
44
+
45
+ A1111_VOCAB_SIZE = 2114
46
+ A1111_MAX_BRANCH_DEPTH = 203
47
+ A1111_NUM_LINKAGE_TYPES = 10
48
+ EXPECTED_ENCODER_TENSORS = 209
49
+
50
+
51
+ def remap_key(key: str) -> str | None:
52
+ """Return the Affinose-side key for an a5-lineage key, or None if not encoder."""
53
+ if key.startswith("sequence_embeddings."):
54
+ s = key[len("sequence_embeddings."):]
55
+ s = s.replace("convolution_projection", "conv_proj")
56
+ s = s.replace("convolutions.", "conv_layers.")
57
+ if s.startswith("input_norm"):
58
+ s = s.replace("input_norm", "LayerNorm", 1)
59
+ elif s.startswith("output_norm"):
60
+ s = s.replace("output_norm", "conv_norm", 1)
61
+ return "seq_embeddings." + s
62
+ if key.startswith("sequence_layers."):
63
+ s = key[len("sequence_layers."):]
64
+ s = s.replace("attention_norm", "attention_layer_norm")
65
+ s = s.replace(".output_norm", ".output_layer_norm")
66
+ return "seq_layers." + s
67
+ return None
68
+
69
+
70
+ def assert_pretrained(seq_layers, tol: float = 1e-4) -> None:
71
+ """Fail loudly if any layer is still at pristine Xavier init.
72
+
73
+ An untrained nn.Linear sits EXACTLY at its xavier_uniform bound with a zero
74
+ bias; a trained one does not. This is the check that would have caught the
75
+ original 198-of-209 wipe.
76
+ """
77
+ for i, layer in enumerate(seq_layers):
78
+ w = layer.attention.query.weight.data
79
+ bound = math.sqrt(6.0 / (w.shape[0] + w.shape[1]))
80
+ ratio = w.abs().max().item() / bound
81
+ bias_zero = bool((layer.attention.query.bias.data == 0).all())
82
+ if abs(ratio - 1.0) < tol and bias_zero:
83
+ raise RuntimeError(
84
+ f"seq_layers[{i}] is at pristine Xavier init (max/bound={ratio:.6f}, "
85
+ "bias all zero) — pretrained weights were NOT loaded or were "
86
+ "overwritten after loading."
87
+ )
88
+
89
+
90
+ def load_a1111_encoder(checkpoint_path: str, freeze_layers: int = 4):
91
+ """Return (config, seq_embeddings, seq_layers) from an A1111/WA checkpoint."""
92
+ from model.multimodal_glycan_bert_v3 import (
93
+ MultimodalGlycanBERT,
94
+ MultimodalGlycanBERTConfig,
95
+ )
96
+
97
+ ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
98
+ state = ckpt.get("model_state", ckpt.get("model_state_dict", ckpt))
99
+
100
+ src = {}
101
+ for k, v in state.items():
102
+ nk = remap_key(k)
103
+ if nk is not None:
104
+ src[nk] = v
105
+ if len(src) != EXPECTED_ENCODER_TENSORS:
106
+ raise RuntimeError(
107
+ f"expected {EXPECTED_ENCODER_TENSORS} encoder tensors, remapped {len(src)}"
108
+ )
109
+
110
+ vocab = src["seq_embeddings.token_embeddings.weight"].shape[0]
111
+ max_pos = src["seq_embeddings.position_embeddings.weight"].shape[0]
112
+ config = MultimodalGlycanBERTConfig(
113
+ seq_vocab_size=vocab, seq_max_length=max_pos, use_cnn_frontend=True
114
+ )
115
+ model = MultimodalGlycanBERT(config)
116
+
117
+ # MultimodalGlycanBERTConfig.to_seq_config() drops max_branch_depth /
118
+ # num_linkage_types, so the tables build at the v5b defaults (8, 9). Resize
119
+ # them here rather than patching the shared model file, which other lanes use.
120
+ hidden = model.seq_embeddings.branch_embeddings.weight.shape[1]
121
+ n_branch = src["seq_embeddings.branch_embeddings.weight"].shape[0]
122
+ n_link = src["seq_embeddings.linkage_embeddings.weight"].shape[0]
123
+ model.seq_embeddings.branch_embeddings = nn.Embedding(n_branch, hidden)
124
+ model.seq_embeddings.linkage_embeddings = nn.Embedding(n_link, hidden)
125
+
126
+ model.seq_embeddings.load_state_dict(
127
+ {k[len("seq_embeddings."):]: v for k, v in src.items()
128
+ if k.startswith("seq_embeddings.")}, strict=True)
129
+ model.seq_layers.load_state_dict(
130
+ {k[len("seq_layers."):]: v for k, v in src.items()
131
+ if k.startswith("seq_layers.")}, strict=True)
132
+
133
+ seq_embeddings, seq_layers = model.seq_embeddings, model.seq_layers
134
+ assert_pretrained(seq_layers) # gate BEFORE any freezing
135
+ print(f" [A1111] strict-loaded {len(src)}/{EXPECTED_ENCODER_TENSORS} encoder "
136
+ f"tensors (vocab {vocab}, branch {n_branch}, linkage {n_link})")
137
+
138
+ for p in seq_embeddings.parameters():
139
+ p.requires_grad = False
140
+ for i in range(min(freeze_layers, len(seq_layers))):
141
+ for p in seq_layers[i].parameters():
142
+ p.requires_grad = False
143
+
144
+ return config, seq_embeddings, seq_layers
releases/2026-10-02/affinose/a5_tokenizer_shim.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Import the frozen a5 BPE tokenizer without editing its sha-bound source files.
2
+
3
+ The A1111 tokenizer ships as read-only, sha256-pinned source inside the corpus
4
+ package. Its module names do not match its filenames:
5
+
6
+ review_frozen_training_a5_wurcs_bpe.py -> training.a5_wurcs_bpe
7
+ review_frozen_training_a5_topology.py -> training.a5_topology
8
+ review_frozen_sequence_tree_runtime.py -> (consumer, imports the two above)
9
+
10
+ and it imports them as `from .a5_topology import ...` / `from training.a5_topology
11
+ import ...`. Editing the files is not permitted (read-only, sha-bound), so this
12
+ registers them in sys.modules under the names the frozen code expects.
13
+
14
+ Nothing is written; the frozen bytes are loaded exactly as-is.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+ import sys
20
+ import types
21
+ from pathlib import Path
22
+
23
+ V31_ROOT = Path(__file__).resolve().parents[1] / "bertose_source"
24
+ A5_PKG = V31_ROOT / "data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1"
25
+
26
+ _FILES = {
27
+ "a5_topology": "review_frozen_training_a5_topology.py",
28
+ "a5_wurcs_bpe": "review_frozen_training_a5_wurcs_bpe.py",
29
+ "sequence_tree_runtime": "review_frozen_sequence_tree_runtime.py",
30
+ }
31
+
32
+
33
+ def _load(mod_name: str, path: Path, package: str):
34
+ spec = importlib.util.spec_from_file_location(mod_name, path)
35
+ mod = importlib.util.module_from_spec(spec)
36
+ mod.__package__ = package
37
+ sys.modules[mod_name] = mod
38
+ return mod, spec
39
+
40
+
41
+ # The frozen BPE module does:
42
+ # try: from bert_training_v4.downstream_tasks.utils.wurcs_bpe_tokenizer import ...
43
+ # except ImportError: from ..bert_training_v4... (needs a 2-level parent package)
44
+ # Putting the v3.1 root on sys.path lets the ABSOLUTE first branch resolve, so the
45
+ # awkward relative fallback never executes.
46
+
47
+
48
+
49
+ def install(pkg_dir: Path = A5_PKG):
50
+ """Register the frozen modules under `training.*` and return the namespace."""
51
+ if str(V31_ROOT) not in sys.path:
52
+ sys.path.insert(0, str(V31_ROOT))
53
+ if "training" not in sys.modules:
54
+ pkg = types.ModuleType("training")
55
+ pkg.__path__ = [str(pkg_dir)]
56
+ sys.modules["training"] = pkg
57
+ pkg = sys.modules["training"]
58
+
59
+ loaded = {}
60
+ # a5_topology has no intra-package imports, so it must be executed first.
61
+ for short in ("a5_topology", "a5_wurcs_bpe", "sequence_tree_runtime"):
62
+ fname = _FILES[short]
63
+ full = f"training.{short}"
64
+ if full in sys.modules:
65
+ loaded[short] = sys.modules[full]
66
+ continue
67
+ mod, spec = _load(full, pkg_dir / fname, "training")
68
+ sys.modules[short] = mod # satisfy bare `import a5_topology` too
69
+ spec.loader.exec_module(mod)
70
+ setattr(pkg, short, mod)
71
+ loaded[short] = mod
72
+ return loaded
releases/2026-10-02/affinose/bertint_v8.py ADDED
@@ -0,0 +1,969 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bertint V8 — Cross-Attention + Live Bertose Finetuning
3
+
4
+ Architecture:
5
+ GLYCAN: WURCS → BPE → Bertose (live, freeze layers 0-3) → [B, Lg, 768]
6
+ ↓ proj(768→512)
7
+ PROTEIN: precomputed ESM-C → [B, Lp, 960] ↓
8
+ ↓ proj(960→512) ↓
9
+ 2× CrossAttentionBlock(d=512, 8 heads, FFN=1024) ↓
10
+ ↓ ↓
11
+ SHARED mask-aware SWE(d=512, S=512, R=64) ↓
12
+ ↓ ↓
13
+ [B, 512] [B, 512]
14
+ ↓ element-wise product + sum
15
+ [B, 1024]
16
+ ↓ MLP → binding score
17
+
18
+ Key changes from V7:
19
+ - Per-residue protein embeddings (not mean-pooled) for cross-attention
20
+ - CrossAttentionBlock: glycan tokens attend to protein residues and vice versa
21
+ - SWE pooling: variable-length → fixed-length (mask-aware, differentiable)
22
+ - Product + sum interaction (from Twin Peaks) instead of concat
23
+
24
+ Key changes from V3:
25
+ - Live Bertose forward pass (not frozen precomputed embeddings)
26
+ - ESM-C 300M (960-dim, not ESM-C 600M 1152-dim)
27
+
28
+ SWE, CrossAttention, and mask handling ported from V3 (Sessions 5-10).
29
+ """
30
+
31
+ import sys
32
+ import math
33
+ from pathlib import Path
34
+ from typing import Dict, Optional, Tuple
35
+
36
+ import torch
37
+ import torch.nn as nn
38
+ import torch.nn.functional as F
39
+
40
+
41
+ # ============================================================================
42
+ # Bertose model imports (same as V7)
43
+ # ============================================================================
44
+ BERTOSE_ROOT = Path(__file__).resolve().parents[1] / "bertose_source"
45
+
46
+
47
+ def _ensure_bertose_imports():
48
+ """Add Bertose source directories to sys.path if not already present."""
49
+ roots = [
50
+ str(BERTOSE_ROOT),
51
+ str(BERTOSE_ROOT / "bert_training_v4"),
52
+ ]
53
+ for root in roots:
54
+ if root not in sys.path:
55
+ sys.path.insert(0, root)
56
+
57
+
58
+ def load_bertose_config():
59
+ """Create Bertose config matching the V5b checkpoint."""
60
+ _ensure_bertose_imports()
61
+ from model.multimodal_glycan_bert_v3 import MultimodalGlycanBERTConfig
62
+
63
+ return MultimodalGlycanBERTConfig(
64
+ seq_vocab_size=2200,
65
+ use_cnn_frontend=True,
66
+ )
67
+
68
+
69
+ def load_bertose_encoder(
70
+ checkpoint_path: str, freeze_layers: int = 4
71
+ ):
72
+ """
73
+ Load Bertose sequence encoder with pretrained weights.
74
+
75
+ Args:
76
+ checkpoint_path: Path to pretrained Bertose checkpoint.
77
+ freeze_layers: Number of transformer layers to freeze (0-indexed).
78
+
79
+ Returns:
80
+ Tuple of (bertose_config, seq_embeddings, seq_layers).
81
+ """
82
+ _ensure_bertose_imports()
83
+ from model.multimodal_glycan_bert_v3 import (
84
+ MultimodalGlycanBERT,
85
+ MultimodalGlycanBERTConfig,
86
+ )
87
+
88
+ # Load checkpoint
89
+ # A1111 PORT 2026-08-19: the a5/md20x6 lineage stores its sequence tower under
90
+ # different module names and a 2,114-token vocabulary. Delegate to the adapter,
91
+ # which renames the 209 tensors and STRICT-loads them. Verified bit-exact against
92
+ # A1111's native ExactSequenceEmbeddings/EncoderLayer (max|diff| = 0.0).
93
+ import a5_tokenizer_shim as _shim # noqa: F401 (registers frozen a5 modules)
94
+ from a1111_encoder_adapter import load_a1111_encoder as _load_a1111
95
+ _probe = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
96
+ if "model_state" in _probe and any(
97
+ k.startswith("sequence_embeddings.") for k in _probe["model_state"]
98
+ ):
99
+ del _probe
100
+ return _load_a1111(checkpoint_path, freeze_layers=freeze_layers)
101
+ del _probe
102
+
103
+ ckpt = torch.load(checkpoint_path, map_location="cpu")
104
+ state_dict = ckpt.get("model_state_dict", ckpt)
105
+
106
+ # Infer vocab size and max position embeddings from checkpoint
107
+ vocab_size = state_dict["seq_embeddings.token_embeddings.weight"].shape[0]
108
+ max_pos = state_dict["seq_embeddings.position_embeddings.weight"].shape[0]
109
+ config = MultimodalGlycanBERTConfig(
110
+ seq_vocab_size=vocab_size,
111
+ seq_max_length=max_pos,
112
+ use_cnn_frontend=True,
113
+ )
114
+
115
+ # Instantiate full model, then extract sequence encoder
116
+ model = MultimodalGlycanBERT(config)
117
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
118
+ loaded = len(state_dict) - len(unexpected)
119
+ print(f" Loaded {loaded}/{len(state_dict)} pretrained weight tensors")
120
+ print(f" ({len(missing)} missing in checkpoint, {len(unexpected)} unexpected)")
121
+
122
+ seq_embeddings = model.seq_embeddings
123
+ seq_layers = model.seq_layers
124
+
125
+ # Freeze embedding layer + first N transformer layers
126
+ for param in seq_embeddings.parameters():
127
+ param.requires_grad = False
128
+ for i in range(min(freeze_layers, len(seq_layers))):
129
+ for param in seq_layers[i].parameters():
130
+ param.requires_grad = False
131
+
132
+ trainable = sum(
133
+ p.numel() for p in seq_embeddings.parameters() if p.requires_grad
134
+ )
135
+ trainable += sum(
136
+ p.numel()
137
+ for layer in seq_layers
138
+ for p in layer.parameters()
139
+ if p.requires_grad
140
+ )
141
+ total = sum(p.numel() for p in seq_embeddings.parameters())
142
+ total += sum(
143
+ p.numel() for layer in seq_layers for p in layer.parameters()
144
+ )
145
+ print(
146
+ f" Bertose encoder: {total:,} params total, "
147
+ f"{trainable:,} trainable (frozen layers 0-{freeze_layers - 1})"
148
+ )
149
+
150
+ return config, seq_embeddings, seq_layers
151
+
152
+
153
+ # ============================================================================
154
+ # Differentiable Interpolation (from V3, Sessions 5-6)
155
+ # ============================================================================
156
+
157
+
158
+ def differentiable_interp1d(
159
+ x: torch.Tensor, y: torch.Tensor, xnew: torch.Tensor
160
+ ) -> torch.Tensor:
161
+ """
162
+ Fully differentiable 1D linear interpolation.
163
+
164
+ Gradients flow through y (values) back to theta projection and
165
+ earlier layers. The original Interp1d.backward from Twin Peaks
166
+ only returned gradients for xnew (query coords), NOT for y —
167
+ killing 83% of gradient flow. Fixed in Session 5.
168
+
169
+ Args:
170
+ x: [B, N] sorted input coordinates (detached)
171
+ y: [B, N] values at x positions (REQUIRES grad flow!)
172
+ xnew: [B, R] query coordinates
173
+
174
+ Returns:
175
+ [B, R] interpolated values
176
+ """
177
+ n_pts = x.shape[1]
178
+
179
+ # Find interpolation indices
180
+ ind = torch.searchsorted(
181
+ x.contiguous().detach(), xnew.contiguous().detach()
182
+ )
183
+ ind = ind.clamp(1, n_pts - 1)
184
+
185
+ # Gather neighbor values — preserves gradient flow through y
186
+ x_lo = torch.gather(x, 1, ind - 1)
187
+ x_hi = torch.gather(x, 1, ind)
188
+ y_lo = torch.gather(y, 1, ind - 1)
189
+ y_hi = torch.gather(y, 1, ind)
190
+
191
+ # Linear interpolation weights
192
+ denom = (x_hi - x_lo).clamp(min=1e-8)
193
+ alpha = ((xnew - x_lo) / denom).clamp(0, 1)
194
+
195
+ # Interpolated value — fully differentiable w.r.t. y_lo and y_hi
196
+ return y_lo + alpha * (y_hi - y_lo)
197
+
198
+
199
+ # ============================================================================
200
+ # SWE Pooling (from V3, mask-aware fixes from Sessions 6-8)
201
+ # ============================================================================
202
+
203
+
204
+ class SWE_Pooling(nn.Module):
205
+ """
206
+ Sliced-Wasserstein Embedding pooling.
207
+ Maps variable-length token embeddings [B, L, d_in] => [B, num_slices].
208
+
209
+ From Twin Peaks, with mask-aware sorting (Session 6) and
210
+ degenerate-sample handling (Session 8).
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ d_in: int,
216
+ num_slices: int,
217
+ num_ref_points: int,
218
+ freeze_swe: bool = False,
219
+ ):
220
+ super().__init__()
221
+ self.num_slices = num_slices
222
+ self.num_ref_points = num_ref_points
223
+
224
+ # Learnable reference distribution
225
+ ref = torch.linspace(-1, 1, num_ref_points).unsqueeze(1).repeat(
226
+ 1, num_slices
227
+ )
228
+ self.reference = nn.Parameter(ref, requires_grad=not freeze_swe)
229
+
230
+ # Projection directions (weight-normalized)
231
+ self.theta = nn.utils.weight_norm(
232
+ nn.Linear(d_in, num_slices, bias=False), dim=0
233
+ )
234
+ self.theta.weight_g.data = torch.ones_like(self.theta.weight_g.data)
235
+ self.theta.weight_g.requires_grad = False
236
+ nn.init.normal_(self.theta.weight_v)
237
+
238
+ # Weighted aggregation over reference points
239
+ self.weight = nn.Linear(num_ref_points, 1, bias=False)
240
+
241
+ if freeze_swe:
242
+ self.theta.weight_v.requires_grad = False
243
+ self.reference.requires_grad = False
244
+
245
+ def forward(
246
+ self, x: torch.Tensor, mask: Optional[torch.Tensor] = None
247
+ ) -> torch.Tensor:
248
+ """
249
+ Args:
250
+ x: [B, L, d_in] token embeddings
251
+ mask: [B, L] attention mask (1=valid, 0=padding)
252
+
253
+ Returns:
254
+ [B, num_slices] fixed-length representation
255
+ """
256
+ batch_size, seq_len, _ = x.shape
257
+ device = x.device
258
+
259
+ # Degenerate: single token → just project
260
+ if seq_len == 1:
261
+ x_slices = self.theta(x)
262
+ return x_slices.squeeze(1)
263
+
264
+ # Project onto learned directions
265
+ x_slices = self.theta(x) # [B, L, num_slices]
266
+
267
+ # MASK-AWARE SORTING: set padding → -inf so they sort to bottom
268
+ if mask is not None:
269
+ mask_exp = mask.unsqueeze(-1).expand_as(x_slices)
270
+ x_slices = x_slices.masked_fill(mask_exp == 0, float("-inf"))
271
+
272
+ x_sorted, _ = torch.sort(x_slices, dim=1)
273
+
274
+ # Strip padding from sorted array
275
+ if mask is not None:
276
+ valid_counts = mask.sum(dim=1).long()
277
+ degenerate_mask = valid_counts < 2
278
+ safe_counts = valid_counts.clamp(min=2)
279
+ max_valid = safe_counts.max().item()
280
+
281
+ # Vectorized gather: take last safe_count values
282
+ starts = (seq_len - safe_counts).unsqueeze(1)
283
+ offsets = torch.arange(max_valid, device=device).unsqueeze(0)
284
+ raw_idx = starts + offsets
285
+ gather_idx = raw_idx.clamp(max=seq_len - 1).unsqueeze(-1).expand(
286
+ batch_size, max_valid, self.num_slices
287
+ )
288
+ x_sorted = torch.gather(x_sorted, 1, gather_idx)
289
+
290
+ # Replace -inf with 0.0 for degenerate samples
291
+ x_sorted = x_sorted.masked_fill(
292
+ x_sorted == float("-inf"), 0.0
293
+ )
294
+ n_eff = max_valid
295
+ else:
296
+ degenerate_mask = None
297
+ n_eff = seq_len
298
+
299
+ # Interpolate to fixed reference grid
300
+ x_coord = (
301
+ torch.linspace(0, 1, n_eff, device=device)
302
+ .unsqueeze(0)
303
+ .expand(batch_size * self.num_slices, -1)
304
+ )
305
+ x_flat = x_sorted.permute(0, 2, 1).reshape(
306
+ batch_size * self.num_slices, n_eff
307
+ )
308
+ xnew = (
309
+ torch.linspace(0, 1, self.num_ref_points, device=device)
310
+ .unsqueeze(0)
311
+ .expand(batch_size * self.num_slices, -1)
312
+ )
313
+
314
+ y_intp = differentiable_interp1d(x_coord, x_flat, xnew)
315
+ x_interp = y_intp.view(
316
+ batch_size, self.num_slices, self.num_ref_points
317
+ ).permute(0, 2, 1)
318
+
319
+ # Compare with reference distribution
320
+ r_expanded = self.reference.expand_as(x_interp)
321
+ embeddings = (r_expanded - x_interp).permute(0, 2, 1)
322
+
323
+ # Weighted aggregation → [B, num_slices]
324
+ weighted = self.weight(embeddings).sum(dim=-1)
325
+
326
+ # Zero out degenerate samples
327
+ if degenerate_mask is not None and degenerate_mask.any():
328
+ weighted = weighted.masked_fill(
329
+ degenerate_mask.unsqueeze(-1), 0.0
330
+ )
331
+
332
+ return weighted
333
+
334
+
335
+ # ============================================================================
336
+ # Cross-Attention Block (from V3, NaN guard from Session 9)
337
+ # ============================================================================
338
+
339
+
340
+ class PredeclaredAttentivePool(nn.Module):
341
+ """Attentive pooling over post-cross-attention PROTEIN residues.
342
+
343
+ Predeclared in AFFINOSE_PREDECLARATION_ATTENTIVE_POOLING_20260827.md:
344
+
345
+ s_j = w^T p_j ; a_j = softmax_j(s_j) over valid positions ; p_bar = sum_j a_j p_j
346
+
347
+ `w` is a single Linear(dim -> 1, bias=False), ZERO-INITIALISED, and must be exempt from any
348
+ `_init_weights` pass (Xavier init would destroy the equal-at-init property). +dim parameters.
349
+
350
+ At w = 0 every logit is 0, so the masked softmax is uniform over valid positions and p_bar is
351
+ exactly the masked mean — arm A equals arm M at initialisation. Unlike a gated form, the
352
+ gradient dL/dw is non-zero there, so A can actually depart from M.
353
+ """
354
+
355
+ def __init__(self, dim: int):
356
+ super().__init__()
357
+ self.w = nn.Linear(dim, 1, bias=False)
358
+ nn.init.zeros_(self.w.weight)
359
+ self.w._skip_init = True # honoured by _init_weights guards, if any
360
+
361
+ def forward(self, x, mask):
362
+ s = self.w(x).squeeze(-1).masked_fill(mask <= 0, float("-inf"))
363
+ a = torch.softmax(s, dim=1)
364
+ a = torch.nan_to_num(a, nan=0.0).unsqueeze(-1)
365
+ return (x * a * mask.unsqueeze(-1).to(x.dtype)).sum(1)
366
+
367
+
368
+ class CrossAttentionBlock(nn.Module):
369
+ """
370
+ Bidirectional cross-attention between glycan and protein tokens.
371
+
372
+ Glycan tokens attend to protein residues (Q=glycan, KV=protein)
373
+ Protein residues attend to glycan tokens (Q=protein, KV=glycan)
374
+
375
+ Includes NaN guard for all-masked keys (Session 9 fix) and
376
+ padding-position zeroing (Session 7 fix).
377
+ """
378
+
379
+ def __init__(
380
+ self,
381
+ d_model: int,
382
+ num_heads: int,
383
+ ffn_dim: int,
384
+ dropout: float = 0.1,
385
+ ):
386
+ super().__init__()
387
+
388
+ # Glycan → Protein cross-attention
389
+ self.glycan_cross_attn = nn.MultiheadAttention(
390
+ d_model, num_heads, dropout=dropout, batch_first=True
391
+ )
392
+ self.glycan_norm1 = nn.LayerNorm(d_model)
393
+ self.glycan_ffn = nn.Sequential(
394
+ nn.Linear(d_model, ffn_dim),
395
+ nn.GELU(),
396
+ nn.Dropout(dropout),
397
+ nn.Linear(ffn_dim, d_model),
398
+ nn.Dropout(dropout),
399
+ )
400
+ self.glycan_norm2 = nn.LayerNorm(d_model)
401
+
402
+ # Protein → Glycan cross-attention
403
+ self.protein_cross_attn = nn.MultiheadAttention(
404
+ d_model, num_heads, dropout=dropout, batch_first=True
405
+ )
406
+ self.protein_norm1 = nn.LayerNorm(d_model)
407
+ self.protein_ffn = nn.Sequential(
408
+ nn.Linear(d_model, ffn_dim),
409
+ nn.GELU(),
410
+ nn.Dropout(dropout),
411
+ nn.Linear(ffn_dim, d_model),
412
+ nn.Dropout(dropout),
413
+ )
414
+ self.protein_norm2 = nn.LayerNorm(d_model)
415
+
416
+ def forward(
417
+ self,
418
+ glycan: torch.Tensor,
419
+ protein: torch.Tensor,
420
+ glycan_mask: Optional[torch.Tensor] = None,
421
+ protein_mask: Optional[torch.Tensor] = None,
422
+ return_attention: bool = False,
423
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
424
+ """
425
+ Returns updated (glycan, protein) enriched with cross-modal info.
426
+
427
+ NaN guard: nn.MultiheadAttention produces NaN when ALL keys
428
+ are masked. We replace NaN→0 so residual preserves query.
429
+ """
430
+ # Convert to key_padding_mask (True = padded)
431
+ g_key_pad = (~glycan_mask.bool()) if glycan_mask is not None else None
432
+ p_key_pad = (
433
+ (~protein_mask.bool()) if protein_mask is not None else None
434
+ )
435
+
436
+ # Glycan attends to protein
437
+ g_cross, g_attn_weights = self.glycan_cross_attn(
438
+ query=glycan, key=protein, value=protein,
439
+ key_padding_mask=p_key_pad,
440
+ need_weights=return_attention,
441
+ average_attn_weights=False,
442
+ )
443
+ g_cross = torch.nan_to_num(g_cross, nan=0.0)
444
+ glycan = self.glycan_norm1(glycan + g_cross)
445
+ glycan = self.glycan_norm2(glycan + self.glycan_ffn(glycan))
446
+ if glycan_mask is not None:
447
+ glycan = glycan * glycan_mask.unsqueeze(-1)
448
+
449
+ # Protein attends to glycan
450
+ p_cross, p_attn_weights = self.protein_cross_attn(
451
+ query=protein, key=glycan, value=glycan,
452
+ key_padding_mask=g_key_pad,
453
+ need_weights=return_attention,
454
+ average_attn_weights=False,
455
+ )
456
+ p_cross = torch.nan_to_num(p_cross, nan=0.0)
457
+ protein = self.protein_norm1(protein + p_cross)
458
+ protein = self.protein_norm2(protein + self.protein_ffn(protein))
459
+ if protein_mask is not None:
460
+ protein = protein * protein_mask.unsqueeze(-1)
461
+
462
+ if return_attention:
463
+ attn_dict = {
464
+ "glycan_to_protein": g_attn_weights,
465
+ "protein_to_glycan": p_attn_weights,
466
+ }
467
+ return glycan, protein, attn_dict
468
+
469
+ return glycan, protein
470
+
471
+
472
+ # ============================================================================
473
+ # Bertint V8 Model
474
+ # ============================================================================
475
+
476
+
477
+ class BertintV8(nn.Module):
478
+ """
479
+ Glycan-protein interaction predictor with cross-attention.
480
+
481
+ Glycan: Live Bertose (partially frozen) → per-token [B, Lg, 768]
482
+ Protein: Precomputed ESM-C per-residue [B, Lp, 960]
483
+ Cross-attention: 2 bidirectional layers in shared 512-dim space
484
+ SWE: Variable-length → fixed [B, 512] for each side
485
+ Interaction: product + sum → MLP → scalar
486
+ """
487
+
488
+ def __init__(
489
+ self,
490
+ seq_embeddings: nn.Module,
491
+ seq_layers: nn.ModuleList,
492
+ glycan_dim: int = 768,
493
+ protein_dim: int = 960,
494
+ shared_dim: int = 512,
495
+ num_cross_layers: int = 2,
496
+ num_heads: int = 8,
497
+ ffn_dim: int = 1024,
498
+ swe_slices: int = 512,
499
+ swe_ref_points: int = 64,
500
+ head_hidden: int = 256,
501
+ dropout: float = 0.1,
502
+ separate_swe: bool = False,
503
+ pooling_mode: str = "swe",
504
+ protein_attentive_pool: bool = False,
505
+ interaction_mode: str = "product_sum",
506
+ use_cross_attention: bool = True,
507
+ glycan_size_channel: bool = False,
508
+ motif_additive_dim: int = 0,
509
+ protein_mode: str = "real",
510
+ protein_tower: bool = True,
511
+ protein_binding_channel: bool = False,
512
+ ):
513
+ """
514
+ Args:
515
+ seq_embeddings: Pretrained Bertose embedding layer.
516
+ seq_layers: Pretrained Bertose transformer layers.
517
+ glycan_dim: Bertose output dimension (768).
518
+ protein_dim: ESM-C per-residue dimension (960).
519
+ shared_dim: Shared space for cross-attention (512).
520
+ num_cross_layers: Number of cross-attention blocks.
521
+ num_heads: Attention heads per block.
522
+ ffn_dim: FFN hidden dim in cross-attention.
523
+ swe_slices: Number of SWE projection directions.
524
+ swe_ref_points: Number of SWE reference distribution points.
525
+ head_hidden: MLP head hidden dimension.
526
+ dropout: Dropout rate.
527
+ separate_swe: If True, use independent SWE modules.
528
+ pooling_mode: 'swe', 'mean', or 'joint_swe'.
529
+ interaction_mode: 'product_sum' or 'concat'.
530
+ use_cross_attention: If False, skip cross-attention.
531
+ """
532
+ super().__init__()
533
+
534
+ self.separate_swe = separate_swe
535
+ self.pooling_mode = pooling_mode
536
+ self.protein_attentive_pool = protein_attentive_pool
537
+ self.interaction_mode = interaction_mode
538
+ self.use_cross_attention = use_cross_attention
539
+ self.glycan_size_channel = glycan_size_channel
540
+ # 'shuffle' is REMOVED, not deprecated: it permuted inside forward(), which also
541
+ # runs under eval(), so it corrupted validation and test. P is a materialised
542
+ # dataset now (fit-rows-only donor map, hash-bound), so no model path is needed.
543
+ if protein_mode == "shuffle":
544
+ raise ValueError(
545
+ "protein_mode='shuffle' is withdrawn: it permuted inside forward(), which "
546
+ "also runs under eval(), so it permuted validation and test. Use the "
547
+ "materialised P arm (affinose_v2_binding_P_seed*.csv).")
548
+ assert protein_mode in ("real", "constant")
549
+ self.protein_mode = protein_mode
550
+ # G: do not CONSTRUCT the protein pathway at all.
551
+ self.protein_tower = protein_tower
552
+ # R2: the last input channel is a per-residue binding probability, not a
553
+ # feature. It rides along so that padding/masking/collate stay unchanged.
554
+ self.protein_binding_channel = protein_binding_channel
555
+ if protein_binding_channel:
556
+ assert protein_tower, "binding-weighted pooling needs the protein tower"
557
+ protein_dim = protein_dim - 1 # the channel is a weight, not a feature
558
+ if not protein_tower:
559
+ assert protein_mode == "real", (
560
+ "protein_tower=False already removes the protein pathway; combining it "
561
+ "with a protein_mode ablation is redundant and ambiguous")
562
+ use_cross_attention = False
563
+
564
+ print(f" Architecture config:")
565
+ print(f" cross_attention={use_cross_attention}")
566
+ print(f" pooling_mode={pooling_mode}")
567
+ print(f" interaction_mode={interaction_mode}")
568
+
569
+ # === Bertose sequence encoder (partially frozen) ===
570
+ self.seq_embeddings = seq_embeddings
571
+ self.seq_layers = seq_layers
572
+
573
+ # === Projection to shared space ===
574
+ self.glycan_proj = nn.Sequential(
575
+ nn.Linear(glycan_dim, shared_dim),
576
+ nn.LayerNorm(shared_dim),
577
+ )
578
+ if protein_tower:
579
+ self.protein_proj = nn.Sequential(
580
+ nn.Linear(protein_dim, shared_dim),
581
+ nn.LayerNorm(shared_dim),
582
+ )
583
+
584
+ # === Cross-attention stack (optional) ===
585
+ if use_cross_attention:
586
+ self.cross_attention = nn.ModuleList([
587
+ CrossAttentionBlock(
588
+ d_model=shared_dim,
589
+ num_heads=num_heads,
590
+ ffn_dim=ffn_dim,
591
+ dropout=dropout,
592
+ )
593
+ for _ in range(num_cross_layers)
594
+ ])
595
+ else:
596
+ self.cross_attention = nn.ModuleList()
597
+
598
+ # === Pooling ===
599
+ if pooling_mode == "swe":
600
+ if separate_swe:
601
+ self.swe_glycan = SWE_Pooling(
602
+ d_in=shared_dim, num_slices=swe_slices,
603
+ num_ref_points=swe_ref_points,
604
+ )
605
+ if protein_tower:
606
+ self.swe_protein = SWE_Pooling(
607
+ d_in=shared_dim, num_slices=swe_slices,
608
+ num_ref_points=swe_ref_points,
609
+ )
610
+ pool_out_dim = swe_slices
611
+ else:
612
+ self.swe = SWE_Pooling(
613
+ d_in=shared_dim, num_slices=swe_slices,
614
+ num_ref_points=swe_ref_points,
615
+ )
616
+ pool_out_dim = swe_slices
617
+ elif pooling_mode == "mean":
618
+ pool_out_dim = shared_dim
619
+ elif pooling_mode == "joint_swe":
620
+ self.swe_joint = SWE_Pooling(
621
+ d_in=shared_dim, num_slices=swe_slices,
622
+ num_ref_points=swe_ref_points,
623
+ )
624
+ pool_out_dim = swe_slices
625
+
626
+ # === Regression head ===
627
+ if pooling_mode == "joint_swe" or not protein_tower:
628
+ # glycan-only: the head sees ONE pooled vector, not a concatenated pair
629
+ head_input_dim = pool_out_dim
630
+ elif protein_binding_channel:
631
+ # glycan + global protein + binding-weighted protein
632
+ head_input_dim = 3 * pool_out_dim
633
+ else:
634
+ head_input_dim = 2 * pool_out_dim
635
+ # Restore the glycan-magnitude signal that masked mean pooling divides out.
636
+ if glycan_size_channel:
637
+ head_input_dim += 1
638
+
639
+ self.head = nn.Sequential(
640
+ nn.Linear(head_input_dim, head_hidden),
641
+ nn.GELU(),
642
+ nn.Dropout(dropout),
643
+ nn.Linear(head_hidden, head_hidden),
644
+ nn.GELU(),
645
+ nn.Dropout(dropout),
646
+ nn.Linear(head_hidden, 1),
647
+ )
648
+
649
+ # Initialize (skip SWE weight-normed params)
650
+ # ENCODER-INIT FIX 2026-08-17: nn.Module.apply() recurses into the
651
+ # pretrained BERTose encoder assigned above and _init_weights resets
652
+ # every nn.Linear / nn.LayerNorm inside it. Snapshot the encoder,
653
+ # let apply() run unchanged so every OTHER module receives the exact
654
+ # same init (and consumes the identical RNG stream), then restore.
655
+ _encfix_emb = {k: v.detach().clone() for k, v in self.seq_embeddings.state_dict().items()}
656
+ _encfix_lay = {k: v.detach().clone() for k, v in self.seq_layers.state_dict().items()}
657
+ self.apply(self._init_weights)
658
+ self.seq_embeddings.load_state_dict(_encfix_emb)
659
+ self.seq_layers.load_state_dict(_encfix_lay)
660
+
661
+ # ── ADDITIVE MOTIF TERM — CONSTRUCTED LAST, ON PURPOSE ──────────────────────────
662
+ # score += w . motif_features. LAST so it draws from the RNG stream only after every
663
+ # shared module, otherwise this arm is confounded with a different initialisation.
664
+ # Zero-init: identical to the reference at init, but dw != 0 so it can depart.
665
+ self.motif_additive_dim = int(motif_additive_dim)
666
+ if self.motif_additive_dim > 0:
667
+ self.motif_head = nn.Linear(self.motif_additive_dim, 1, bias=False)
668
+ nn.init.zeros_(self.motif_head.weight)
669
+ print(f" [ENCFIX] restored {len(_encfix_emb)}+{len(_encfix_lay)} pretrained encoder tensors after init")
670
+ self._count_params()
671
+
672
+ # Constructed LAST on purpose: every shared submodule above draws from the RNG
673
+ # stream first, so arm A and arm M receive IDENTICAL shared weights under the
674
+ # same seed. Only this module's draws come afterwards. Without this, the extra
675
+ # parameters shift the stream and A - M confounds capacity with a different init.
676
+ if protein_attentive_pool:
677
+ self.protein_pool = PredeclaredAttentivePool(shared_dim)
678
+
679
+ def _init_weights(self, module: nn.Module) -> None:
680
+ """Xavier init for Linear, skip weight-normed SWE modules."""
681
+ if isinstance(module, nn.Linear):
682
+ if hasattr(module, "weight_v"):
683
+ return # Preserve SWE initialization
684
+ nn.init.xavier_uniform_(module.weight)
685
+ if module.bias is not None:
686
+ nn.init.zeros_(module.bias)
687
+ elif isinstance(module, nn.LayerNorm):
688
+ nn.init.ones_(module.weight)
689
+ nn.init.zeros_(module.bias)
690
+
691
+ def _count_params(self) -> None:
692
+ """Log parameter counts."""
693
+ total = sum(p.numel() for p in self.parameters())
694
+ trainable = sum(
695
+ p.numel() for p in self.parameters() if p.requires_grad
696
+ )
697
+ print(f"BertintV8: {total:,} total, {trainable:,} trainable")
698
+
699
+ def _masked_mean_pool(self, x, mask):
700
+ """Masked mean pooling: average valid tokens only."""
701
+ mask_expanded = mask.unsqueeze(-1)
702
+ x_masked = x * mask_expanded
703
+ summed = x_masked.sum(dim=1)
704
+ counts = mask.sum(dim=1, keepdim=True).clamp(min=1)
705
+ return summed / counts
706
+
707
+ def forward(
708
+ self,
709
+ token_ids: torch.Tensor,
710
+ attention_mask: torch.Tensor,
711
+ branch_depths: torch.Tensor,
712
+ linkage_types: torch.Tensor,
713
+ protein_emb: torch.Tensor,
714
+ protein_mask: torch.Tensor,
715
+ motif_features: Optional[torch.Tensor] = None,
716
+ log_conc: Optional[torch.Tensor] = None,
717
+ has_conc: Optional[torch.Tensor] = None,
718
+ return_attention: bool = False,
719
+ ) -> torch.Tensor:
720
+ """
721
+ Forward pass with cross-attention.
722
+
723
+ Args:
724
+ token_ids: [B, Lg] BPE token IDs.
725
+ attention_mask: [B, Lg] glycan attention mask (1=valid, 0=pad).
726
+ branch_depths: [B, Lg] branch depth per token.
727
+ linkage_types: [B, Lg] linkage type per token.
728
+ protein_emb: [B, Lp, protein_dim] per-residue ESM-C embeddings.
729
+ protein_mask: [B, Lp] protein attention mask (1=valid, 0=pad).
730
+
731
+ Returns:
732
+ [B] binding score predictions.
733
+ """
734
+ # === 0. Protein-channel ablation (G / P arms) ===
735
+ # 'constant': zero the protein input, so protein_pooled is identical for every
736
+ # example -> the model is effectively GLYCAN-ONLY (Codex's G).
737
+ # 'shuffle': permute whole protein tensors + masks together across the batch,
738
+ # destroying protein-glycan pairing while preserving the protein marginal
739
+ # (an approximation of a label-blind pairing null, Codex's P).
740
+ if self.protein_mode == "constant":
741
+ protein_emb = torch.zeros_like(protein_emb)
742
+ protein_mask = torch.ones_like(protein_mask)
743
+ elif self.protein_mode == "shuffle":
744
+ perm = torch.randperm(protein_emb.shape[0], device=protein_emb.device)
745
+ protein_emb = protein_emb[perm]
746
+ protein_mask = protein_mask[perm]
747
+
748
+ # === 1. Bertose forward: per-token embeddings ===
749
+ x = self.seq_embeddings(token_ids, branch_depths, linkage_types)
750
+ for layer in self.seq_layers:
751
+ x = layer(x, attention_mask)
752
+ # x: [B, Lg, 768] — all tokens (not just CLS!)
753
+
754
+ # Glycan mask: use the attention_mask from BPE tokenizer
755
+ glycan_mask = attention_mask # [B, Lg], 1=valid, 0=pad
756
+
757
+ # === G arm: glycan-only. The protein tensor and mask are never read. ===
758
+ if not self.protein_tower:
759
+ x = self.seq_embeddings(token_ids, branch_depths, linkage_types)
760
+ for layer in self.seq_layers:
761
+ x = layer(x, attention_mask)
762
+ glycan = self.glycan_proj(x) * attention_mask.unsqueeze(-1)
763
+ if self.pooling_mode == "mean":
764
+ pooled = self._masked_mean_pool(glycan, attention_mask)
765
+ else:
766
+ pooled = self.swe(glycan, attention_mask)
767
+ if self.glycan_size_channel:
768
+ n_tok = attention_mask.sum(dim=1, keepdim=True).to(pooled.dtype)
769
+ pooled = torch.cat([pooled, torch.log1p(n_tok)], dim=-1)
770
+ return self.head(pooled).squeeze(-1)
771
+
772
+ # R2: peel the binding-probability channel off the protein input. Done BEFORE
773
+ # projection so the projection never sees the weight as a feature.
774
+ binding_w = None
775
+ if self.protein_binding_channel:
776
+ binding_w = protein_emb[..., -1] # [B, Lp]
777
+ protein_emb = protein_emb[..., :-1] # [B, Lp, 960]
778
+
779
+ # === 2. Project to shared space ===
780
+ glycan = self.glycan_proj(x) # [B, Lg, 512]
781
+ protein = self.protein_proj(protein_emb) # [B, Lp, 512]
782
+
783
+ # Zero padding positions after projection (Session 7 fix:
784
+ # LayerNorm bias produces non-trivial values at padding)
785
+ glycan = glycan * glycan_mask.unsqueeze(-1)
786
+ protein = protein * protein_mask.unsqueeze(-1)
787
+
788
+ # === 3. Cross-attention (optional) ===
789
+ all_attention_maps = []
790
+ if self.use_cross_attention:
791
+ for cross_layer in self.cross_attention:
792
+ if return_attention:
793
+ glycan, protein, attn_dict = cross_layer(
794
+ glycan, protein, glycan_mask, protein_mask,
795
+ return_attention=True,
796
+ )
797
+ all_attention_maps.append(attn_dict)
798
+ else:
799
+ glycan, protein = cross_layer(
800
+ glycan, protein, glycan_mask, protein_mask
801
+ )
802
+
803
+ # === 4. Pooling ===
804
+ if self.pooling_mode == "joint_swe":
805
+ joint_tokens = torch.cat([glycan, protein], dim=1)
806
+ joint_mask = torch.cat([glycan_mask, protein_mask], dim=1)
807
+ pooled = self.swe_joint(joint_tokens, joint_mask)
808
+ return self.head(pooled).squeeze(-1)
809
+
810
+ if self.pooling_mode == "swe":
811
+ if self.separate_swe:
812
+ glycan_pooled = self.swe_glycan(glycan, glycan_mask)
813
+ protein_pooled = self.swe_protein(protein, protein_mask)
814
+ else:
815
+ glycan_pooled = self.swe(glycan, glycan_mask)
816
+ protein_pooled = self.swe(protein, protein_mask)
817
+ elif self.pooling_mode == "mean":
818
+ glycan_pooled = self._masked_mean_pool(glycan, glycan_mask)
819
+ if self.protein_attentive_pool:
820
+ # arm A: zero-initialised attentive protein pooling. Identical to the mean-pool
821
+ # arm M at initialisation; A - M therefore measures only the trained use of the
822
+ # added capacity.
823
+ protein_pooled = self.protein_pool(protein, protein_mask)
824
+ else:
825
+ protein_pooled = self._masked_mean_pool(protein, protein_mask)
826
+ if binding_w is not None:
827
+ # w~ = w / sum(w) over VALID positions only; a protein with no
828
+ # predicted site falls back to the mean rather than dividing by zero
829
+ w = binding_w * protein_mask
830
+ denom = w.sum(dim=1, keepdim=True)
831
+ w = torch.where(denom > 1e-8, w / denom.clamp(min=1e-8),
832
+ protein_mask / protein_mask.sum(1, keepdim=True).clamp(min=1))
833
+ protein_binding = (protein * w.unsqueeze(-1)).sum(1)
834
+ protein_pooled = torch.cat([protein_pooled, protein_binding], dim=-1)
835
+
836
+ # === 5. Interaction ===
837
+ if self.interaction_mode == "product_sum":
838
+ interaction = torch.cat([
839
+ glycan_pooled * protein_pooled,
840
+ glycan_pooled + protein_pooled,
841
+ ], dim=-1)
842
+ elif self.interaction_mode == "concat":
843
+ interaction = torch.cat([
844
+ glycan_pooled, protein_pooled,
845
+ ], dim=-1)
846
+
847
+ # Masked mean pooling is invariant to token count, so glycan size is absent
848
+ # from the pooled vector by construction. Append it back as one scalar.
849
+ if self.glycan_size_channel:
850
+ n_tok = glycan_mask.sum(dim=1, keepdim=True).to(interaction.dtype)
851
+ interaction = torch.cat([interaction, torch.log1p(n_tok)], dim=-1)
852
+
853
+ # === 6. Predict binding score ===
854
+ out = self.head(interaction).squeeze(-1)
855
+
856
+ # Additive motif main effect -- the functional form of the ridge that beats us on
857
+ # doubly-cold (0.2290 vs 0.1444), learned jointly instead of bolted on.
858
+ if getattr(self, "motif_additive_dim", 0) > 0:
859
+ # ⛔ FAIL CLOSED. A missed call site must crash here, never train without motifs.
860
+ if motif_features is None:
861
+ raise ValueError("motif_additive_dim > 0 but motif_features was not supplied -- "
862
+ "refusing to run a motif arm without motif inputs")
863
+ out = out + self.motif_head(motif_features.to(out.dtype)).squeeze(-1)
864
+ if return_attention and all_attention_maps:
865
+ return out, all_attention_maps
866
+ return out
867
+
868
+
869
+ # ============================================================================
870
+ # Loss (same as V7 — simple MSE)
871
+ # ============================================================================
872
+
873
+
874
+ class BertintV8Loss(nn.Module):
875
+ """MSE loss for regression."""
876
+
877
+ def __init__(self):
878
+ super().__init__()
879
+ self.mse = nn.MSELoss()
880
+
881
+ def forward(
882
+ self, pred: torch.Tensor, target: torch.Tensor
883
+ ) -> torch.Tensor:
884
+ """Compute MSE loss."""
885
+ return self.mse(pred, target)
886
+
887
+
888
+ # ============================================================================
889
+ # Sanity check
890
+ # ============================================================================
891
+ if __name__ == "__main__":
892
+ print("=" * 60)
893
+ print("BertintV8 Architecture Sanity Check")
894
+ print("=" * 60)
895
+
896
+ # Mock Bertose encoder (for testing without cluster)
897
+ class MockEmbeddings(nn.Module):
898
+ """Mock Bertose embeddings for local testing."""
899
+
900
+ def __init__(self, dim: int = 768):
901
+ super().__init__()
902
+ self.proj = nn.Linear(64, dim)
903
+
904
+ def forward(self, token_ids, branch_depths, linkage_types):
905
+ """Return random embeddings."""
906
+ batch_size, seq_len = token_ids.shape
907
+ return torch.randn(batch_size, seq_len, 768)
908
+
909
+ class MockLayer(nn.Module):
910
+ """Mock transformer layer."""
911
+
912
+ def forward(self, x, mask):
913
+ """Identity."""
914
+ return x
915
+
916
+ seq_emb = MockEmbeddings()
917
+ seq_layers = nn.ModuleList([MockLayer() for _ in range(12)])
918
+
919
+ model = BertintV8(
920
+ seq_embeddings=seq_emb,
921
+ seq_layers=seq_layers,
922
+ glycan_dim=768,
923
+ protein_dim=960,
924
+ )
925
+
926
+ # Simulate batch
927
+ batch_size = 4
928
+ lg = 37 # Glycan: 37 BPE tokens
929
+ lp = 150 # Protein: 150 residues
930
+
931
+ token_ids = torch.randint(0, 100, (batch_size, lg))
932
+ attention_mask = torch.ones(batch_size, lg).float()
933
+ branch_depths = torch.zeros(batch_size, lg, dtype=torch.long)
934
+ linkage_types = torch.zeros(batch_size, lg, dtype=torch.long)
935
+ protein_emb = torch.randn(batch_size, lp, 960)
936
+ protein_mask = torch.ones(batch_size, lp).float()
937
+
938
+ out = model(
939
+ token_ids=token_ids,
940
+ attention_mask=attention_mask,
941
+ branch_depths=branch_depths,
942
+ linkage_types=linkage_types,
943
+ protein_emb=protein_emb,
944
+ protein_mask=protein_mask,
945
+ )
946
+
947
+ print(f"\nInput shapes:")
948
+ print(f" Glycan tokens: {token_ids.shape}")
949
+ print(f" Protein emb: {protein_emb.shape}")
950
+ print(f"\nOutput shape: {out.shape} — values: {out.detach()}")
951
+
952
+ # Test loss
953
+ loss_fn = BertintV8Loss()
954
+ target = torch.rand(batch_size)
955
+ loss = loss_fn(out, target)
956
+ print(f"\nLoss: {loss.item():.6f}")
957
+
958
+ # Test backward
959
+ loss.backward()
960
+ grad_count = sum(
961
+ 1 for p in model.parameters() if p.grad is not None and p.requires_grad
962
+ )
963
+ total_trainable = sum(
964
+ 1 for p in model.parameters() if p.requires_grad
965
+ )
966
+ print(f"Gradients: {grad_count}/{total_trainable} trainable params")
967
+
968
+ print(f"\n✅ V8 sanity check passed!")
969
+ """Bertint V8 model — cross-attention + live Bertose finetuning."""
releases/2026-10-02/affinose/dataset_v8.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bertint V8 Dataset — Per-Residue Protein Embeddings for Cross-Attention
3
+
4
+ Like V7 but keeps per-residue ESM-C embeddings [L, D] instead of
5
+ mean-pooling to [D]. This enables token-level cross-attention between
6
+ glycan tokens and protein residues.
7
+
8
+ Changes from V7:
9
+ - protein_emb: [Lp, 960] per-residue (not [960] mean-pooled)
10
+ - collate_fn: pads protein sequences to max length in batch
11
+ - Returns protein_mask for cross-attention padding
12
+ """
13
+
14
+ import json
15
+ import gzip
16
+ import csv
17
+ import logging
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional, Tuple
20
+
21
+ import h5py
22
+ import numpy as np
23
+ import pandas as pd
24
+ import torch
25
+ from torch.utils.data import Dataset
26
+ from torch.nn.utils.rnn import pad_sequence
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ # ============================================================================
32
+ # BPE Tokenizer Loader (same as V7)
33
+ # ============================================================================
34
+
35
+ def load_bpe_tokenizer(vocab_path: str):
36
+ """
37
+ Load the Bertose BPE tokenizer directly from source.
38
+
39
+ Bypasses downstream_tasks package imports. Adds utils
40
+ directory to sys.path and imports WURCSBPETokenizer directly.
41
+
42
+ Args:
43
+ vocab_path: Path to BPE vocabulary JSON file.
44
+
45
+ Returns:
46
+ WURCSBPETokenizer instance.
47
+ """
48
+ import sys
49
+
50
+ utils_dir = str(Path(__file__).resolve().parents[1] / "bertose_source" /
51
+ "bert_training_v4/downstream_tasks/utils")
52
+ if utils_dir not in sys.path:
53
+ sys.path.insert(0, utils_dir)
54
+
55
+ # A1111 PORT 2026-08-19: use the frozen a5 tokenizer (2,114-token vocabulary)
56
+ # instead of the submitted-era 2,090-token one. Only the 10 special tokens
57
+ # share IDs between the two, so features MUST be regenerated, not reused.
58
+ # Verified: reproduces A1111's own review_token_ids.npy / review_branch_depths.npy
59
+ # byte-for-byte on 2,762/2,762 glycans with tree_channels_valid=True.
60
+ import a5_tokenizer_shim
61
+ a5_tokenizer_shim.install()
62
+ # V3 FIX 2026-08-24: the BASE tokenizer's linkage mapping disagrees with the
63
+ # one the encoder was pretrained under -- (1,2)->0/unknown->8 instead of
64
+ # LINKAGE_NONE=0/(1,2)->1/unknown->9. linkage_embeddings is (10,768), learned
65
+ # under V3, so base IDs index the wrong rows. Not fixable by a +1 remap.
66
+ from training.sequence_tree_runtime import ReviewA5WURCSBPETokenizerV3
67
+ return ReviewA5WURCSBPETokenizerV3(vocab_path=vocab_path)
68
+
69
+
70
+ # ============================================================================
71
+ # Dataset
72
+ # ============================================================================
73
+
74
+ class BertintV8Dataset(Dataset):
75
+ """
76
+ Dataset for glycan-protein interaction with cross-attention support.
77
+
78
+ Returns:
79
+ - BPE-tokenized glycan sequences for live Bertose forward pass
80
+ - Per-residue ESM-C protein embeddings [Lp, D] (NOT mean-pooled)
81
+ - Masks for both sides (for cross-attention padding)
82
+
83
+ Args:
84
+ csv_path: Path to binding data CSV.
85
+ split_path: Path to glycan-cold splits JSON.
86
+ split: One of 'train', 'val', 'test'.
87
+ protein_emb_path: Path to ESM-C embeddings HDF5.
88
+ vocab_path: Path to BPE vocabulary JSON.
89
+ max_glycan_length: Maximum glycan sequence length.
90
+ max_protein_length: Maximum protein residues (truncate longer).
91
+ target_col: Column name for regression target.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ csv_path: str,
97
+ split_path: str,
98
+ split: str,
99
+ protein_emb_path: str,
100
+ vocab_path: str,
101
+ max_glycan_length: int = 256,
102
+ max_protein_length: int = 1024,
103
+ target_col: str = "target_rank",
104
+ roles_path: str = None,
105
+ stratum: str = None,
106
+ features_dir: str = None,
107
+ expected_rows: int = None,
108
+ ):
109
+ logger.info(f"Loading {split} dataset from {csv_path}")
110
+
111
+ # ------------------------------------------------------------------
112
+ # CONTRACT r2 (2026-08-24): partition comes from the A5 role TSV's
113
+ # evaluation_stratum, not from splits_glycan_cold.json.
114
+ #
115
+ # H5-SAFE JOIN. Four CarboGrove protein_id values carry a TRAILING
116
+ # SPACE in the CSV (Laburnum/Lima Bean/Aspergillus/Limulus, 4,247 rows)
117
+ # while the role TSV holds the stripped form. Stripping protein_id in
118
+ # place would fix the stratum join and then silently lose the SAME
119
+ # 4,247 rows at the H5 lookup, whose keys also keep the space. So we
120
+ # join on a separate canonical key and never mutate protein_id.
121
+ # ------------------------------------------------------------------
122
+ if roles_path is None or stratum is None:
123
+ raise ValueError("roles_path and stratum are required under contract r2")
124
+
125
+ df = pd.read_csv(csv_path, float_precision="round_trip")
126
+ n_csv = len(df)
127
+ df["_join_protein"] = df["protein_id"].astype(str).str.strip()
128
+ df["_join_glycan"] = df["glycan_wurcs"].astype(str).str.strip()
129
+
130
+ roles = pd.read_csv(roles_path, sep="\t", compression="gzip",
131
+ usecols=["protein_id", "glycan_wurcs", "evaluation_stratum"])
132
+ roles["_join_protein"] = roles["protein_id"].astype(str).str.strip()
133
+ roles["_join_glycan"] = roles["glycan_wurcs"].astype(str).str.strip()
134
+ roles = roles[["_join_protein", "_join_glycan", "evaluation_stratum"]]
135
+ if roles.duplicated(["_join_protein", "_join_glycan"]).any():
136
+ raise RuntimeError("role TSV has duplicate (protein, glycan) pairs")
137
+
138
+ merged = df.merge(roles, on=["_join_protein", "_join_glycan"],
139
+ how="left", validate="many_to_one")
140
+ if len(merged) != n_csv:
141
+ raise RuntimeError(f"stratum join changed row count {n_csv} -> {len(merged)}")
142
+ unmatched = int(merged["evaluation_stratum"].isna().sum())
143
+ if unmatched:
144
+ raise RuntimeError(f"{unmatched} CSV rows matched no evaluation_stratum")
145
+
146
+ df = merged[merged["evaluation_stratum"] == stratum].copy()
147
+ df = df.dropna(subset=[target_col])
148
+ logger.info(f" stratum {stratum}: {len(df):,} records "
149
+ f"(from {n_csv:,} CSV rows, 0 unmatched)")
150
+
151
+ self.target_col = target_col
152
+ self.max_protein_length = max_protein_length
153
+
154
+ # ------------------------------------------------------------------
155
+ # CONTRACT r2: glycan channels come from the SEALED merged feature
156
+ # table. NO TOKENIZER IS IN THE PRODUCTION PATH. Tier A (2,877) are
157
+ # the official WURCSArray-derived arrays; tier B (1,048) come from the
158
+ # sealed WURCSArray extension. Anything the WURCSArray authority marks
159
+ # tree_channels_valid=False FAILS CLOSED and is excluded.
160
+ # ------------------------------------------------------------------
161
+ self.max_glycan_length = max_glycan_length
162
+ feat = Path(features_dir)
163
+ chans = ("token_ids", "residue_ids", "branch_depths",
164
+ "linkage_types", "attention_mask")
165
+ arrays = {c: np.load(feat / f"review_{c}.npy") for c in chans}
166
+ valid = np.load(feat / "review_tree_channels_valid.npy")
167
+ if arrays["token_ids"].shape[1] != max_glycan_length:
168
+ raise RuntimeError(
169
+ f"feature width {arrays['token_ids'].shape[1]} != "
170
+ f"max_glycan_length {max_glycan_length}")
171
+ frow, fvalid = {}, {}
172
+ with gzip.open(feat / "review_affinose_glycan_index.tsv.gz",
173
+ "rt", newline="") as fh:
174
+ for r in csv.DictReader(fh, delimiter="\t"):
175
+ w = r["canonical_wurcs"].strip()
176
+ frow[w] = int(r["feature_row"])
177
+ fvalid[w] = (r["tree_channels_valid"] == "True")
178
+ logger.info(f" sealed feature table: {len(frow)} glycans, "
179
+ f"{int(valid.sum())} valid / {int((~valid).sum())} fail-closed")
180
+
181
+ need = set(df["_join_glycan"].unique())
182
+ unknown = sorted(w for w in need if w not in frow)
183
+ if unknown:
184
+ raise RuntimeError(
185
+ f"{len(unknown)} glycans absent from the sealed feature table, "
186
+ f"e.g. {unknown[0][:60]}")
187
+
188
+ excluded = sorted(w for w in need if not fvalid[w])
189
+ if excluded:
190
+ before = len(df)
191
+ df = df[~df["_join_glycan"].isin(set(excluded))].copy()
192
+ logger.info(f" FAIL-CLOSED: dropped {len(excluded)} glycans "
193
+ f"(tree_channels_valid=False), {before - len(df):,} rows")
194
+ self.excluded_glycans = excluded
195
+
196
+ # MOTIF CHANNEL: 165 raw motif counts per glycan, row-aligned to the sealed
197
+ # feature index; the pinned float32 array contains counts 0-13. Do not binarize.
198
+ _motif = np.load(feat / "review_motif_features.npy")
199
+ if _motif.shape[0] != arrays["token_ids"].shape[0]:
200
+ raise RuntimeError("motif array rows %d != feature rows %d"
201
+ % (_motif.shape[0], arrays["token_ids"].shape[0]))
202
+
203
+ self.tokenized_cache = {}
204
+ for w in df["_join_glycan"].unique():
205
+ i = frow[w]
206
+ self.tokenized_cache[w] = {
207
+ "token_ids": torch.tensor(arrays["token_ids"][i], dtype=torch.long),
208
+ "attention_mask": torch.tensor(arrays["attention_mask"][i], dtype=torch.long),
209
+ "branch_depths": torch.tensor(arrays["branch_depths"][i], dtype=torch.long),
210
+ "linkage_types": torch.tensor(arrays["linkage_types"][i], dtype=torch.long),
211
+ "motif_features": torch.tensor(_motif[i], dtype=torch.float),
212
+ }
213
+ logger.info(f" {len(self.tokenized_cache)} glycans served from sealed arrays")
214
+
215
+ self.records = df.reset_index(drop=True)
216
+
217
+ # Load protein embeddings — KEEP PER-RESIDUE (not mean-pooled!)
218
+ logger.info(f" Loading per-residue protein embeddings...")
219
+ self.protein_embs: Dict[str, torch.Tensor] = {}
220
+ with h5py.File(protein_emb_path, "r") as f:
221
+ for key in f.keys():
222
+ emb = torch.from_numpy(f[key][:]).float()
223
+ # emb: [L, D] per-residue
224
+ if emb.dim() == 1:
225
+ # Edge case: single-residue protein
226
+ emb = emb.unsqueeze(0)
227
+ # BOS/EOS FIX 2026-08-19: ESM-C emits L+2 vectors per protein
228
+ # (verified: every entry in esmc_embeddings.h5 is exactly
229
+ # len(sequence)+2). The original code truncated but never stripped
230
+ # them, so position 0 was the BOS token fed to the model as
231
+ # residue 1 and the whole protein was shifted by one.
232
+ if emb.shape[0] >= 3:
233
+ emb = emb[1:-1]
234
+ # Truncate very long proteins
235
+ if emb.shape[0] > max_protein_length:
236
+ emb = emb[:max_protein_length]
237
+ protein_id = key.replace("|", "/")
238
+ self.protein_embs[protein_id] = emb
239
+ logger.info(f" {len(self.protein_embs)} proteins loaded")
240
+
241
+ # Log protein length statistics
242
+ lengths = [v.shape[0] for v in self.protein_embs.values()]
243
+ logger.info(
244
+ f" Protein lengths: min={min(lengths)}, "
245
+ f"max={max(lengths)}, mean={np.mean(lengths):.0f}"
246
+ )
247
+
248
+ # Filter to records with available embeddings
249
+ # CONTRACT r2: fail closed. protein_id is used EXACTLY as it appears
250
+ # in the CSV (trailing spaces included) because the H5 keys keep them.
251
+ available = set(self.protein_embs.keys())
252
+ has_protein = self.records["protein_id"].isin(available)
253
+ if not has_protein.all():
254
+ missing = int((~has_protein).sum())
255
+ bad = sorted(self.records.loc[~has_protein, "protein_id"].unique())[:5]
256
+ raise RuntimeError(
257
+ f"{missing} records missing protein embeddings "
258
+ f"({len(bad)} distinct shown): {bad}")
259
+
260
+ if expected_rows is not None and len(self.records) != int(expected_rows):
261
+ raise RuntimeError(
262
+ f"retained row count {len(self.records)} != manifest "
263
+ f"expected {expected_rows} for stratum {stratum}")
264
+ logger.info(f" Final {split} dataset: {len(self.records):,} records")
265
+
266
+ def __len__(self) -> int:
267
+ return len(self.records)
268
+
269
+ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
270
+ row = self.records.iloc[idx]
271
+ wurcs = row["glycan_wurcs"]
272
+ protein_id = row["protein_id"]
273
+
274
+ # Glycan tokens (pre-cached, already padded to max_glycan_length)
275
+ cached = self.tokenized_cache[wurcs]
276
+
277
+ # Protein embedding — per-residue [Lp, D]
278
+ protein_emb = self.protein_embs[protein_id]
279
+
280
+ # Target
281
+ target = torch.tensor(row[self.target_col], dtype=torch.float)
282
+
283
+ # Concentration features
284
+ has_conc = torch.tensor(row.get("has_conc", 0), dtype=torch.float)
285
+ log_conc = torch.tensor(row.get("log_conc", 0.0), dtype=torch.float)
286
+
287
+ return {
288
+ "token_ids": cached["token_ids"],
289
+ "attention_mask": cached["attention_mask"],
290
+ "branch_depths": cached["branch_depths"],
291
+ "linkage_types": cached["linkage_types"],
292
+ "motif_features": cached["motif_features"],
293
+ "protein_emb": protein_emb, # [Lp, D] variable-length!
294
+ "protein_length": protein_emb.shape[0],
295
+ "target": target,
296
+ "has_conc": has_conc,
297
+ "log_conc": log_conc,
298
+ }
299
+
300
+
301
+ def collate_fn(
302
+ batch: List[Dict[str, torch.Tensor]],
303
+ ) -> Dict[str, torch.Tensor]:
304
+ """
305
+ Collate with variable-length protein padding.
306
+
307
+ Glycan sequences are already padded (BPE tokenizer pads to
308
+ max_glycan_length). Protein sequences need padding to the
309
+ max length in the batch.
310
+ """
311
+ # Glycan: already fixed-length, just stack
312
+ token_ids = torch.stack([item["token_ids"] for item in batch])
313
+ attention_mask = torch.stack(
314
+ [item["attention_mask"] for item in batch]
315
+ ).float()
316
+ branch_depths = torch.stack(
317
+ [item["branch_depths"] for item in batch]
318
+ )
319
+ linkage_types = torch.stack(
320
+ [item["linkage_types"] for item in batch]
321
+ )
322
+ motif_features = torch.stack([item["motif_features"] for item in batch])
323
+
324
+ # Protein: variable-length → pad to max in batch
325
+ protein_embs = [item["protein_emb"] for item in batch]
326
+ protein_padded = pad_sequence(protein_embs, batch_first=True) # [B, Lp_max, D]
327
+
328
+ # Protein mask: 1 for real residues, 0 for padding
329
+ protein_lengths = [item["protein_length"] for item in batch]
330
+ max_prot_len = protein_padded.shape[1]
331
+ protein_mask = torch.zeros(len(batch), max_prot_len)
332
+ for i, length in enumerate(protein_lengths):
333
+ protein_mask[i, :length] = 1.0
334
+
335
+ # Targets and metadata
336
+ targets = torch.stack([item["target"] for item in batch])
337
+ has_conc = torch.stack([item["has_conc"] for item in batch])
338
+ log_conc = torch.stack([item["log_conc"] for item in batch])
339
+
340
+ return {
341
+ "token_ids": token_ids,
342
+ "attention_mask": attention_mask,
343
+ "branch_depths": branch_depths,
344
+ "linkage_types": linkage_types,
345
+ "motif_features": motif_features,
346
+ "protein_emb": protein_padded, # [B, Lp_max, D]
347
+ "protein_mask": protein_mask, # [B, Lp_max]
348
+ "target": targets,
349
+ "has_conc": has_conc,
350
+ "log_conc": log_conc,
351
+ }
releases/2026-10-02/affinose/training_v8.py ADDED
@@ -0,0 +1,1038 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bertint V8 Training — Cross-Attention + Live Bertose Finetuning
3
+
4
+ Based on V7 trainer with changes for V8 architecture:
5
+ - Per-residue protein embeddings (variable-length, padded in collate)
6
+ - protein_mask passed to model for cross-attention
7
+ - AMP (GradScaler + autocast) built in from the start
8
+ - Regression only (no classification mode — V7 showed regression wins)
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import logging
14
+ import os
15
+ import hashlib
16
+ import random
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Dict, List, Optional, Tuple
21
+
22
+ import numpy as np
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+ from scipy.stats import spearmanr, pearsonr
27
+ from torch.cuda.amp import GradScaler, autocast
28
+ from torch.utils.data import DataLoader
29
+
30
+ from bertint_v8 import BertintV8, BertintV8Loss, load_bertose_encoder
31
+ from dataset_v8 import BertintV8Dataset, collate_fn
32
+
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format="%(asctime)s - %(levelname)s - %(message)s",
36
+ )
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ # ============================================================================
41
+ # Reproducibility
42
+ # ============================================================================
43
+
44
+
45
+ def set_seed(seed: int = 42) -> None:
46
+ """Set random seeds for reproducibility."""
47
+ random.seed(seed)
48
+ np.random.seed(seed)
49
+ torch.manual_seed(seed)
50
+ if torch.cuda.is_available():
51
+ torch.cuda.manual_seed_all(seed)
52
+
53
+
54
+ # ============================================================================
55
+ # Metrics
56
+ # ============================================================================
57
+
58
+
59
+ def _atomic_torch_save(obj, path):
60
+ """Write via a temp file + os.replace so a preemption cannot truncate."""
61
+ tmp = str(path) + ".tmp"
62
+ torch.save(obj, tmp)
63
+ os.replace(tmp, path)
64
+
65
+ class AssayGroupedBatchSampler(torch.utils.data.Sampler):
66
+ """Batches of K assays x M rows, emitted as contiguous blocks.
67
+
68
+ The block structure is what lets the loss recover the grouping from the batch shape without
69
+ threading assay ids through collate_fn. Sampling is uniform over ASSAYS, not rows, so the
70
+ gradient weighting matches the equal-assay macro we actually report.
71
+ """
72
+
73
+ def __init__(self, exp_ids, assays_per_batch, rows_per_assay, seed=42):
74
+ import collections
75
+ self.K, self.M = assays_per_batch, rows_per_assay
76
+ idx = collections.defaultdict(list)
77
+ for i, e in enumerate(exp_ids):
78
+ idx[e].append(i)
79
+ # an assay smaller than M cannot fill a block; excluded and COUNTED, never padded
80
+ self.groups = [np.asarray(v) for v in idx.values() if len(v) >= self.M]
81
+ self.excluded_assays = len(idx) - len(self.groups)
82
+ self.rng = np.random.default_rng(seed)
83
+ self.n_batches = max(len(self.groups) // self.K, 1)
84
+ logger.info(" assay-grouped sampler: %d usable assays (%d too small for M=%d), "
85
+ "%d batches/epoch of %dx%d",
86
+ len(self.groups), self.excluded_assays, self.M,
87
+ self.n_batches, self.K, self.M)
88
+
89
+ def __iter__(self):
90
+ order = self.rng.permutation(len(self.groups))
91
+ for b in range(self.n_batches):
92
+ batch = []
93
+ for gi in order[b * self.K:(b + 1) * self.K]:
94
+ g = self.groups[gi]
95
+ pick = self.rng.choice(len(g), self.M, replace=False)
96
+ batch.extend(int(x) for x in g[pick])
97
+ if len(batch) == self.K * self.M:
98
+ yield batch
99
+
100
+ def __len__(self):
101
+ return self.n_batches
102
+
103
+
104
+ class WithinAssayPairwiseLoss(nn.Module):
105
+ """RankNet pairwise logistic loss, computed INSIDE each assay block.
106
+
107
+ The batch arrives as K contiguous blocks of M rows (see AssayGroupedBatchSampler), so it
108
+ reshapes to (K, M) and every pair compared is guaranteed to come from the same assay — which
109
+ is the only comparison the estimand defines.
110
+ """
111
+
112
+ def __init__(self, assays_per_batch, rows_per_assay):
113
+ super().__init__()
114
+ self.K, self.M = assays_per_batch, rows_per_assay
115
+
116
+ def forward(self, pred, target):
117
+ if pred.numel() != self.K * self.M:
118
+ # eval batches are not grouped; fall back so the same object can score them
119
+ return F.mse_loss(pred, target)
120
+ p = pred.view(self.K, self.M)
121
+ y = target.view(self.K, self.M)
122
+ d = p.unsqueeze(2) - p.unsqueeze(1) # (K, M, M) predicted differences
123
+ s = torch.sign(y.unsqueeze(2) - y.unsqueeze(1)) # (K, M, M) true order, 0 where tied
124
+ m = s != 0
125
+ if not m.any():
126
+ return pred.sum() * 0.0
127
+ return F.softplus(-s[m] * d[m]).mean()
128
+
129
+
130
+ def equal_assay_macro(preds, targets, groups, min_rows=5, min_distinct=2):
131
+ """Mean per-assay Spearman, equal weight per eligible assay.
132
+
133
+ Eligibility mirrors the split's own rule: an assay needs >= min_rows rows and >= min_distinct
134
+ distinct target values, plus >= min_distinct distinct PREDICTIONS (a constant prediction makes
135
+ Spearman undefined, and treating that as 0.0 would silently reward degenerate assays).
136
+ Returns (macro, n_eligible, n_total).
137
+ """
138
+ import collections
139
+ idx = collections.defaultdict(list)
140
+ for i, g in enumerate(groups):
141
+ idx[g].append(i)
142
+ vals = []
143
+ for g, ii in idx.items():
144
+ if len(ii) < min_rows:
145
+ continue
146
+ t_ = targets[ii]
147
+ p_ = preds[ii]
148
+ if len(np.unique(t_)) < min_distinct or len(np.unique(p_)) < min_distinct:
149
+ continue
150
+ r, _ = spearmanr(p_, t_)
151
+ if not np.isnan(r):
152
+ vals.append(float(r))
153
+ if not vals:
154
+ return 0.0, 0, len(idx)
155
+ return float(np.mean(vals)), len(vals), len(idx)
156
+
157
+
158
+ def compute_metrics(
159
+ preds: np.ndarray, targets: np.ndarray
160
+ ) -> Dict[str, float]:
161
+ """Compute Spearman, Pearson, MSE."""
162
+ rho, _ = spearmanr(preds, targets)
163
+ r, _ = pearsonr(preds, targets)
164
+ mse = np.mean((preds - targets) ** 2)
165
+ # CONTRACT r2: a NaN correlation means a collapsed model. Coercing it to
166
+ # 0.0 disguises collapse as merely-bad performance, so fail closed.
167
+ if np.isnan(rho) or np.isnan(r) or not np.isfinite(mse):
168
+ raise RuntimeError(
169
+ "non-finite metric: spearman=%r pearson=%r mse=%r "
170
+ "(pred std=%r, target std=%r)" % (rho, r, mse,
171
+ float(np.std(preds)), float(np.std(targets))))
172
+ return {"spearman": float(rho), "pearson": float(r), "mse": float(mse)}
173
+
174
+
175
+ # ============================================================================
176
+ # Trainer
177
+ # ============================================================================
178
+
179
+
180
+ class BertintV8Trainer:
181
+ """
182
+ Trainer for BertintV8 with cross-attention and AMP.
183
+
184
+ Args:
185
+ model: BertintV8 model.
186
+ criterion: Loss function.
187
+ train_loader: Training data loader.
188
+ val_loader: Validation data loader.
189
+ test_loader: Test data loader.
190
+ output_dir: Directory for checkpoints and results.
191
+ lr_encoder: Learning rate for Bertose encoder layers.
192
+ lr_head: Learning rate for cross-attention, SWE, and head.
193
+ weight_decay: Weight decay for AdamW.
194
+ max_grad_norm: Maximum gradient norm for clipping.
195
+ epochs: Number of training epochs.
196
+ patience: Early stopping patience.
197
+ checkpoint_interval: Save checkpoint every N epochs.
198
+ resume: Whether to resume from last checkpoint.
199
+ """
200
+
201
+ def __init__(
202
+ self,
203
+ model: BertintV8,
204
+ criterion: nn.Module,
205
+ train_loader: DataLoader,
206
+ val_loader: DataLoader,
207
+ test_loader: DataLoader,
208
+ output_dir: str,
209
+ lr_encoder: float = 1e-5,
210
+ lr_head: float = 1e-4,
211
+ weight_decay: float = 0.01,
212
+ max_grad_norm: float = 1.0,
213
+ epochs: int = 50,
214
+ patience: int = 15,
215
+ checkpoint_interval: int = 5,
216
+ resume: bool = False,
217
+ fingerprint: str = None,
218
+ input_hashes: dict = None,
219
+ warmup_pct: float = 0.0,
220
+ ):
221
+ self.model = model
222
+ self.criterion = criterion
223
+ self.train_loader = train_loader
224
+ self.val_loader = val_loader
225
+ self.test_loader = test_loader
226
+ self.output_dir = output_dir
227
+ self.epochs = epochs
228
+ self.patience = patience
229
+ self.checkpoint_interval = checkpoint_interval
230
+ self.resume = resume
231
+ self.max_grad_norm = max_grad_norm
232
+
233
+ os.makedirs(output_dir, exist_ok=True)
234
+
235
+ self.device = torch.device(
236
+ "cuda" if torch.cuda.is_available() else "cpu"
237
+ )
238
+ self.model.to(self.device)
239
+ self.criterion.to(self.device)
240
+
241
+ # AMP scaler
242
+ self.scaler = GradScaler()
243
+
244
+ # Separate param groups: encoder (small lr) vs rest (larger lr)
245
+ encoder_params = []
246
+ head_params = []
247
+ for name, param in model.named_parameters():
248
+ if not param.requires_grad:
249
+ continue
250
+ if name.startswith("seq_embeddings") or name.startswith(
251
+ "seq_layers"
252
+ ):
253
+ encoder_params.append(param)
254
+ else:
255
+ head_params.append(param)
256
+
257
+ logger.info(
258
+ f" Param groups: encoder={len(encoder_params)} tensors "
259
+ f"(lr={lr_encoder}), head={len(head_params)} tensors "
260
+ f"(lr={lr_head})"
261
+ )
262
+
263
+ self.optimizer = torch.optim.AdamW(
264
+ [
265
+ {
266
+ "params": encoder_params,
267
+ "lr": lr_encoder,
268
+ "weight_decay": weight_decay,
269
+ },
270
+ {
271
+ "params": head_params,
272
+ "lr": lr_head,
273
+ "weight_decay": weight_decay,
274
+ },
275
+ ]
276
+ )
277
+
278
+ # OneCycleLR with per-batch stepping (matches Twin Peaks pattern)
279
+ # Built-in warmup (pct_start) + cosine annealing
280
+ total_steps = len(train_loader) * epochs
281
+ if warmup_pct > 0:
282
+ pct_start = warmup_pct
283
+ else:
284
+ pct_start = 0.3 # Default: 30% warmup
285
+
286
+ self.scheduler = torch.optim.lr_scheduler.OneCycleLR(
287
+ self.optimizer,
288
+ max_lr=[lr_encoder, lr_head],
289
+ total_steps=total_steps,
290
+ pct_start=pct_start,
291
+ anneal_strategy='cos',
292
+ )
293
+ warmup_steps_actual = int(total_steps * pct_start)
294
+ logger.info(
295
+ f" Scheduler: OneCycleLR per-batch stepping"
296
+ )
297
+ logger.info(
298
+ f" total_steps={total_steps:,}, "
299
+ f"warmup={warmup_steps_actual:,} steps "
300
+ f"({pct_start*100:.0f}%), cosine decay"
301
+ )
302
+
303
+ # State
304
+ self.start_epoch = 0
305
+ self.best_metric = -float("inf")
306
+ self.patience_counter = 0
307
+ self.history: List[Dict] = []
308
+
309
+ # CONTRACT r2 FIX: the fingerprint must exist BEFORE resume, which is
310
+ # the last statement of __init__. Assigning it after construction left
311
+ # the guard reading None and a foreign checkpoint loaded unchecked.
312
+ self.fingerprint = fingerprint
313
+ self.input_hashes = input_hashes or {}
314
+ self.early_stopped = False
315
+ self.best_epoch = -1
316
+ if resume:
317
+ self._resume_from_checkpoint()
318
+
319
+ def train(self) -> Dict:
320
+ """Full training loop with early stopping."""
321
+ logger.info(f"\nStarting V8 training for {self.epochs} epochs")
322
+ logger.info(f" Device: {self.device}")
323
+ logger.info(f" Train batches: {len(self.train_loader)}")
324
+ logger.info(f" Val batches: {len(self.val_loader)}")
325
+ logger.info(f" AMP: enabled")
326
+
327
+ for epoch in range(self.start_epoch, self.epochs):
328
+ t0 = time.time()
329
+
330
+ train_loss = self._train_epoch(epoch)
331
+ val_loss, val_metrics = self._eval_epoch(self.val_loader)
332
+
333
+ elapsed = time.time() - t0
334
+ rho_pooled = val_metrics["spearman"]
335
+ rho = val_metrics["macro_spearman"] # the DECLARED selector
336
+ r = val_metrics["pearson"]
337
+
338
+ logger.info(
339
+ f" Epoch {epoch + 1:3d} | Train loss={train_loss:.4f} | "
340
+ f"Val loss={val_loss:.4f} MACRO={rho:.4f} "
341
+ f"(pooled={rho_pooled:.4f}, {val_metrics['macro_assays_eligible']}"
342
+ f"/{val_metrics['macro_assays_total']} assays) r={r:.4f} | "
343
+ f"{elapsed:.1f}s"
344
+ )
345
+
346
+ # Track best
347
+ if rho > self.best_metric:
348
+ self.best_metric = rho
349
+ self.patience_counter = 0
350
+ _atomic_torch_save(
351
+ self.model.state_dict(),
352
+ os.path.join(self.output_dir, "best_model.pt"),
353
+ )
354
+ self.best_epoch = epoch
355
+ logger.info(f" * New best: {rho:.4f}")
356
+ else:
357
+ self.patience_counter += 1
358
+
359
+ # History
360
+ self.history.append(
361
+ {
362
+ "epoch": epoch + 1,
363
+ "train_loss": train_loss,
364
+ "val_loss": val_loss,
365
+ "val_metrics": val_metrics,
366
+ "lr_encoder": self.optimizer.param_groups[0]["lr"],
367
+ "lr_head": self.optimizer.param_groups[1]["lr"],
368
+ }
369
+ )
370
+
371
+ # (scheduler.step() is now called per-batch in _train_epoch)
372
+
373
+ # Periodic checkpoint
374
+ if (epoch + 1) % self.checkpoint_interval == 0:
375
+ self._save_checkpoint(epoch + 1)
376
+
377
+ # Early stopping
378
+ if self.patience_counter >= self.patience:
379
+ logger.info(
380
+ f" Early stopping at epoch {epoch + 1} "
381
+ f"(no improvement for {self.patience} epochs)"
382
+ )
383
+ self.early_stopped = True
384
+ self._save_checkpoint(epoch + 1)
385
+ break
386
+
387
+ # Load best and test
388
+ logger.info(f"\n{'=' * 60}")
389
+ logger.info("Loading best model for test evaluation...")
390
+ best_path = os.path.join(self.output_dir, "best_model.pt")
391
+ self.model.load_state_dict(
392
+ torch.load(best_path, map_location=self.device, weights_only=False)
393
+ )
394
+
395
+ test_loss, test_metrics = self._eval_epoch(self.test_loader)
396
+
397
+ logger.info(f"\n{'=' * 60}")
398
+ logger.info("TEST RESULTS:")
399
+ logger.info(f" Spearman rho: {test_metrics['spearman']:.4f}")
400
+ logger.info(f" Pearson r: {test_metrics['pearson']:.4f}")
401
+ logger.info(f" MSE: {test_metrics['mse']:.6f}")
402
+ logger.info(f"{'=' * 60}")
403
+
404
+ # Save results
405
+ # CONTRACT r2: the old label hard-coded "SWE" regardless of what was
406
+ # actually requested. Describe the configuration that ran.
407
+ results = {
408
+ "task_type": "regression",
409
+ "architecture": getattr(self, "architecture_label",
410
+ "cross-attention + live Bertose"),
411
+ "best_metric": self.best_metric,
412
+ "best_metric_is": "validation",
413
+ "best_epoch": self.best_epoch,
414
+ "early_stopped": self.early_stopped,
415
+ "epochs_ran": len(self.history),
416
+ "test_metrics": test_metrics,
417
+ "test_loss": test_loss,
418
+ "history": self.history,
419
+ "fingerprint": self.fingerprint,
420
+ "input_hashes": self.input_hashes,
421
+ }
422
+ # CONTRACT r2: dump per-record predictions. Without them no
423
+ # cohort-averaged statistic can be recovered after the fact.
424
+ try:
425
+ import numpy as _np
426
+ _p = getattr(self, "_last_preds", None)
427
+ _t = getattr(self, "_last_targets", None)
428
+ if _p is not None and _t is not None:
429
+ _np.savez_compressed(
430
+ os.path.join(self.output_dir, "test_predictions.npz"),
431
+ predictions=_np.asarray(_p, dtype=_np.float32),
432
+ targets=_np.asarray(_t, dtype=_np.float32))
433
+ logger.info(f" [PRED] wrote {len(_p)} test predictions")
434
+ else:
435
+ raise RuntimeError("no test predictions captured")
436
+ except Exception as _e:
437
+ raise RuntimeError("failed to write test predictions: %s" % _e)
438
+
439
+ results_path = os.path.join(self.output_dir, "results.json")
440
+ with open(results_path, "w") as f:
441
+ json.dump(results, f, indent=2)
442
+ logger.info(f"Results saved to {results_path}")
443
+
444
+ return results
445
+
446
+ def _train_epoch(self, epoch: int) -> float:
447
+ """Run one training epoch with AMP."""
448
+ self.model.train()
449
+ total_loss = 0.0
450
+ n_batches = len(self.train_loader)
451
+
452
+ for batch_idx, batch in enumerate(self.train_loader):
453
+ # Move to device
454
+ token_ids = batch["token_ids"].to(self.device)
455
+ attention_mask = batch["attention_mask"].to(self.device)
456
+ branch_depths = batch["branch_depths"].to(self.device)
457
+ linkage_types = batch["linkage_types"].to(self.device)
458
+ protein_emb = batch["protein_emb"].to(self.device)
459
+ protein_mask = batch["protein_mask"].to(self.device)
460
+ target = batch["target"].to(self.device)
461
+
462
+ self.optimizer.zero_grad()
463
+
464
+ # AMP forward
465
+ with autocast():
466
+ pred = self.model(
467
+ token_ids=token_ids,
468
+ attention_mask=attention_mask,
469
+ branch_depths=branch_depths,
470
+ linkage_types=linkage_types,
471
+ protein_emb=protein_emb,
472
+ protein_mask=protein_mask,
473
+ motif_features=batch["motif_features"].to(self.device),
474
+ )
475
+ loss = self.criterion(pred, target)
476
+
477
+ # AMP backward
478
+ self.scaler.scale(loss).backward()
479
+ self.scaler.unscale_(self.optimizer)
480
+ torch.nn.utils.clip_grad_norm_(
481
+ self.model.parameters(), self.max_grad_norm
482
+ )
483
+ self.scaler.step(self.optimizer)
484
+ self.scaler.update()
485
+
486
+ # Per-batch LR scheduling (OneCycleLR)
487
+ self.scheduler.step()
488
+
489
+ total_loss += loss.item()
490
+
491
+ # Progress logging
492
+ if (batch_idx + 1) % 200 == 0:
493
+ avg = total_loss / (batch_idx + 1)
494
+ lr_enc = self.optimizer.param_groups[0]["lr"]
495
+ logger.info(
496
+ f" [E{epoch + 1}][{batch_idx + 1}/{n_batches}] "
497
+ f"loss={avg:.4f} lr_enc={lr_enc:.2e}"
498
+ )
499
+
500
+ return total_loss / n_batches
501
+
502
+ @torch.no_grad()
503
+ def _eval_epoch(
504
+ self, loader: DataLoader
505
+ ) -> Tuple[float, Dict[str, float]]:
506
+ """Run evaluation with AMP."""
507
+ self.model.eval()
508
+ total_loss = 0.0
509
+ all_preds = []
510
+ all_targets = []
511
+
512
+ for batch in loader:
513
+ token_ids = batch["token_ids"].to(self.device)
514
+ attention_mask = batch["attention_mask"].to(self.device)
515
+ branch_depths = batch["branch_depths"].to(self.device)
516
+ linkage_types = batch["linkage_types"].to(self.device)
517
+ protein_emb = batch["protein_emb"].to(self.device)
518
+ protein_mask = batch["protein_mask"].to(self.device)
519
+ target = batch["target"].to(self.device)
520
+
521
+ with autocast():
522
+ pred = self.model(
523
+ token_ids=token_ids,
524
+ attention_mask=attention_mask,
525
+ branch_depths=branch_depths,
526
+ linkage_types=linkage_types,
527
+ protein_emb=protein_emb,
528
+ protein_mask=protein_mask,
529
+ motif_features=batch["motif_features"].to(self.device),
530
+ )
531
+ loss = self.criterion(pred, target)
532
+
533
+ total_loss += loss.item()
534
+ all_preds.extend(pred.float().cpu().numpy())
535
+ all_targets.extend(target.cpu().numpy())
536
+
537
+ avg_loss = total_loss / len(loader)
538
+ metrics = compute_metrics(
539
+ np.array(all_preds), np.array(all_targets)
540
+ )
541
+ # ⛔ DECLARED SELECTOR: equal-assay macro, not pooled. Cohort ids come from the dataset's
542
+ # own row order, which equals evaluation order because val/test loaders use shuffle=False.
543
+ # Asserted, not assumed — a silent misalignment here would select on scrambled assays.
544
+ try:
545
+ recs = loader.dataset.records
546
+ assert len(recs) == len(all_preds), (
547
+ "eval row count %d != dataset rows %d; loader order is not dataset order"
548
+ % (len(all_preds), len(recs)))
549
+ groups = recs["exp_id"].values
550
+ macro, n_elig, n_tot = equal_assay_macro(
551
+ np.array(all_preds), np.array(all_targets), groups)
552
+ metrics["macro_spearman"] = macro
553
+ metrics["macro_assays_eligible"] = int(n_elig)
554
+ metrics["macro_assays_total"] = int(n_tot)
555
+ except Exception as e:
556
+ logger.error("FAIL-CLOSED: equal-assay macro unavailable: %s", e)
557
+ raise
558
+ self._last_preds = all_preds
559
+ self._last_targets = all_targets
560
+ return avg_loss, metrics
561
+
562
+ def _save_checkpoint(self, epoch: int) -> None:
563
+ """Save full training state for resume."""
564
+ ckpt = {
565
+ "epoch": epoch,
566
+ "model_state_dict": self.model.state_dict(),
567
+ "optimizer_state_dict": self.optimizer.state_dict(),
568
+ "scheduler_state_dict": self.scheduler.state_dict(),
569
+ "scaler_state_dict": self.scaler.state_dict(),
570
+ "best_metric": self.best_metric,
571
+ "patience_counter": self.patience_counter,
572
+ "history": self.history,
573
+ # CONTRACT r2: without RNG state a resumed fit replays the data
574
+ # order and dropout stream from epoch 0 and is a DIFFERENT
575
+ # experiment from an uninterrupted fit at the same seed.
576
+ "py_rng": random.getstate(),
577
+ "np_rng": np.random.get_state(),
578
+ "torch_rng": torch.get_rng_state(),
579
+ "cuda_rng": (torch.cuda.get_rng_state_all()
580
+ if torch.cuda.is_available() else None),
581
+ "best_epoch": getattr(self, "best_epoch", -1),
582
+ "early_stopped": getattr(self, "early_stopped", False),
583
+ "fingerprint": getattr(self, "fingerprint", None),
584
+ }
585
+ path = os.path.join(self.output_dir, "last_checkpoint.pt")
586
+ _atomic_torch_save(ckpt, path)
587
+ logger.info(f" [CKPT] Saved epoch {epoch}")
588
+
589
+ def _resume_from_checkpoint(self) -> None:
590
+ """Resume training from last checkpoint."""
591
+ ckpt_path = os.path.join(self.output_dir, "last_checkpoint.pt")
592
+ if not os.path.exists(ckpt_path):
593
+ logger.info(" No checkpoint found, starting fresh")
594
+ return
595
+
596
+ ckpt = torch.load(ckpt_path, map_location=self.device, weights_only=False)
597
+ # CONTRACT r2: every arm/seed/port produces an identically shaped state
598
+ # dict, so shape is no check at all. Refuse anything whose trajectory
599
+ # fingerprint differs from this cell's.
600
+ mine = getattr(self, "fingerprint", None)
601
+ theirs = ckpt.get("fingerprint")
602
+ if mine is not None and theirs is not None and mine != theirs:
603
+ raise RuntimeError(
604
+ "refusing foreign checkpoint: fingerprint %s != %s"
605
+ % (theirs, mine))
606
+ if mine is not None and theirs is None:
607
+ raise RuntimeError(
608
+ "refusing unfingerprinted checkpoint (pre-contract-r2 run) "
609
+ "at %s" % ckpt_path)
610
+ self.model.load_state_dict(ckpt["model_state_dict"])
611
+ self.optimizer.load_state_dict(ckpt["optimizer_state_dict"])
612
+ self.scheduler.load_state_dict(ckpt["scheduler_state_dict"])
613
+ if "scaler_state_dict" in ckpt:
614
+ self.scaler.load_state_dict(ckpt["scaler_state_dict"])
615
+ self.start_epoch = ckpt["epoch"]
616
+ self.best_metric = ckpt["best_metric"]
617
+ self.patience_counter = ckpt["patience_counter"]
618
+ self.history = ckpt["history"]
619
+ self.best_epoch = ckpt.get("best_epoch", -1)
620
+ self.early_stopped = ckpt.get("early_stopped", False)
621
+ if ckpt.get("py_rng") is not None:
622
+ random.setstate(ckpt["py_rng"])
623
+ np.random.set_state(ckpt["np_rng"])
624
+ torch.set_rng_state(ckpt["torch_rng"].cpu()
625
+ if hasattr(ckpt["torch_rng"], "cpu")
626
+ else ckpt["torch_rng"])
627
+ if ckpt.get("cuda_rng") is not None and torch.cuda.is_available():
628
+ # torch.load(map_location=device, weights_only=False) moves these onto the GPU;
629
+ # set_rng_state_all requires CPU ByteTensors.
630
+ torch.cuda.set_rng_state_all(
631
+ [t.cpu() if hasattr(t, "cpu") else t
632
+ for t in ckpt["cuda_rng"]])
633
+ logger.info(" [RNG] restored python/numpy/torch/cuda state")
634
+ else:
635
+ raise RuntimeError("checkpoint carries no RNG state; refusing resume")
636
+ logger.info(
637
+ f" Resumed from epoch {self.start_epoch}, "
638
+ f"best={self.best_metric:.4f}"
639
+ )
640
+
641
+
642
+ # ============================================================================
643
+ # Main
644
+ # ============================================================================
645
+
646
+
647
+ def main():
648
+ """Entry point for V8 training."""
649
+ parser = argparse.ArgumentParser(description="Bertint V8 Training")
650
+ parser.add_argument(
651
+ "--csv_path", required=True, help="Path to binding data CSV"
652
+ )
653
+ parser.add_argument(
654
+ "--split_path", required=True, help="Path to glycan-cold splits JSON"
655
+ )
656
+ parser.add_argument(
657
+ "--protein_emb_path", required=True, help="Path to ESM-C HDF5"
658
+ )
659
+ parser.add_argument(
660
+ "--vocab_path", required=True, help="Path to BPE vocab JSON"
661
+ )
662
+ parser.add_argument(
663
+ "--bertose_checkpoint", required=True, help="Bertose checkpoint"
664
+ )
665
+ parser.add_argument("--output_dir", required=True, help="Output dir")
666
+
667
+ # Model architecture
668
+ parser.add_argument(
669
+ "--freeze_layers", type=int, default=4, help="Layers to freeze"
670
+ )
671
+ parser.add_argument(
672
+ "--shared_dim", type=int, default=512, help="Shared dim"
673
+ )
674
+ parser.add_argument(
675
+ "--num_cross_layers", type=int, default=2, help="Cross-attn layers"
676
+ )
677
+ parser.add_argument(
678
+ "--num_heads", type=int, default=8, help="Attention heads"
679
+ )
680
+ parser.add_argument(
681
+ "--swe_slices", type=int, default=512, help="SWE slices"
682
+ )
683
+ parser.add_argument(
684
+ "--dropout", type=float, default=0.1, help="Dropout rate"
685
+ )
686
+ parser.add_argument(
687
+ "--protein_dim", type=int, default=960, help="ESM-C dim"
688
+ )
689
+ parser.add_argument(
690
+ "--separate_swe", action="store_true",
691
+ help="Use separate SWE modules for glycan and protein"
692
+ )
693
+
694
+ # Training
695
+ parser.add_argument(
696
+ "--lr_encoder", type=float, default=1e-5, help="Encoder LR"
697
+ )
698
+ parser.add_argument(
699
+ "--lr_head", type=float, default=1e-4, help="Head LR"
700
+ )
701
+ parser.add_argument(
702
+ "--weight_decay", type=float, default=0.01, help="Weight decay"
703
+ )
704
+ parser.add_argument(
705
+ "--max_grad_norm", type=float, default=1.0, help="Grad clip"
706
+ )
707
+ parser.add_argument(
708
+ "--batch_size", type=int, default=32, help="Batch size"
709
+ )
710
+ parser.add_argument(
711
+ "--epochs", type=int, default=50, help="Max epochs"
712
+ )
713
+ parser.add_argument(
714
+ "--patience", type=int, default=15, help="Early stopping"
715
+ )
716
+ parser.add_argument(
717
+ "--max_glycan_length", type=int, default=256, help="Max glycan len"
718
+ )
719
+ parser.add_argument(
720
+ "--max_protein_length", type=int, default=1024, help="Max protein len"
721
+ )
722
+ parser.add_argument("--protein_mode", default="real",
723
+ choices=["real","constant"],
724
+ help="real | constant (protein input zeroed; NOT the G arm — the "
725
+ "protein pathway still exists. Use --glycan_only for G.)")
726
+ # G arm: do not construct the protein pathway at all. 'constant' only zeroes the
727
+ # input; projection, cross-attention and the 2x-width head all remain.
728
+ parser.add_argument("--ranking_loss", action="store_true",
729
+ help="within-assay pairwise ranking loss + assay-grouped batches; "
730
+ "aligns the objective with the equal-assay macro estimand")
731
+ parser.add_argument("--assays_per_batch", type=int, default=4)
732
+ parser.add_argument("--rows_per_assay", type=int, default=8)
733
+ parser.add_argument("--protein_binding_channel", action="store_true",
734
+ help="R2: last protein channel is a per-residue binding "
735
+ "probability; pool globally AND binding-weighted, then "
736
+ "concatenate (KinForm Eqs 2-5). Use --protein_dim 961.")
737
+ parser.add_argument("--glycan_only", action="store_true",
738
+ help="protected arm G: no protein tensor, mask, projection or "
739
+ "cross-attention; head narrows to the glycan width")
740
+ parser.add_argument("--protein_attentive_pool", action="store_true",
741
+ help="arm A: zero-init attentive protein pooling "
742
+ "(identical to mean pooling at init)")
743
+ parser.add_argument(
744
+ "--motif_additive_dim", type=int, default=0,
745
+ help="d>0 adds score += w . motif_features over d raw motif counts (released counts 0-13), zero-initialised")
746
+ parser.add_argument("--glycan_size_channel", action="store_true",
747
+ help="Append log1p(glycan token count) to the head input")
748
+ parser.add_argument("--seed", type=int, default=42, help="Random seed")
749
+ # ---- contract r2 -----------------------------------------------------
750
+ parser.add_argument("--roles_tsv", required=True)
751
+ parser.add_argument("--train_stratum", required=True)
752
+ parser.add_argument("--val_stratum", required=True)
753
+ parser.add_argument("--test_stratum", required=True)
754
+ parser.add_argument("--features_dir", required=True)
755
+ parser.add_argument("--expected_train_rows", type=int, required=True)
756
+ parser.add_argument("--expected_val_rows", type=int, required=True)
757
+ parser.add_argument("--expected_test_rows", type=int, required=True)
758
+ parser.add_argument("--manifest_sha256", required=True)
759
+ parser.add_argument("--cell_id", required=True)
760
+ parser.add_argument("--input_hashes", required=True)
761
+ parser.add_argument(
762
+ "--warmup_pct", type=float, default=0.05,
763
+ help="Fraction of total steps for warmup (0.05=5%%, 0.10=10%%)"
764
+ )
765
+ parser.add_argument(
766
+ "--target_col", default="target_rank", help="Target column"
767
+ )
768
+ parser.add_argument(
769
+ "--checkpoint_interval", type=int, default=5, help="Ckpt every N"
770
+ )
771
+ parser.add_argument(
772
+ "--resume", action="store_true", help="Resume from checkpoint"
773
+ )
774
+ # Ablation controls
775
+ parser.add_argument(
776
+ "--pooling_mode", default="swe",
777
+ choices=["swe", "mean", "joint_swe"],
778
+ help="Pooling strategy: swe (default), mean, or joint_swe"
779
+ )
780
+ parser.add_argument(
781
+ "--interaction_mode", default="product_sum",
782
+ choices=["product_sum", "concat"],
783
+ help="Interaction: product_sum (default) or concat"
784
+ )
785
+ parser.add_argument(
786
+ "--no_cross_attention", action="store_true",
787
+ help="Disable cross-attention blocks (ablation)"
788
+ )
789
+
790
+ args = parser.parse_args()
791
+
792
+ set_seed(args.seed)
793
+ logger.info("Bertint V8 Training — Cross-Attention + Live Bertose")
794
+ logger.info(f" freeze_layers={args.freeze_layers}")
795
+ logger.info(f" lr_encoder={args.lr_encoder}")
796
+ logger.info(f" lr_head={args.lr_head}")
797
+ logger.info(f" batch_size={args.batch_size}")
798
+ logger.info(f" shared_dim={args.shared_dim}")
799
+ logger.info(f" cross_layers={args.num_cross_layers}")
800
+ logger.info(f" separate_swe={args.separate_swe}")
801
+ logger.info(f" pooling_mode={args.pooling_mode}")
802
+ logger.info(f" interaction_mode={args.interaction_mode}")
803
+ logger.info(f" cross_attention={not args.no_cross_attention}")
804
+
805
+ # Load datasets
806
+ logger.info("\nLoading datasets...")
807
+ train_ds = BertintV8Dataset(
808
+ args.csv_path, args.split_path, "train",
809
+ args.protein_emb_path, args.vocab_path,
810
+ max_glycan_length=args.max_glycan_length,
811
+ max_protein_length=args.max_protein_length,
812
+ target_col=args.target_col,
813
+ roles_path=args.roles_tsv,
814
+ stratum=args.train_stratum,
815
+ features_dir=args.features_dir,
816
+ expected_rows=args.expected_train_rows,
817
+ )
818
+ val_ds = BertintV8Dataset(
819
+ args.csv_path, args.split_path, "val",
820
+ args.protein_emb_path, args.vocab_path,
821
+ max_glycan_length=args.max_glycan_length,
822
+ max_protein_length=args.max_protein_length,
823
+ target_col=args.target_col,
824
+ roles_path=args.roles_tsv,
825
+ stratum=args.val_stratum,
826
+ features_dir=args.features_dir,
827
+ expected_rows=args.expected_val_rows,
828
+ )
829
+ test_ds = BertintV8Dataset(
830
+ args.csv_path, args.split_path, "test",
831
+ args.protein_emb_path, args.vocab_path,
832
+ max_glycan_length=args.max_glycan_length,
833
+ max_protein_length=args.max_protein_length,
834
+ target_col=args.target_col,
835
+ roles_path=args.roles_tsv,
836
+ stratum=args.test_stratum,
837
+ features_dir=args.features_dir,
838
+ expected_rows=args.expected_test_rows,
839
+ )
840
+
841
+ if args.ranking_loss:
842
+ # ⛔ TRAIN ONLY. val/test keep plain batching + shuffle=False so the macro is computed
843
+ # exactly as it was for every previously reported arm.
844
+ _sampler = AssayGroupedBatchSampler(
845
+ train_ds.records["exp_id"].values,
846
+ args.assays_per_batch, args.rows_per_assay, seed=args.seed)
847
+ train_loader = DataLoader(
848
+ train_ds, batch_sampler=_sampler,
849
+ num_workers=4, pin_memory=True, collate_fn=collate_fn,
850
+ )
851
+ else:
852
+ train_loader = DataLoader(
853
+ train_ds, batch_size=args.batch_size, shuffle=True,
854
+ num_workers=4, pin_memory=True, collate_fn=collate_fn,
855
+ )
856
+ val_loader = DataLoader(
857
+ val_ds, batch_size=args.batch_size, shuffle=False,
858
+ num_workers=2, pin_memory=True, collate_fn=collate_fn,
859
+ )
860
+ test_loader = DataLoader(
861
+ test_ds, batch_size=args.batch_size, shuffle=False,
862
+ num_workers=2, pin_memory=True, collate_fn=collate_fn,
863
+ )
864
+
865
+ # Build model
866
+ logger.info("\nBuilding model...")
867
+ config, seq_emb, seq_layers = load_bertose_encoder(
868
+ args.bertose_checkpoint, freeze_layers=args.freeze_layers
869
+ )
870
+
871
+ model = BertintV8(
872
+ seq_embeddings=seq_emb,
873
+ seq_layers=seq_layers,
874
+ glycan_dim=config.seq_hidden_size,
875
+ protein_dim=args.protein_dim,
876
+ shared_dim=args.shared_dim,
877
+ num_cross_layers=args.num_cross_layers,
878
+ num_heads=args.num_heads,
879
+ swe_slices=args.swe_slices,
880
+ dropout=args.dropout,
881
+ separate_swe=args.separate_swe,
882
+ pooling_mode=args.pooling_mode,
883
+ interaction_mode=args.interaction_mode,
884
+ use_cross_attention=not args.no_cross_attention,
885
+ glycan_size_channel=args.glycan_size_channel,
886
+ motif_additive_dim=args.motif_additive_dim,
887
+ protein_mode=args.protein_mode,
888
+ protein_attentive_pool=args.protein_attentive_pool,
889
+ protein_tower=not args.glycan_only,
890
+ protein_binding_channel=args.protein_binding_channel,
891
+ )
892
+
893
+ total_params = sum(p.numel() for p in model.parameters())
894
+ trainable_params = sum(
895
+ p.numel() for p in model.parameters() if p.requires_grad
896
+ )
897
+ logger.info(f" Total params: {total_params:,}")
898
+ logger.info(f" Trainable: {trainable_params:,}")
899
+
900
+ # Loss
901
+ criterion = (WithinAssayPairwiseLoss(args.assays_per_batch, args.rows_per_assay)
902
+ if args.ranking_loss else BertintV8Loss())
903
+ logger.info(" objective: %s", type(criterion).__name__)
904
+
905
+ # ---------------- CONTRACT r2: seal inputs, then fingerprint -----------
906
+ def _sha_file(p):
907
+ h = hashlib.sha256()
908
+ with open(p, "rb") as fh:
909
+ for c in iter(lambda: fh.read(1 << 22), b""):
910
+ h.update(c)
911
+ return h.hexdigest()
912
+
913
+ _here = os.path.dirname(os.path.abspath(__file__))
914
+ _feat = args.features_dir
915
+ _sealed = {
916
+ "csv": args.csv_path,
917
+ "roles_tsv": args.roles_tsv,
918
+ "protein_emb_h5": args.protein_emb_path,
919
+ "vocabulary": args.vocab_path,
920
+ "upstream_checkpoint": args.bertose_checkpoint,
921
+ "dataset_v8.py": os.path.join(_here, "dataset_v8.py"),
922
+ "training_v8.py": os.path.abspath(__file__),
923
+ "bertint_v8.py": os.path.join(_here, "bertint_v8.py"),
924
+ "a1111_encoder_adapter.py": os.path.join(_here, "a1111_encoder_adapter.py"),
925
+ }
926
+ for _n in ("token_ids", "branch_depths", "linkage_types", "attention_mask",
927
+ "tree_channels_valid", "motif_features"):
928
+ _sealed["feature_" + _n] = os.path.join(_feat, "review_%s.npy" % _n)
929
+ _sealed["feature_index"] = os.path.join(_feat, "review_affinose_glycan_index.tsv.gz")
930
+
931
+ _hashes = {}
932
+ for _k, _p in _sealed.items():
933
+ if not os.path.isfile(_p):
934
+ raise RuntimeError("sealed input missing: %s -> %s" % (_k, _p))
935
+ _hashes[_k] = _sha_file(_p)
936
+
937
+ # every dynamically resolved module under the port tree or bertose root
938
+ _imports = {}
939
+ for _m in list(sys.modules.values()):
940
+ _f = getattr(_m, "__file__", None)
941
+ if not _f or not os.path.isfile(_f):
942
+ continue
943
+ _fa = os.path.abspath(_f)
944
+ if _fa.startswith(_here) or "/bertose" in _fa or "/glycan-SD-VS/" in _fa:
945
+ _imports[_fa] = _sha_file(_fa)
946
+ _hashes["_imported_modules"] = _imports
947
+ logger.info(" [SEAL] hashed %d artifacts + %d imported modules"
948
+ % (len(_sealed), len(_imports)))
949
+
950
+ # ENFORCE against the pinned expectation file, if supplied
951
+ if getattr(args, "input_hashes", None):
952
+ with open(args.input_hashes) as _fh:
953
+ _exp = json.load(_fh)
954
+ _bad = []
955
+ for _k, _v in _exp.items():
956
+ if _k.startswith("_"):
957
+ continue
958
+ if _hashes.get(_k) != _v:
959
+ _bad.append("%s: got %s expected %s"
960
+ % (_k, str(_hashes.get(_k))[:16], str(_v)[:16]))
961
+ if _bad:
962
+ raise RuntimeError("SEALED INPUT HASH DRIFT:\n " + "\n ".join(_bad))
963
+ logger.info(" [SEAL] all %d pinned hashes match" % len(
964
+ [k for k in _exp if not k.startswith("_")]))
965
+ else:
966
+ raise RuntimeError("--input_hashes is required under contract r2")
967
+
968
+ # roster identity, not just a count
969
+ _roster = {}
970
+ for _split, _ds in (("train", train_ds), ("val", val_ds), ("test", test_ds)):
971
+ _h = hashlib.sha256()
972
+ _r = _ds.records
973
+ for _i, _row in enumerate(zip(_r["protein_id"], _r["glycan_wurcs"],
974
+ _r.get("exp_id", _r["protein_id"]))):
975
+ _h.update(("%d|%s|%s|%s\n" % (_i, _row[0], _row[1], _row[2])).encode())
976
+ _roster[_split] = _h.hexdigest()
977
+ _hashes["_roster_digests"] = _roster
978
+ logger.info(" [SEAL] roster digests train=%s val=%s test=%s"
979
+ % (_roster["train"][:16], _roster["val"][:16], _roster["test"][:16]))
980
+
981
+ _fp = hashlib.sha256("|".join([
982
+ args.manifest_sha256, args.cell_id, str(args.seed),
983
+ args.train_stratum, args.val_stratum, args.test_stratum,
984
+ str(args.expected_train_rows), str(args.expected_val_rows),
985
+ str(args.expected_test_rows),
986
+ _hashes["upstream_checkpoint"], _hashes["dataset_v8.py"],
987
+ _hashes["training_v8.py"], _hashes["bertint_v8.py"],
988
+ _roster["train"], _roster["val"], _roster["test"],
989
+ str(args.epochs), str(args.batch_size), str(args.lr_encoder),
990
+ str(args.lr_head), str(args.freeze_layers), str(args.warmup_pct),
991
+ ]).encode()).hexdigest()
992
+ logger.info(" [FINGERPRINT] %s cell=%s" % (_fp[:16], args.cell_id))
993
+
994
+ os.makedirs(args.output_dir, exist_ok=True)
995
+ with open(os.path.join(args.output_dir, "receipt.json"), "w") as _fh:
996
+ json.dump({"cell_id": args.cell_id, "fingerprint": _fp,
997
+ "manifest_sha256": args.manifest_sha256,
998
+ "args": vars(args), "input_hashes": _hashes,
999
+ "roster_digests": _roster,
1000
+ "slurm_job_id": os.environ.get("SLURM_JOB_ID"),
1001
+ "slurm_array_task_id": os.environ.get("SLURM_ARRAY_TASK_ID"),
1002
+ "slurm_restart_count": os.environ.get("SLURM_RESTART_COUNT"),
1003
+ "node": os.environ.get("SLURMD_NODENAME"),
1004
+ "gpu": (torch.cuda.get_device_name(0)
1005
+ if torch.cuda.is_available() else None)},
1006
+ _fh, indent=2, default=str)
1007
+
1008
+ # Train
1009
+ trainer = BertintV8Trainer(
1010
+ model=model,
1011
+ criterion=criterion,
1012
+ train_loader=train_loader,
1013
+ val_loader=val_loader,
1014
+ test_loader=test_loader,
1015
+ output_dir=args.output_dir,
1016
+ lr_encoder=args.lr_encoder,
1017
+ lr_head=args.lr_head,
1018
+ weight_decay=args.weight_decay,
1019
+ max_grad_norm=args.max_grad_norm,
1020
+ epochs=args.epochs,
1021
+ patience=args.patience,
1022
+ checkpoint_interval=args.checkpoint_interval,
1023
+ resume=args.resume,
1024
+ warmup_pct=args.warmup_pct,
1025
+ fingerprint=_fp,
1026
+ input_hashes=_hashes,
1027
+ )
1028
+ trainer.architecture_label = (
1029
+ "cross-attention=%s pooling=%s interaction=%s + live Bertose"
1030
+ % (not args.no_cross_attention if hasattr(args, "no_cross_attention") else True,
1031
+ args.pooling_mode, args.interaction_mode))
1032
+
1033
+ results = trainer.train()
1034
+ logger.info("\nTraining complete!")
1035
+
1036
+
1037
+ if __name__ == "__main__":
1038
+ main()
releases/2026-10-02/benchmark_exclusion/MANIFEST_SHA256.tsv ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ sha256 path
2
+ 1db45969e1c06f12c1d8d2df35a166cb65c4ce0bf603aabebc2e6689bedc8e72 README.md
3
+ e2e0dfa7d133362146de10ba3dd774e9a3e186088f976189b0aae9d26b442bc4 code_inventory.tsv
4
+ 050c0af96f133ded3139baf6dfea594f91c6659f411dc25fc06d5847951b2f0c required_successor_inputs.json
5
+ 0507537ca8ee05b249f9ab3f3fa09de94b7acc633b91235f2778ae07659096bd source_verification.json
6
+ 3d598a249db90c928f8a317923ba5d37eebbc1f63383a2c39e0541f20c31ccac writing/review/glytoucan/scripts/build_connection_level_support_audit.py
7
+ a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py
8
+ 0f88f53e22d0c068a87a09508a9d286e4982eddcb4e2154e63b2ca25339ece69 writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py
9
+ 2c8821586df1caa53556b4cc14e6cd7507af4830aa8a149657571e0455eb6853 writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py
10
+ cd86a0a0b228c32e51ec73490711e37da3e6c9fdf9e64e980b7222b5312484aa writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py
11
+ 966889118fa567729e1959750b1601018ba117aed0bfad3508005ba6b1896a08 writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py
releases/2026-10-02/benchmark_exclusion/README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmark-identity exclusion and partition code
2
+
3
+ This folder supplies the five exclusion/partition scripts named in `BENCHMARK_IDENTITY_EXCLUSION_3833_20260922.md`, plus their only imported local helper. These are byte-identical copies of the original sources. This folder is included in the submission code package and reviewer ZIP. The matching exclusion lists and count tables are in `04_source_data/benchmark_exclusion` in the full submission folder.
4
+
5
+ The fifth requested file is in `multimodal/tests/`, not `multimodal/scripts/`. Relative project structure is preserved below `writing/review/` because the source resolves its inputs and helper imports from its own path. `code_inventory.tsv` records each original path, SHA-256 and copied path.
6
+
7
+ ## What the code does
8
+
9
+ 1. `build_p1_direct_local_neighbor_audit_20260710.py` parses fixed-linkage residue trees and retrieves candidate matches using a three-iteration Weisfeiler–Lehman hash. Graphs contain residue nodes labelled by the complete WURCS residue-definition string and intermediate `BOND` nodes; edges carry the corresponding endpoint position. WL uses SHA-256 label strings, edge positions and a 32-byte digest. Exact NetworkX isomorphism confirms equality of original node `label` and edge `position` attributes. Hash agreement alone is not identity evidence. The script also measures other neighbour relations for separate analyses.
10
+ 2. `build_p1_non_iar_exact_graph_authority_20260711.py` excludes sequence parents only by exact canonical WURCS string or confirmed graph identity. It filters out GlyCosmos IAR and grey-seal-only anchor memberships. A glycan also carrying another active endpoint role remains an anchor. One-edit neighbours, motifs and substructures do not cause exclusion.
11
+ 3. `repartition_a5_non_iar_80_20_20260712.py` assigns whole precluster blocks, deterministically ordered by a salted hash, using the weighted cumulative midpoint relative to the 80% target. It verifies no retained group crosses training/development for block, raw feature hash or registry family.
12
+ 4. `build_review_a5_non_iar_later_role_successor_20260720.py` adds 93 later strict interaction non-fit anchors. It requires 73 newly excluded parents, all exact matches, with no new graph-only aliases; 59 training and 14 development parents change to excluded while every other role is preserved. Its invariants require 3,673 exact matches plus 160 graph-only matches, 3,833 exclusions, 221,899 training parents and 55,476 development parents.
13
+ 5. `test_build_review_a5_non_iar_later_role_successor_20260720.py` validates the existing successor outputs and their receipts. It does not build the inputs.
14
+
15
+ Graph matching covers connected residue trees with fully specified linkage positions, **not necessarily completely specified residue stereochemistry**. Full residue-definition strings must match, including any unresolved residue annotations. Non-fixed structures receive exact-string matching only. Exclusion of matching identities does not establish motif-family or biosynthetic-family novelty.
16
+
17
+ ## Environment and checks
18
+
19
+ Python 3.10 or newer is needed by the source syntax. The graph verifier imports `networkx`; its local parser helper imports `pandas`. The other four requested files use the standard library. Historical dependency versions are not pinned by these sources; no historical environment is asserted here.
20
+
21
+ Preparation checked syntax with Python 3.12.14 and verified all six copied files byte for byte. Three standard-library builder `--help` entry points were checked. Local pandas metadata is 2.2.3; NetworkX is unavailable in that interpreter, so the graph module import/self-test was not run. No packages were installed. No exclusion scan, full builder, model inference, training, or payload-based test suite was run.
22
+
23
+ ## Inputs needed for replay
24
+
25
+ This is a source bundle, not a self-contained data release. Original frozen data remain outside it. Restore the named inputs in the matching project layout before running a builder. Do not replace pinned files with newer files of the same name.
26
+
27
+ - Initial graph audit: role-complete anchor manifest, frozen motif/fingerprint manifest and objective-eligibility manifest. The anchor-producing and fingerprint-producing pipelines are upstream prerequisites, not imported dependencies bundled here.
28
+ - Initial exclusion authority: role-complete anchor manifest, graph-neighbour edge ledger, and parent-identity manifest.
29
+ - 80/20 partition: the frozen July 11 parent/objective partition precursor plus fingerprints and parent identities. This precursor is a required input; the five scripts are not a continuous from-raw-data pipeline.
30
+ - Successor: the 22 exact path/hash inputs in `required_successor_inputs.json`, plus role CSVs named by the strict classification authority JSON. The source validates their expected hashes before processing. It also records the supplied test source.
31
+ - Tests: the successor output directory and original 80/20 predecessor parent partition.
32
+
33
+ `required_successor_inputs.json` records the hashes declared by the unchanged source and whether the inputs exist locally; this preparation did not freshly hash or copy the data payloads. Source constants additionally document the first three steps' input paths and, where available, required input hashes.
34
+
35
+ ## Commands
36
+
37
+ Run from this folder with the required dependencies installed in your selected environment. Help commands do not scan data:
38
+
39
+ ```sh
40
+ python3 writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py --help
41
+ python3 writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py --help
42
+ python3 writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py --help
43
+ python3 writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py --help
44
+ ```
45
+
46
+ The following full replay commands are documented from the CLI/source and were **not executed** during preparation. Run each only after restoring its exact frozen inputs; choose a fresh output directory. Do not overwrite original scientific artifacts.
47
+
48
+ ```sh
49
+ python3 writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py --anchor-manifest writing/review/multimodal/outputs/p1_role_complete_anchor_union_20260711/review_p1_role_complete_anchor_manifest.tsv.gz --final-anchor-authority --output-dir replay/graph_audit
50
+ python3 writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py --output-dir replay/exclusion_authority
51
+ python3 writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py --output-directory replay/partition_80_20
52
+ python3 writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py --output-directory replay/later_role_successor
53
+ REVIEW_A5_NON_IAR_SUCCESSOR_OUTPUT=replay/later_role_successor python3 writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py
54
+ ```
55
+
56
+ These commands use frozen predecessor inputs at their source-declared paths; later steps do not automatically consume preceding `replay/` outputs. The successor has source-bound input hashes and cannot be redirected through arbitrary replacement datasets. Its receipt contains execution paths, so receipt bytes need not match when replayed at a new location even when deterministic payload bytes match.
57
+
58
+ ## Verification boundary
59
+
60
+ See `source_verification.json` for line anchors, source hashes and preparation checks. The arithmetic is enforced by source invariants and supported by saved artifacts; this candidate preparation is not a fresh numerical reproduction of the 3,833-parent result, a checkpoint certification, or a claim that later AFFINose splits were all represented in these pretraining-time anchors.
releases/2026-10-02/benchmark_exclusion/SHA256SUMS ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 8452e3ac521af1d3ea24bb5a99e02a930d913fc96394e5610fe8b86e19ea89ec MANIFEST_SHA256.tsv
2
+ 96567c3b9c3e254bceb6bd82e43c1f252b987ced58dad41d3b4406b9f58a76ec README.md
3
+ e2e0dfa7d133362146de10ba3dd774e9a3e186088f976189b0aae9d26b442bc4 code_inventory.tsv
4
+ 050c0af96f133ded3139baf6dfea594f91c6659f411dc25fc06d5847951b2f0c required_successor_inputs.json
5
+ 0507537ca8ee05b249f9ab3f3fa09de94b7acc633b91235f2778ae07659096bd source_verification.json
6
+ 3d598a249db90c928f8a317923ba5d37eebbc1f63383a2c39e0541f20c31ccac writing/review/glytoucan/scripts/build_connection_level_support_audit.py
7
+ a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py
8
+ 0f88f53e22d0c068a87a09508a9d286e4982eddcb4e2154e63b2ca25339ece69 writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py
9
+ 2c8821586df1caa53556b4cc14e6cd7507af4830aa8a149657571e0455eb6853 writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py
10
+ cd86a0a0b228c32e51ec73490711e37da3e6c9fdf9e64e980b7222b5312484aa writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py
11
+ 966889118fa567729e1959750b1601018ba117aed0bfad3508005ba6b1896a08 writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py
releases/2026-10-02/benchmark_exclusion/code_inventory.tsv ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ original_path candidate_path sha256 bytes unchanged
2
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c 36975 True
3
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py 0f88f53e22d0c068a87a09508a9d286e4982eddcb4e2154e63b2ca25339ece69 15043 True
4
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py cd86a0a0b228c32e51ec73490711e37da3e6c9fdf9e64e980b7222b5312484aa 14084 True
5
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py 2c8821586df1caa53556b4cc14e6cd7507af4830aa8a149657571e0455eb6853 38441 True
6
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py 966889118fa567729e1959750b1601018ba117aed0bfad3508005ba6b1896a08 7856 True
7
+ /Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/glytoucan/scripts/build_connection_level_support_audit.py writing/review/glytoucan/scripts/build_connection_level_support_audit.py 3d598a249db90c928f8a317923ba5d37eebbc1f63383a2c39e0541f20c31ccac 21842 True
releases/2026-10-02/benchmark_exclusion/required_successor_inputs.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scope": "Exact source-pinned input list; data hashes are not freshly recomputed here. Strict3 JSON additionally names role CSV files which the builder hashes at runtime. Original dataset payloads are not bundled.",
3
+ "inputs": [
4
+ {
5
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_non_iar_exact_graph_authority_20260711/review_p1_non_iar_anchor_manifest.tsv.gz",
6
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_non_iar_exact_graph_authority_20260711/review_p1_non_iar_anchor_manifest.tsv.gz",
7
+ "expected_sha256_from_successor_source": "048facdfd9a5ff15f1295d28f1d07b4dae2c656ca8714c3ed06df9e284ac199a",
8
+ "locally_exists": true,
9
+ "local_bytes": 113824,
10
+ "payload_copied": false,
11
+ "hash_freshly_checked": false
12
+ },
13
+ {
14
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_non_iar_exact_graph_authority_20260711/review_p1_non_iar_exact_graph_parent_manifest.tsv.gz",
15
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_non_iar_exact_graph_authority_20260711/review_p1_non_iar_exact_graph_parent_manifest.tsv.gz",
16
+ "expected_sha256_from_successor_source": "088971d5dbe61f840a45a67a84df39478edd7a80089fd6528fd9fda8063b26b9",
17
+ "locally_exists": true,
18
+ "local_bytes": 3857629,
19
+ "payload_copied": false,
20
+ "hash_freshly_checked": false
21
+ },
22
+ {
23
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_non_iar_pretraining_partition_80_20_20260712/review_p1_non_iar_pretraining_parent_partition.tsv.gz",
24
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_non_iar_pretraining_partition_80_20_20260712/review_p1_non_iar_pretraining_parent_partition.tsv.gz",
25
+ "expected_sha256_from_successor_source": "7c4ed33a6cbf50df8d3264421c652c480d3a5eff0eace10c3c958e6dac6b2160",
26
+ "locally_exists": true,
27
+ "local_bytes": 12149150,
28
+ "payload_copied": false,
29
+ "hash_freshly_checked": false
30
+ },
31
+ {
32
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_non_iar_pretraining_partition_80_20_20260712/review_p1_non_iar_pretraining_objective_partition.tsv.gz",
33
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_non_iar_pretraining_partition_80_20_20260712/review_p1_non_iar_pretraining_objective_partition.tsv.gz",
34
+ "expected_sha256_from_successor_source": "d09eebb3f7ac47058d2d0cabfd42905f624581bf4c942dceac9cad9c1973e5a0",
35
+ "locally_exists": true,
36
+ "local_bytes": 4238642,
37
+ "payload_copied": false,
38
+ "hash_freshly_checked": false
39
+ },
40
+ {
41
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p0_p1_freeze_20260710/review_p1_wurcs_motif_fingerprint_manifest.tsv.gz",
42
+ "candidate_relative_path": "writing/review/multimodal/outputs/p0_p1_freeze_20260710/review_p1_wurcs_motif_fingerprint_manifest.tsv.gz",
43
+ "expected_sha256_from_successor_source": "69994cbc859146e97867e62669fd9f71b8d02140ecd9ae8056375af608afdcab",
44
+ "locally_exists": true,
45
+ "local_bytes": 167661349,
46
+ "payload_copied": false,
47
+ "hash_freshly_checked": false
48
+ },
49
+ {
50
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p0_p1_freeze_20260710/review_p1_parent_identity_manifest.tsv.gz",
51
+ "candidate_relative_path": "writing/review/multimodal/outputs/p0_p1_freeze_20260710/review_p1_parent_identity_manifest.tsv.gz",
52
+ "expected_sha256_from_successor_source": "55b830d839370c0cec0aab2b61b4721e6ace9ba3ee49e4c93e7be6e486d1c5c4",
53
+ "locally_exists": true,
54
+ "local_bytes": 33418108,
55
+ "payload_copied": false,
56
+ "hash_freshly_checked": false
57
+ },
58
+ {
59
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_role_complete_direct_local_neighbor_audit_20260711/review_p1_direct_local_neighbor_edges.tsv.gz",
60
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_role_complete_direct_local_neighbor_audit_20260711/review_p1_direct_local_neighbor_edges.tsv.gz",
61
+ "expected_sha256_from_successor_source": "e95d8122449f4b91271fd297be41a7912244f2398739e349064fc55b781e3ae0",
62
+ "locally_exists": true,
63
+ "local_bytes": 1457038,
64
+ "payload_copied": false,
65
+ "hash_freshly_checked": false
66
+ },
67
+ {
68
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py",
69
+ "candidate_relative_path": "writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py",
70
+ "expected_sha256_from_successor_source": "a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c",
71
+ "locally_exists": true,
72
+ "local_bytes": 36975,
73
+ "payload_copied": false,
74
+ "hash_freshly_checked": false
75
+ },
76
+ {
77
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_downstream_strict3_authority_20260720_r2/review_a5_downstream_strict3_authority.json",
78
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_downstream_strict3_authority_20260720_r2/review_a5_downstream_strict3_authority.json",
79
+ "expected_sha256_from_successor_source": "c1bbcfe410c0ee78824657da84a4055e58fdf196624d8981e1aa239ae629f1d8",
80
+ "locally_exists": true,
81
+ "local_bytes": 91064,
82
+ "payload_copied": false,
83
+ "hash_freshly_checked": false
84
+ },
85
+ {
86
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_affinose_motif_scaffold_balanced_k20_20260711/review_p1_affinose_balanced_k20_a5_anchors.tsv.gz",
87
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_affinose_motif_scaffold_balanced_k20_20260711/review_p1_affinose_balanced_k20_a5_anchors.tsv.gz",
88
+ "expected_sha256_from_successor_source": "cdeb7b4075f00efc50c0649ac4be11560154e29a6a728c4ddeb54dd6bc26f6c9",
89
+ "locally_exists": true,
90
+ "local_bytes": 8489,
91
+ "payload_copied": false,
92
+ "hash_freshly_checked": false
93
+ },
94
+ {
95
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_nonfit_anchors_20260720.tsv.gz",
96
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_nonfit_anchors_20260720.tsv.gz",
97
+ "expected_sha256_from_successor_source": "85c8f760dba0e11a4346cd07bc7eb09f5f31d6bf1b0407658130da4173f8a5f9",
98
+ "locally_exists": true,
99
+ "local_bytes": 29453,
100
+ "payload_copied": false,
101
+ "hash_freshly_checked": false
102
+ },
103
+ {
104
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_candidate_summary_20260720.json",
105
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_candidate_summary_20260720.json",
106
+ "expected_sha256_from_successor_source": "d1a56f0bb7f77535c619cc2739933bd751cb5d3e3fcd2e8175fc88671e83e6ee",
107
+ "locally_exists": true,
108
+ "local_bytes": 29848,
109
+ "payload_copied": false,
110
+ "hash_freshly_checked": false
111
+ },
112
+ {
113
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_candidate_verification_receipt_20260720.json",
114
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_affinose_strict_glycan_components_20260720_r1/review_a5_affinose_strict_glycan_candidate_verification_receipt_20260720.json",
115
+ "expected_sha256_from_successor_source": "a92f442f2b74e630783b5a0eb6fb16738d6aa4c86f71b28c487cc42622d3eee1",
116
+ "locally_exists": true,
117
+ "local_bytes": 5962,
118
+ "payload_copied": false,
119
+ "hash_freshly_checked": false
120
+ },
121
+ {
122
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/data/review_sequences_a5_non_iar_80_20_parser_v3_failclosed_20260720_r3/review_sequence_parent_index.tsv.gz",
123
+ "candidate_relative_path": "data/review_sequences_a5_non_iar_80_20_parser_v3_failclosed_20260720_r3/review_sequence_parent_index.tsv.gz",
124
+ "expected_sha256_from_successor_source": "c61e766dbffc4a1d88c7c7fb94deb683943c0861403350e79ab09c70b317fa52",
125
+ "locally_exists": true,
126
+ "local_bytes": 17392883,
127
+ "payload_copied": false,
128
+ "hash_freshly_checked": false
129
+ },
130
+ {
131
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_affinose_p6_observation_authority_20260720_r2/review_a5_affinose_p6_failclosed_authority_20260720.json",
132
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_affinose_p6_observation_authority_20260720_r2/review_a5_affinose_p6_failclosed_authority_20260720.json",
133
+ "expected_sha256_from_successor_source": "a77e486e200e50e9becb3ad45f70ef97272b5a339267c636d746af8d9bf2b2b5",
134
+ "locally_exists": true,
135
+ "local_bytes": 22820,
136
+ "payload_copied": false,
137
+ "hash_freshly_checked": false
138
+ },
139
+ {
140
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/review_a5_affinose_p6_observation_authority_20260720_r2/review_a5_affinose_p6_failclosed_receipt_20260720.json",
141
+ "candidate_relative_path": "writing/review/multimodal/outputs/review_a5_affinose_p6_observation_authority_20260720_r2/review_a5_affinose_p6_failclosed_receipt_20260720.json",
142
+ "expected_sha256_from_successor_source": "d6fb00bd5a79d5a1caa9d06f440357202d85bb6cb3f33c7732f063b4721cf23f",
143
+ "locally_exists": true,
144
+ "local_bytes": 2652,
145
+ "payload_copied": false,
146
+ "hash_freshly_checked": false
147
+ },
148
+ {
149
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/P0_P1_non_iar_authority_correction_20260711.md",
150
+ "candidate_relative_path": "writing/review/multimodal/P0_P1_non_iar_authority_correction_20260711.md",
151
+ "expected_sha256_from_successor_source": "f7ba54c4ec66b74e0061ffa75bd963af9589f28a7124e95208099f169657936f",
152
+ "locally_exists": true,
153
+ "local_bytes": 5448,
154
+ "payload_copied": false,
155
+ "hash_freshly_checked": false
156
+ },
157
+ {
158
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/P1_parent_split_refit_contract_20260710.md",
159
+ "candidate_relative_path": "writing/review/multimodal/P1_parent_split_refit_contract_20260710.md",
160
+ "expected_sha256_from_successor_source": "2533d651ccbf6d86111709676bf528d7c805d1df214ada62fd45772dda0f388e",
161
+ "locally_exists": true,
162
+ "local_bytes": 22645,
163
+ "payload_copied": false,
164
+ "hash_freshly_checked": false
165
+ },
166
+ {
167
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/P1_reviewer_complete_generalization_protocol_20260710.md",
168
+ "candidate_relative_path": "writing/review/multimodal/P1_reviewer_complete_generalization_protocol_20260710.md",
169
+ "expected_sha256_from_successor_source": "9556f456490bdd7e03926cc78d9c0a73146386a9b7ac7188a556b1bc23c37b4a",
170
+ "locally_exists": true,
171
+ "local_bytes": 27011,
172
+ "payload_copied": false,
173
+ "hash_freshly_checked": false
174
+ },
175
+ {
176
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/review_a5_interaction_endpoint_adjudication_20260720.md",
177
+ "candidate_relative_path": "writing/review/multimodal/review_a5_interaction_endpoint_adjudication_20260720.md",
178
+ "expected_sha256_from_successor_source": "614a4e2144dda63538a26dcbd8fce0a9d604a97c2b4ad07cf80820407ca29fdb",
179
+ "locally_exists": true,
180
+ "local_bytes": 24284,
181
+ "payload_copied": false,
182
+ "hash_freshly_checked": false
183
+ },
184
+ {
185
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_a5_ms_harmonized_identity_repair_20260720_r1/review_a5_ms_repaired_parent_manifest.tsv.gz",
186
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_a5_ms_harmonized_identity_repair_20260720_r1/review_a5_ms_repaired_parent_manifest.tsv.gz",
187
+ "expected_sha256_from_successor_source": "f4abd4b186f5f4758918b89f19870bc112a7c6be6085f870208235cd03005e26",
188
+ "locally_exists": true,
189
+ "local_bytes": 209281,
190
+ "payload_copied": false,
191
+ "hash_freshly_checked": false
192
+ },
193
+ {
194
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/outputs/p1_a5_static_repair_child_authority_20260720_r1/review_a5_static_repair_child_authority.tsv.gz",
195
+ "candidate_relative_path": "writing/review/multimodal/outputs/p1_a5_static_repair_child_authority_20260720_r1/review_a5_static_repair_child_authority.tsv.gz",
196
+ "expected_sha256_from_successor_source": "2ca981b78bca56928d449d35467671b03db515496ac61162e68f8a1381a3661f",
197
+ "locally_exists": true,
198
+ "local_bytes": 10920113,
199
+ "payload_copied": false,
200
+ "hash_freshly_checked": false
201
+ }
202
+ ]
203
+ }
releases/2026-10-02/benchmark_exclusion/source_verification.json ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "status": "PASS_STATIC_SOURCE_AND_UNCHANGED_CANDIDATE_PREPARATION",
3
+ "checked_utc": "2026-09-23T00:13:39.128129+00:00",
4
+ "source_report": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/BENCHMARK_IDENTITY_EXCLUSION_3833_20260922.md",
5
+ "files": [
6
+ {
7
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py",
8
+ "candidate_path": "writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py",
9
+ "sha256": "a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c",
10
+ "bytes": 36975,
11
+ "unchanged": true,
12
+ "role": "requested_script",
13
+ "present_in_shipped_06_code": false
14
+ },
15
+ {
16
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py",
17
+ "candidate_path": "writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py",
18
+ "sha256": "0f88f53e22d0c068a87a09508a9d286e4982eddcb4e2154e63b2ca25339ece69",
19
+ "bytes": 15043,
20
+ "unchanged": true,
21
+ "role": "requested_script",
22
+ "present_in_shipped_06_code": false
23
+ },
24
+ {
25
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py",
26
+ "candidate_path": "writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py",
27
+ "sha256": "cd86a0a0b228c32e51ec73490711e37da3e6c9fdf9e64e980b7222b5312484aa",
28
+ "bytes": 14084,
29
+ "unchanged": true,
30
+ "role": "requested_script",
31
+ "present_in_shipped_06_code": false
32
+ },
33
+ {
34
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py",
35
+ "candidate_path": "writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py",
36
+ "sha256": "2c8821586df1caa53556b4cc14e6cd7507af4830aa8a149657571e0455eb6853",
37
+ "bytes": 38441,
38
+ "unchanged": true,
39
+ "role": "requested_script",
40
+ "present_in_shipped_06_code": false
41
+ },
42
+ {
43
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py",
44
+ "candidate_path": "writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py",
45
+ "sha256": "966889118fa567729e1959750b1601018ba117aed0bfad3508005ba6b1896a08",
46
+ "bytes": 7856,
47
+ "unchanged": true,
48
+ "role": "requested_script",
49
+ "present_in_shipped_06_code": false
50
+ },
51
+ {
52
+ "original_path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/glytoucan/scripts/build_connection_level_support_audit.py",
53
+ "candidate_path": "writing/review/glytoucan/scripts/build_connection_level_support_audit.py",
54
+ "sha256": "3d598a249db90c928f8a317923ba5d37eebbc1f63383a2c39e0541f20c31ccac",
55
+ "bytes": 21842,
56
+ "unchanged": true,
57
+ "role": "local_import_helper",
58
+ "present_in_shipped_06_code": false
59
+ }
60
+ ],
61
+ "python_for_syntax_check": "3.12.14 (main, Aug 25 2026, 13:50:33) [Clang 22.1.3 ]",
62
+ "local_dependency_metadata": {
63
+ "networkx": "unavailable in this interpreter",
64
+ "pandas": "2.2.3"
65
+ },
66
+ "checks": {
67
+ "six_python_files_ast_parse": true,
68
+ "all_copies_byte_identical": true,
69
+ "all_five_requested_scripts_absent_from_shipped_code": true,
70
+ "no_science_pipeline_run": true,
71
+ "shipped_code_and_zip_untouched": true,
72
+ "cli_help_checks": [
73
+ {
74
+ "file": "writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py",
75
+ "command": "python -B FILE --help",
76
+ "returncode": 0,
77
+ "output": "usage: build_p1_non_iar_exact_graph_authority_20260711.py [-h]\n [--anchor-manifest ANCHOR_MANIFEST]\n [--graph-edges GRAPH_EDGES]\n [--parent-manifest PARENT_MANIFEST]\n [--output-dir OUTPUT_DIR]\n\noptions:\n -h, --help show this help message and exit\n --anchor-manifest ANCHOR_MANIFEST\n --graph-edges GRAPH_EDGES\n --parent-manifest PARENT_MANIFEST\n --output-dir OUTPUT_DIR\n",
78
+ "stderr": ""
79
+ },
80
+ {
81
+ "file": "writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py",
82
+ "command": "python -B FILE --help",
83
+ "returncode": 0,
84
+ "output": "usage: repartition_a5_non_iar_80_20_20260712.py [-h]\n [--source-parent SOURCE_PARENT]\n [--source-objective SOURCE_OBJECTIVE]\n [--fingerprints FINGERPRINTS]\n [--identity IDENTITY]\n [--output-directory OUTPUT_DIRECTORY]\n\noptions:\n -h, --help show this help message and exit\n --source-parent SOURCE_PARENT\n --source-objective SOURCE_OBJECTIVE\n --fingerprints FINGERPRINTS\n --identity IDENTITY\n --output-directory OUTPUT_DIRECTORY\n",
85
+ "stderr": ""
86
+ },
87
+ {
88
+ "file": "writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py",
89
+ "command": "python -B FILE --help",
90
+ "returncode": 0,
91
+ "output": "usage: build_review_a5_non_iar_later_role_successor_20260720.py\n [-h] [--output-directory OUTPUT_DIRECTORY]\n\noptions:\n -h, --help show this help message and exit\n --output-directory OUTPUT_DIRECTORY\n",
92
+ "stderr": ""
93
+ }
94
+ ]
95
+ },
96
+ "findings": [
97
+ "Graph retrieval uses WL on SHA256 residue-label strings and BOND labels with edge endpoint positions, 3 iterations, digest_size32; exact isomorphism confirms original label and position equality.",
98
+ "Graph representation is a bipartite residue-bond tree; only fixed_tree_exact_linkage anchors/parents enter graph checks.",
99
+ "The fixed-tree tier does not require completely specified residue stereochemistry: prefer connected residue trees with fully specified linkages.",
100
+ "Authority includes exact string OR graph identity only; near neighbours and shared motifs are not exclusion criteria.",
101
+ "IAR-only GlyCosmos and grey-seal memberships are dropped; a glycan with another active benchmark role remains an anchor.",
102
+ "Successor checks93 added anchors,73 additional exact parents,0 additional graph-only aliases,59 train+14 dev moves,final3833 exclusions; all other roles are unchanged.",
103
+ "Fifth named script is under multimodal/tests/, not multimodal/scripts/."
104
+ ],
105
+ "limits": [
106
+ "Counts are checked as source-enforced invariants and previously saved evidence; no new full data recount or science pipeline run.",
107
+ "Frozen upstream input payloads and parent-partition precursor are required; this is not a self-contained dataset release.",
108
+ "NetworkX/pandas historical version pins are not established by these scripts.",
109
+ "The full payload-based successor tests were not rerun; they reopen large existing manifests."
110
+ ],
111
+ "line_evidence": {
112
+ "graph_parser_WL_and_isomorphism": "build_p1_direct_local_neighbor_audit_20260710.py:47-49,111-197,449-452,517-520,576-588,695-734",
113
+ "IAR_source_filter_and_identity_OR": "build_p1_non_iar_exact_graph_authority_20260711.py:55-59,119-151,226-244",
114
+ "whole_block_split": "repartition_a5_non_iar_80_20_20260712.py:118-150,220-233",
115
+ "successor_anchor_and_role_rules": "build_review_a5_non_iar_later_role_successor_20260720.py:294-298,338-359,383-438,447-484",
116
+ "tier_definition_source_not_copied": "multimodal/scripts/build_p1_wurcs_motif_fingerprints_20260710.py:147-213"
117
+ },
118
+ "tier_definition_source": {
119
+ "path": "/Users/supantha/Documents/esm-g/plan/bert_training_v3/v3.1_cluster_training/writing/review/multimodal/scripts/build_p1_wurcs_motif_fingerprints_20260710.py",
120
+ "sha256": "b0d2ca8d60722f144d988e59e3281017879890f88f50a3ba93d603f05236401f",
121
+ "copied": false,
122
+ "reason": "Source of prerequisite fingerprint tier, not an imported runtime dependency."
123
+ }
124
+ }
releases/2026-10-02/benchmark_exclusion/writing/review/glytoucan/scripts/build_connection_level_support_audit.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Audit linkage, edge, and modification support patterns in GlyCorpus rows.
3
+
4
+ This is a curation audit, not a full WURCS chemical validator. It extracts
5
+ repeatable WURCS/IUPAC connection signatures, assigns conservative support
6
+ tiers, and summarizes unique patterns for review.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import gzip
13
+ import json
14
+ import re
15
+ from collections import Counter
16
+ from pathlib import Path
17
+
18
+ import pandas as pd
19
+
20
+
21
+ ROOT = Path.cwd()
22
+ EXCLUDED_INPUT = (
23
+ ROOT
24
+ / "review/glytoucan/outputs/family_publication_context_addendum_20260702/family_context_support_by_row.tsv"
25
+ )
26
+ FULL_INPUT = (
27
+ ROOT
28
+ / "review/glytoucan/outputs/final_training_recommendation_v2_20260702/glycorpus_final_training_recommendation_v2.tsv.gz"
29
+ )
30
+ OUTDIR = ROOT / "review/glytoucan/outputs/connection_level_support_audit_20260702"
31
+
32
+ IUPAC_LINK_RE = re.compile(r"(?P<residue>[A-Za-z?][A-Za-z0-9?/-]*)(?P<link>\([ab?][0-9?]-[0-9?/]+[ab?]?\))")
33
+ EDGE_RE = re.compile(r"(?P<n1>[A-Za-z]+)(?P<p1>[0-9?]+)-(?P<n2>[A-Za-z]+)(?P<p2>[0-9?]+)")
34
+ RESDEF_RE = re.compile(r"\[([^\]]*)\]")
35
+
36
+ COMMON_ANIMAL_NEU5AC = {
37
+ "Neu5Ac(a2-3)",
38
+ "Neu5Ac(a2-6)",
39
+ "Neu5Ac(a2-8)",
40
+ "Neu5Ac(a2-9)",
41
+ }
42
+ COMMON_ALPHA_SIALIC_ACCEPTORS = {"3", "6", "8", "9"}
43
+
44
+
45
+ def node_label(index: int) -> str:
46
+ """Return WURCS-style node labels for the first several hundred nodes."""
47
+ letters = "abcdefghijklmnopqrstuvwxyz"
48
+ index -= 1
49
+ label = ""
50
+ while True:
51
+ label = letters[index % 26] + label
52
+ index = index // 26 - 1
53
+ if index < 0:
54
+ return label
55
+
56
+
57
+ def split_wurcs_sections(body: str) -> list[str]:
58
+ sections = []
59
+ current = []
60
+ bracket_depth = 0
61
+ for char in body:
62
+ if char == "[":
63
+ bracket_depth += 1
64
+ elif char == "]" and bracket_depth:
65
+ bracket_depth -= 1
66
+ if char == "/" and bracket_depth == 0:
67
+ sections.append("".join(current))
68
+ current = []
69
+ else:
70
+ current.append(char)
71
+ sections.append("".join(current))
72
+ return sections
73
+
74
+
75
+ def split_wurcs(wurcs: str) -> tuple[list[str], dict[str, str], str]:
76
+ if not wurcs.startswith("WURCS=2.0/"):
77
+ return [], {}, ""
78
+ body = wurcs[len("WURCS=2.0/") :]
79
+ parts = split_wurcs_sections(body)
80
+ if len(parts) < 4:
81
+ return [], {}, ""
82
+ resdefs = RESDEF_RE.findall(parts[1])
83
+ seq = parts[2].split("-") if parts[2] else []
84
+ node_to_resdef = {}
85
+ for i, value in enumerate(seq, start=1):
86
+ label = node_label(i)
87
+ try:
88
+ resdef = resdefs[int(value) - 1]
89
+ except (ValueError, IndexError):
90
+ resdef = ""
91
+ node_to_resdef[label] = resdef
92
+ link_section = "/".join(parts[3:])
93
+ return resdefs, node_to_resdef, link_section
94
+
95
+
96
+ def residue_class_from_resdef(resdef: str) -> str:
97
+ low = resdef.lower()
98
+ if not resdef:
99
+ return "unknown_residue"
100
+ if "aad" in low or "neu" in low or "kdn" in low or "kdo" in low:
101
+ return "nonulosonic_like"
102
+ if low.startswith("ha"):
103
+ return "ketose_fructose_like"
104
+ if re.search(r"_\d+\*opo|opo/", low):
105
+ return "phosphate_modified"
106
+ if re.search(r"_\d+\*oso|oso/", low):
107
+ return "sulfate_modified"
108
+ if re.search(r"c{6,}|nccc|occc", low):
109
+ return "long_substituent_or_lipid_like"
110
+ if "_2*n" in low or "_5*n" in low or "ncc" in low:
111
+ return "amino_acetamido_modified"
112
+ if "x" in low or low.startswith("u"):
113
+ return "ambiguous_or_unknown"
114
+ if low.startswith("a") or low.startswith("h"):
115
+ return "monosaccharide_like"
116
+ return "other_residue"
117
+
118
+
119
+ def modification_classes(text: str) -> list[str]:
120
+ classes = []
121
+ low = text.lower()
122
+ if "opo" in low or re.search(r"\bp-", low) or "phosphate" in low:
123
+ classes.append("phosphate")
124
+ if "oso" in low or "sulf" in low:
125
+ classes.append("sulfate")
126
+ if re.search(r"c{6,}|nccc|occc|cer|lipid", low):
127
+ classes.append("long_lipid_like")
128
+ if "ncc/3=o" in low or "_2*n" in low or "_5*n" in low:
129
+ classes.append("amino_acetamido")
130
+ if "_6*oc" in low or "me(" in low or "methyl" in low:
131
+ classes.append("methyl_or_o_acetyl_like")
132
+ if "?" in text or "[" in low and ("x" in low or "[u" in low):
133
+ classes.append("unknown_or_ambiguous")
134
+ return sorted(set(classes))
135
+
136
+
137
+ def iupac_linkages(iupac: str) -> list[str]:
138
+ links = []
139
+ for match in IUPAC_LINK_RE.finditer(iupac or ""):
140
+ residue = match.group("residue")
141
+ link = match.group("link")
142
+ links.append(f"{residue}{link}")
143
+ return links
144
+
145
+
146
+ def parse_iupac_link(link: str) -> tuple[str, str, str, str] | None:
147
+ match = re.match(
148
+ r"(?P<residue>[A-Za-z?][A-Za-z0-9?/-]*)\((?P<anomer>[ab?])(?P<donor>[0-9?]+)-(?P<acceptor>[0-9?/]+)(?P<acceptor_anomer>[ab?]?)\)",
149
+ link,
150
+ )
151
+ if not match:
152
+ return None
153
+ return (
154
+ match.group("residue"),
155
+ match.group("anomer"),
156
+ match.group("donor"),
157
+ match.group("acceptor"),
158
+ )
159
+
160
+
161
+ def sialic_link_context(iupac_links: list[str]) -> tuple[list[str], list[str]]:
162
+ unusual = []
163
+ uncertain = []
164
+ for link in iupac_links:
165
+ parsed = parse_iupac_link(link)
166
+ if not parsed:
167
+ continue
168
+ residue, anomer, donor, acceptor = parsed
169
+ if not (residue.startswith("Neu") or residue.startswith("Kdn")):
170
+ continue
171
+ if residue.startswith("Neu5Ac") or residue.startswith("Neu5Gc") or residue == "Neu":
172
+ if "?" in link:
173
+ uncertain.append(link)
174
+ elif "/" in acceptor:
175
+ uncertain.append(link)
176
+ elif anomer != "a":
177
+ unusual.append(link)
178
+ elif donor == "2" and acceptor not in COMMON_ALPHA_SIALIC_ACCEPTORS:
179
+ unusual.append(link)
180
+ return unusual, uncertain
181
+
182
+
183
+ def edge_records(wurcs: str) -> tuple[list[dict], list[str], list[str]]:
184
+ resdefs, node_to_resdef, link_section = split_wurcs(wurcs)
185
+ edges = []
186
+ signatures = []
187
+ flags = []
188
+ for token in link_section.split("_") if link_section else []:
189
+ match = EDGE_RE.search(token)
190
+ if not match:
191
+ if token:
192
+ flags.append("unparsed_wurcs_edge_token")
193
+ continue
194
+ n1, p1, n2, p2 = match.group("n1"), match.group("p1"), match.group("n2"), match.group("p2")
195
+ r1 = residue_class_from_resdef(node_to_resdef.get(n1, ""))
196
+ r2 = residue_class_from_resdef(node_to_resdef.get(n2, ""))
197
+ mods = modification_classes(token)
198
+ edge = {
199
+ "edge_token": token,
200
+ "endpoint_position_pair": f"{p1}-{p2}",
201
+ "endpoint_class_pair": f"{r1}--{r2}",
202
+ "edge_signature": f"{r1}:{p1}-{r2}:{p2}",
203
+ "edge_modification_classes": "|".join(mods),
204
+ }
205
+ edges.append(edge)
206
+ signatures.append(edge["edge_signature"])
207
+ pset = {p1, p2}
208
+ if p1 == "1" and p2 == "1":
209
+ flags.append("both_anomeric_1_1_edge")
210
+ if "2" in pset and (r1 == "nonulosonic_like" or r2 == "nonulosonic_like"):
211
+ flags.append("nonulosonic_position_2_edge")
212
+ if "2" in pset and (r1 == "ketose_fructose_like" or r2 == "ketose_fructose_like"):
213
+ flags.append("ketose_fructose_position_2_edge")
214
+ if "phosphate" in mods:
215
+ flags.append("phosphate_edge_or_substituent")
216
+ if "sulfate" in mods:
217
+ flags.append("sulfate_edge_or_substituent")
218
+ if "long_lipid_like" in mods or r1 == "long_substituent_or_lipid_like" or r2 == "long_substituent_or_lipid_like":
219
+ flags.append("long_lipid_like_edge_or_substituent")
220
+ if "?" in p1 + p2 + token:
221
+ flags.append("unknown_edge_position_or_substituent")
222
+ for resdef in resdefs:
223
+ mods = modification_classes(resdef)
224
+ for mod in mods:
225
+ if mod == "phosphate":
226
+ flags.append("phosphate_residue_modification")
227
+ elif mod == "sulfate":
228
+ flags.append("sulfate_residue_modification")
229
+ elif mod == "long_lipid_like":
230
+ flags.append("long_lipid_like_residue_modification")
231
+ elif mod == "unknown_or_ambiguous":
232
+ flags.append("unknown_residue_or_modification")
233
+ return edges, sorted(set(signatures)), sorted(set(flags))
234
+
235
+
236
+ def connection_support(row: pd.Series, iupac_links: list[str], edge_flags: list[str], edge_signatures: list[str]) -> tuple[str, str, str]:
237
+ iupac_set = set(iupac_links)
238
+ unusual_sialic_links, uncertain_sialic_links = sialic_link_context(iupac_links)
239
+ proposed_use = row.get("proposed_training_use", "")
240
+ confidence = row.get("family_assignment_confidence", "")
241
+ family = row.get("primary_context_family", "")
242
+ support_level = row.get("support_level", "")
243
+ broad_bucket = row.get("broad_training_bucket_v2", "")
244
+ exact_csdb = str(row.get("exact_csdb_wurcs_match", row.get("csdb_exact_wurcs_rescue", ""))).lower() in {"true", "yes"}
245
+
246
+ if (
247
+ row.get("confirmed_error_flag", "") == "yes"
248
+ or proposed_use == "exclude_from_all_clean_training"
249
+ or broad_bucket == "hard_exclude"
250
+ or support_level == "confirmed_or_near_confirmed_error"
251
+ ):
252
+ return (
253
+ "specific_connection_entry_concern",
254
+ "exclude_from_clean_training",
255
+ "Specific reviewer-like connection concern, usually explicit Neu5Ac(a2-2) or related C2 pattern.",
256
+ )
257
+ if exact_csdb:
258
+ return (
259
+ "exact_record_external_support",
260
+ "eligible_for_exact_or_primary_context_depending_on_claim",
261
+ "Exact CSDB-WURCS support is present; still separate animal/exact-claim context as needed.",
262
+ )
263
+ if "Neu5Ac(a2-2)" in iupac_set:
264
+ return (
265
+ "specific_connection_unresolved",
266
+ "manual_recheck_or_exclude_from_clean_training",
267
+ "Explicit Neu5Ac(a2-2) connection remains unresolved without exact source support.",
268
+ )
269
+ if unusual_sialic_links:
270
+ return (
271
+ "specific_connection_unresolved",
272
+ "manual_recheck_before_broad_inclusion",
273
+ f"Unusual sialic-acid linkage token(s): {'|'.join(unusual_sialic_links[:5])}.",
274
+ )
275
+ if uncertain_sialic_links:
276
+ return (
277
+ "connection_unresolved_or_ambiguous",
278
+ "exclude_from_exact_topology_keep_only_labeled_context",
279
+ f"Uncertain sialic-acid linkage token(s): {'|'.join(uncertain_sialic_links[:5])}.",
280
+ )
281
+ if family == "nonulosonic_kdo_neu5ac_kdn" and "nonulosonic_position_2_edge" in edge_flags:
282
+ return (
283
+ "context_supported_nonulosonic_connection",
284
+ "broad_wurcs_or_sensitivity_not_exact_biology",
285
+ "Kdo/Kdn/nonulosonic C2-link chemistry is supported in bacterial/LPS contexts; exact row remains unproven.",
286
+ )
287
+ if "both_anomeric_1_1_edge" in edge_flags:
288
+ if any(re.match(r"(?:[LD]-)?(?:\\?-)?Glc\([ab?]1-1[ab?]\)", link) for link in iupac_set):
289
+ return (
290
+ "connection_pattern_supported",
291
+ "broad_wurcs_or_sensitivity_not_exact_biology",
292
+ "Both-anomeric 1-1 chemistry has trehalose-like support, but exact row still lacks exact-record evidence.",
293
+ )
294
+ return (
295
+ "connection_motif_supported_low_confidence",
296
+ "sensitivity_only_unless_manually_reviewed",
297
+ "WURCS 1-1 motif maps broadly to known chemistry, but row-level connection context is weak.",
298
+ )
299
+ if "ketose_fructose_position_2_edge" in edge_flags:
300
+ if any(re.search(r"Fru|Fruf", link) for link in iupac_links):
301
+ return (
302
+ "connection_pattern_supported",
303
+ "broad_wurcs_or_sensitivity_not_exact_biology",
304
+ "Fructan/ketose 2-x chemistry has literature support; exact row remains unverified.",
305
+ )
306
+ return (
307
+ "connection_motif_supported_medium_confidence",
308
+ "broad_wurcs_sensitivity_not_exact_biology",
309
+ "WURCS ketose/fructose-like 2-x motif has family support; explicit row context is limited.",
310
+ )
311
+ if "phosphate_edge_or_substituent" in edge_flags or "phosphate_residue_modification" in edge_flags:
312
+ return (
313
+ "connection_or_modification_pattern_supported",
314
+ "broad_wurcs_or_sensitivity_not_exact_biology",
315
+ "Ribitol/phosphate wall-teichoic-like chemistry is supported; exact edge placement is not per-row proven.",
316
+ )
317
+ if "sulfate_edge_or_substituent" in edge_flags or "sulfate_residue_modification" in edge_flags:
318
+ return (
319
+ "connection_or_modification_pattern_supported",
320
+ "broad_wurcs_or_sensitivity_not_exact_biology",
321
+ "Sulfated glycan chemistry is supported; exact modification placement is not per-row proven.",
322
+ )
323
+ if "long_lipid_like_edge_or_substituent" in edge_flags or "long_lipid_like_residue_modification" in edge_flags:
324
+ return (
325
+ "connection_or_modification_motif_supported_low_confidence",
326
+ "sensitivity_only_unless_manually_reviewed",
327
+ "Glycolipid/lipooligosaccharide chemistry exists, but this motif-level assignment needs manual review for exact use.",
328
+ )
329
+ if "unknown_edge_position_or_substituent" in edge_flags or "unknown_residue_or_modification" in edge_flags:
330
+ return (
331
+ "connection_unresolved_or_ambiguous",
332
+ "exclude_from_exact_topology_keep_only_labeled_context",
333
+ "Unknown or ambiguous linkage/residue/modification cannot support exact topology supervision.",
334
+ )
335
+ if confidence in {"high", "medium"} and proposed_use.startswith("include"):
336
+ return (
337
+ "family_supported_no_specific_connection_alert",
338
+ "broad_wurcs_not_exact_biology",
339
+ "No additional high-priority connection alert was found by this audit; support remains family/pattern-level.",
340
+ )
341
+ if broad_bucket in {"broad_core", "broad_citation_backed", "csdb_supported_broad_only_rescue"}:
342
+ return (
343
+ "no_specific_connection_alert_found",
344
+ "keep_existing_training_recommendation",
345
+ "The connection/modification audit did not find a reviewer-like linkage alert in this retained row.",
346
+ )
347
+ if broad_bucket in {"broad_labeled_not_exact", "broad_labeled_context"}:
348
+ return (
349
+ "labeled_record_no_specific_connection_alert",
350
+ "keep_as_labeled_database_record_not_exact_topology",
351
+ "No reviewer-like linkage alert was found, but the row still carries non-exact or context labels.",
352
+ )
353
+ if broad_bucket == "sensitivity_or_quarantine":
354
+ return (
355
+ "quarantine_record_no_specific_connection_alert",
356
+ "sensitivity_only_or_release_with_label",
357
+ "No single reviewer-like linkage alert was found, but the row remains outside clean training for existing curation reasons.",
358
+ )
359
+ return (
360
+ "connection_support_not_assigned",
361
+ "manual_or_release_only",
362
+ "No supported connection pattern was assigned in this audit.",
363
+ )
364
+
365
+
366
+ def audit_rows(df: pd.DataFrame, scope: str) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
367
+ records = []
368
+ edge_rows = []
369
+ for idx, row in df.fillna("").iterrows():
370
+ wurcs = row.get("canonical_wurcs", "")
371
+ iupac = row.get("iupac_name", "")
372
+ edges, signatures, flags = edge_records(wurcs)
373
+ links = iupac_linkages(iupac)
374
+ status, action, rationale = connection_support(row, links, flags, signatures)
375
+ glycorpus_id = row.get("glycorpus_id", "")
376
+ accession = row.get("primary_accession", "")
377
+ records.append(
378
+ {
379
+ "scope": scope,
380
+ "glycorpus_id": glycorpus_id,
381
+ "primary_accession": accession,
382
+ "iupac_linkages": "|".join(links),
383
+ "wurcs_edge_signatures": "|".join(signatures),
384
+ "connection_flags": "|".join(flags),
385
+ "connection_support_status": status,
386
+ "connection_training_action": action,
387
+ "connection_support_rationale": rationale,
388
+ "edge_count_extracted": len(edges),
389
+ "family_assignment_confidence": row.get("family_assignment_confidence", ""),
390
+ "primary_context_family": row.get("primary_context_family", ""),
391
+ "proposed_training_use": row.get("proposed_training_use", ""),
392
+ "exclusion_category": row.get("exclusion_category", ""),
393
+ "support_level": row.get("support_level", ""),
394
+ "broad_training_bucket_v2": row.get("broad_training_bucket_v2", ""),
395
+ "canonical_wurcs": wurcs,
396
+ "iupac_name": iupac,
397
+ }
398
+ )
399
+ for edge in edges:
400
+ edge_rows.append(
401
+ {
402
+ "scope": scope,
403
+ "glycorpus_id": glycorpus_id,
404
+ "primary_accession": accession,
405
+ **edge,
406
+ }
407
+ )
408
+ row_df = pd.DataFrame(records)
409
+ edge_df = pd.DataFrame(edge_rows)
410
+ if edge_df.empty:
411
+ pattern_df = pd.DataFrame(columns=["edge_signature", "rows", "examples"])
412
+ else:
413
+ pattern_df = (
414
+ edge_df.groupby(["edge_signature", "endpoint_position_pair", "endpoint_class_pair"], dropna=False)
415
+ .agg(
416
+ edge_instances=("glycorpus_id", "size"),
417
+ rows=("glycorpus_id", "nunique"),
418
+ examples=("primary_accession", lambda s: ";".join(list(dict.fromkeys(s.astype(str)))[:8])),
419
+ )
420
+ .reset_index()
421
+ .sort_values(["rows", "edge_instances"], ascending=False)
422
+ )
423
+ return row_df, edge_df, pattern_df
424
+
425
+
426
+ def write_table(path: Path, df: pd.DataFrame) -> None:
427
+ path.parent.mkdir(parents=True, exist_ok=True)
428
+ if path.suffix == ".gz":
429
+ with gzip.open(path, "wt") as handle:
430
+ df.to_csv(handle, sep="\t", index=False, lineterminator="\n")
431
+ else:
432
+ df.to_csv(path, sep="\t", index=False, lineterminator="\n")
433
+
434
+
435
+ def main() -> int:
436
+ parser = argparse.ArgumentParser()
437
+ parser.add_argument("--skip-full", action="store_true")
438
+ args = parser.parse_args()
439
+ OUTDIR.mkdir(parents=True, exist_ok=True)
440
+
441
+ excluded = pd.read_csv(EXCLUDED_INPUT, sep="\t", dtype=str).fillna("")
442
+ excluded_row, excluded_edge, excluded_pattern = audit_rows(excluded, "excluded_primary_3819")
443
+ write_table(OUTDIR / "connection_row_audit_excluded_primary_3819.tsv", excluded_row)
444
+ write_table(OUTDIR / "connection_edge_instances_excluded_primary_3819.tsv", excluded_edge)
445
+ write_table(OUTDIR / "connection_pattern_summary_excluded_primary_3819.tsv", excluded_pattern)
446
+
447
+ support_summary = (
448
+ excluded_row.groupby(["connection_support_status", "connection_training_action"], dropna=False)
449
+ .size()
450
+ .reset_index(name="rows")
451
+ .sort_values("rows", ascending=False)
452
+ )
453
+ write_table(OUTDIR / "connection_support_summary_excluded_primary_3819.tsv", support_summary)
454
+
455
+ flag_counter: Counter[str] = Counter()
456
+ for flags in excluded_row["connection_flags"]:
457
+ for flag in str(flags).split("|"):
458
+ if flag:
459
+ flag_counter[flag] += 1
460
+ flag_summary = pd.DataFrame(
461
+ [{"connection_flag": flag, "rows": rows} for flag, rows in flag_counter.most_common()]
462
+ )
463
+ write_table(OUTDIR / "connection_flag_summary_excluded_primary_3819.tsv", flag_summary)
464
+
465
+ meta = {
466
+ "excluded_primary_rows": int(len(excluded_row)),
467
+ "excluded_primary_unique_edge_signatures": int(excluded_pattern["edge_signature"].nunique()),
468
+ "excluded_primary_support_counts": (
469
+ excluded_row.groupby("connection_support_status", dropna=False)
470
+ .size()
471
+ .sort_values(ascending=False)
472
+ .to_dict()
473
+ ),
474
+ }
475
+
476
+ if not args.skip_full:
477
+ full = pd.read_csv(FULL_INPUT, sep="\t", dtype=str).fillna("")
478
+ full_row, full_edge, full_pattern = audit_rows(full, "full_final_recommendation_260703")
479
+ # Full row audit is useful but compacted because it is large.
480
+ full_cols = [
481
+ "scope",
482
+ "glycorpus_id",
483
+ "primary_accession",
484
+ "iupac_linkages",
485
+ "wurcs_edge_signatures",
486
+ "connection_flags",
487
+ "connection_support_status",
488
+ "connection_training_action",
489
+ "edge_count_extracted",
490
+ "support_level",
491
+ "broad_training_bucket_v2",
492
+ ]
493
+ write_table(OUTDIR / "connection_row_audit_full_260703.tsv.gz", full_row[full_cols])
494
+ write_table(OUTDIR / "connection_pattern_summary_full_260703.tsv", full_pattern)
495
+ full_support = (
496
+ full_row.groupby(["connection_support_status", "connection_training_action"], dropna=False)
497
+ .size()
498
+ .reset_index(name="rows")
499
+ .sort_values("rows", ascending=False)
500
+ )
501
+ write_table(OUTDIR / "connection_support_summary_full_260703.tsv", full_support)
502
+ meta.update(
503
+ {
504
+ "full_rows": int(len(full_row)),
505
+ "full_unique_edge_signatures": int(full_pattern["edge_signature"].nunique()),
506
+ "full_support_counts": (
507
+ full_row.groupby("connection_support_status", dropna=False)
508
+ .size()
509
+ .sort_values(ascending=False)
510
+ .to_dict()
511
+ ),
512
+ }
513
+ )
514
+
515
+ (OUTDIR / "connection_level_audit_summary.json").write_text(json.dumps(meta, indent=2))
516
+ print(json.dumps(meta, indent=2))
517
+ return 0
518
+
519
+
520
+ if __name__ == "__main__":
521
+ raise SystemExit(main())
releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_p1_direct_local_neighbor_audit_20260710.py ADDED
@@ -0,0 +1,969 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build the provisional exact/direct-local-neighbour exclusion audit.
3
+
4
+ This script is label blind. Hashes retrieve candidate graph pairs, but every
5
+ reported relation is confirmed with exact labelled graph isomorphism. It does
6
+ not authorize A5 training; final motif-cluster and AFFINose roles are still
7
+ open, and non-fixed WURCS tiers receive exact-string handling only.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import csv
14
+ import gzip
15
+ import hashlib
16
+ import io
17
+ import json
18
+ import sys
19
+ from collections import Counter, defaultdict
20
+ from contextlib import contextmanager
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Iterable, Iterator
24
+
25
+ import networkx as nx
26
+
27
+
28
+ WRITING_ROOT = Path(__file__).resolve().parents[3]
29
+ MM = WRITING_ROOT / "review/multimodal"
30
+ FREEZE = MM / "outputs/p0_p1_freeze_20260710"
31
+ FINGERPRINT = FREEZE / "review_p1_wurcs_motif_fingerprint_manifest.tsv.gz"
32
+ OBJECTIVE = FREEZE / "review_p1_objective_eligibility_manifest.tsv.gz"
33
+ ANCHORS = (
34
+ MM
35
+ / "outputs/p1_targeted_similarity_retention_role_aware_20260710/"
36
+ "review_p1_targeted_anchor_manifest.tsv.gz"
37
+ )
38
+ DEFAULT_OUTPUT_DIR = MM / "outputs/p1_direct_local_neighbor_audit_20260710"
39
+
40
+ PARSER_SOURCE = (
41
+ WRITING_ROOT / "review/glytoucan/scripts/build_connection_level_support_audit.py"
42
+ )
43
+ sys.path.insert(0, str(PARSER_SOURCE.parent))
44
+ from build_connection_level_support_audit import EDGE_RE, split_wurcs # noqa: E402
45
+
46
+
47
+ NODE_MATCH = nx.algorithms.isomorphism.categorical_node_match("label", "")
48
+ EDGE_MATCH = nx.algorithms.isomorphism.categorical_edge_match("position", "")
49
+ FIXED_TIER = "fixed_tree_exact_linkage"
50
+ RELATION_ORDER = {
51
+ "exact_wurcs": 0,
52
+ "graph_identity": 1,
53
+ "residue_substitution": 2,
54
+ "linkage_change": 3,
55
+ "terminal_insertion_or_deletion": 4,
56
+ "branch_attachment_relocation": 5,
57
+ }
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class AnchorGraph:
62
+ index: int
63
+ wurcs: str
64
+ sources: str
65
+ tier: str
66
+ residue_count: int
67
+ graph: nx.Graph
68
+ exact_signature: str
69
+
70
+
71
+ def sha256(path: Path) -> str:
72
+ digest = hashlib.sha256()
73
+ with path.open("rb") as handle:
74
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
75
+ digest.update(chunk)
76
+ return digest.hexdigest()
77
+
78
+
79
+ def hash_text(value: str) -> str:
80
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
81
+
82
+
83
+ def as_bool(value: object) -> bool:
84
+ return str(value).strip().lower() in {"1", "true", "yes"}
85
+
86
+
87
+ @contextmanager
88
+ def gzip_tsv_writer(
89
+ path: Path, fieldnames: list[str]
90
+ ) -> Iterator[csv.DictWriter]:
91
+ with path.open("wb") as raw:
92
+ with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as zipped:
93
+ with io.TextIOWrapper(zipped, encoding="utf-8", newline="") as text:
94
+ writer = csv.DictWriter(
95
+ text,
96
+ fieldnames=fieldnames,
97
+ delimiter="\t",
98
+ lineterminator="\n",
99
+ )
100
+ writer.writeheader()
101
+ yield writer
102
+
103
+
104
+ def write_gzip_tsv(
105
+ path: Path, fieldnames: list[str], rows: Iterable[dict[str, object]]
106
+ ) -> None:
107
+ with gzip_tsv_writer(path, fieldnames) as writer:
108
+ writer.writerows(rows)
109
+
110
+
111
+ def parse_fixed_tree(wurcs: str) -> nx.Graph:
112
+ """Parse a simple exact-linkage WURCS tree into a residue-bond graph."""
113
+ _, node_to_resdef, link_section = split_wurcs(wurcs)
114
+ if not node_to_resdef or any(not value for value in node_to_resdef.values()):
115
+ raise ValueError("missing_or_unmapped_residue")
116
+
117
+ graph = nx.Graph()
118
+ for node_id, residue in node_to_resdef.items():
119
+ graph.add_node(
120
+ ("residue", node_id),
121
+ kind="residue",
122
+ label=residue,
123
+ signature_label=hash_text(residue),
124
+ )
125
+
126
+ tokens = [token for token in link_section.split("_") if token]
127
+ for edge_index, token in enumerate(tokens):
128
+ match = EDGE_RE.fullmatch(token)
129
+ if not match:
130
+ raise ValueError(f"non_simple_edge:{token}")
131
+ left = ("residue", match.group("n1"))
132
+ right = ("residue", match.group("n2"))
133
+ if left not in graph or right not in graph:
134
+ raise ValueError(f"edge_references_unknown_residue:{token}")
135
+ left_position = match.group("p1")
136
+ right_position = match.group("p2")
137
+ if "?" in left_position + right_position:
138
+ raise ValueError(f"unknown_linkage:{token}")
139
+ bond = ("bond", edge_index)
140
+ graph.add_node(
141
+ bond,
142
+ kind="bond",
143
+ label="BOND",
144
+ signature_label=hash_text("BOND"),
145
+ )
146
+ graph.add_edge(left, bond, position=left_position)
147
+ graph.add_edge(right, bond, position=right_position)
148
+
149
+ residue_count = len(node_to_resdef)
150
+ if len(tokens) != max(0, residue_count - 1):
151
+ raise ValueError("not_a_residue_tree")
152
+ if graph.number_of_nodes() and not nx.is_tree(graph):
153
+ raise ValueError("parsed_graph_not_tree")
154
+ graph.graph["residue_count"] = residue_count
155
+ return graph
156
+
157
+
158
+ def residue_nodes(graph: nx.Graph) -> list[object]:
159
+ return sorted(
160
+ (node for node, data in graph.nodes(data=True) if data["kind"] == "residue"),
161
+ key=str,
162
+ )
163
+
164
+
165
+ def bond_nodes(graph: nx.Graph) -> list[object]:
166
+ return sorted(
167
+ (node for node, data in graph.nodes(data=True) if data["kind"] == "bond"),
168
+ key=str,
169
+ )
170
+
171
+
172
+ def signature(graph: nx.Graph) -> str:
173
+ """Return an isomorphism-invariant candidate key, never final evidence."""
174
+ if not graph:
175
+ return "EMPTY"
176
+ digest = nx.weisfeiler_lehman_graph_hash(
177
+ graph,
178
+ node_attr="signature_label",
179
+ edge_attr="position",
180
+ iterations=3,
181
+ digest_size=32,
182
+ )
183
+ return f"{graph.number_of_nodes()}:{graph.number_of_edges()}:{digest}"
184
+
185
+
186
+ def exact_isomorphic(left: nx.Graph, right: nx.Graph) -> bool:
187
+ if (
188
+ left.number_of_nodes() != right.number_of_nodes()
189
+ or left.number_of_edges() != right.number_of_edges()
190
+ ):
191
+ return False
192
+ return nx.is_isomorphic(
193
+ left,
194
+ right,
195
+ node_match=NODE_MATCH,
196
+ edge_match=EDGE_MATCH,
197
+ )
198
+
199
+
200
+ def residue_wildcard_signature(graph: nx.Graph, residue: object) -> str:
201
+ previous = graph.nodes[residue]["label"]
202
+ previous_signature = graph.nodes[residue]["signature_label"]
203
+ graph.nodes[residue]["label"] = "RESIDUE_WILDCARD"
204
+ graph.nodes[residue]["signature_label"] = hash_text("RESIDUE_WILDCARD")
205
+ try:
206
+ return signature(graph)
207
+ finally:
208
+ graph.nodes[residue]["label"] = previous
209
+ graph.nodes[residue]["signature_label"] = previous_signature
210
+
211
+
212
+ def bond_wildcard_signature(graph: nx.Graph, bond: object) -> str:
213
+ previous = {
214
+ neighbor: graph.edges[bond, neighbor]["position"]
215
+ for neighbor in graph.neighbors(bond)
216
+ }
217
+ for neighbor in previous:
218
+ graph.edges[bond, neighbor]["position"] = "*"
219
+ try:
220
+ return signature(graph)
221
+ finally:
222
+ for neighbor, value in previous.items():
223
+ graph.edges[bond, neighbor]["position"] = value
224
+
225
+
226
+ def verify_residue_substitution(
227
+ left: nx.Graph,
228
+ left_residue: object,
229
+ right: nx.Graph,
230
+ right_residue: object,
231
+ ) -> bool:
232
+ if exact_isomorphic(left, right):
233
+ return False
234
+ left_copy = left.copy()
235
+ right_copy = right.copy()
236
+ left_copy.nodes[left_residue]["label"] = "RESIDUE_WILDCARD"
237
+ left_copy.nodes[left_residue]["signature_label"] = hash_text(
238
+ "RESIDUE_WILDCARD"
239
+ )
240
+ right_copy.nodes[right_residue]["label"] = "RESIDUE_WILDCARD"
241
+ right_copy.nodes[right_residue]["signature_label"] = hash_text(
242
+ "RESIDUE_WILDCARD"
243
+ )
244
+ return exact_isomorphic(left_copy, right_copy)
245
+
246
+
247
+ def verify_linkage_change(
248
+ left: nx.Graph,
249
+ left_bond: object,
250
+ right: nx.Graph,
251
+ right_bond: object,
252
+ ) -> bool:
253
+ if exact_isomorphic(left, right):
254
+ return False
255
+ left_copy = left.copy()
256
+ right_copy = right.copy()
257
+ for graph, bond in ((left_copy, left_bond), (right_copy, right_bond)):
258
+ for neighbor in graph.neighbors(bond):
259
+ graph.edges[bond, neighbor]["position"] = "*"
260
+ return exact_isomorphic(left_copy, right_copy)
261
+
262
+
263
+ def terminal_leaf_nodes(graph: nx.Graph) -> list[object]:
264
+ if int(graph.graph["residue_count"]) <= 1:
265
+ return []
266
+ return [node for node in residue_nodes(graph) if graph.degree(node) == 1]
267
+
268
+
269
+ def remove_terminal_residue(graph: nx.Graph, residue: object) -> nx.Graph:
270
+ if residue not in graph or graph.nodes[residue]["kind"] != "residue":
271
+ raise ValueError("terminal deletion target is not a residue")
272
+ if graph.degree(residue) != 1:
273
+ raise ValueError("terminal deletion target is not a leaf")
274
+ bond = next(iter(graph.neighbors(residue)))
275
+ if graph.nodes[bond]["kind"] != "bond" or graph.degree(bond) != 2:
276
+ raise ValueError("terminal residue does not have one simple bond")
277
+ result = graph.copy()
278
+ result.remove_nodes_from((residue, bond))
279
+ result.graph["residue_count"] = int(graph.graph["residue_count"]) - 1
280
+ return result
281
+
282
+
283
+ def verify_terminal_edit(
284
+ smaller: nx.Graph, larger: nx.Graph, larger_leaf: object
285
+ ) -> bool:
286
+ if int(larger.graph["residue_count"]) != int(smaller.graph["residue_count"]) + 1:
287
+ return False
288
+ return exact_isomorphic(smaller, remove_terminal_residue(larger, larger_leaf))
289
+
290
+
291
+ def cut_components(
292
+ graph: nx.Graph, bond: object
293
+ ) -> list[tuple[nx.Graph, str]]:
294
+ neighbors = list(graph.neighbors(bond))
295
+ if len(neighbors) != 2:
296
+ raise ValueError("branch cut requires a simple two-ended bond")
297
+ positions = {
298
+ neighbor: graph.edges[bond, neighbor]["position"] for neighbor in neighbors
299
+ }
300
+ cut = graph.copy()
301
+ cut.remove_node(bond)
302
+ components: list[tuple[nx.Graph, str]] = []
303
+ for neighbor in neighbors:
304
+ nodes = nx.node_connected_component(cut, neighbor)
305
+ component = cut.subgraph(nodes).copy()
306
+ component.graph["residue_count"] = sum(
307
+ data["kind"] == "residue" for _, data in component.nodes(data=True)
308
+ )
309
+ components.append((component, positions[neighbor]))
310
+ return components
311
+
312
+
313
+ def branch_cut_signature(graph: nx.Graph, bond: object) -> str:
314
+ parts = sorted((signature(component), position) for component, position in cut_components(graph, bond))
315
+ return hash_text(json.dumps(parts, separators=(",", ":")))
316
+
317
+
318
+ def verify_branch_relocation(
319
+ left: nx.Graph,
320
+ left_bond: object,
321
+ right: nx.Graph,
322
+ right_bond: object,
323
+ ) -> bool:
324
+ if exact_isomorphic(left, right):
325
+ return False
326
+ if int(left.graph["residue_count"]) != int(right.graph["residue_count"]):
327
+ return False
328
+ left_parts = cut_components(left, left_bond)
329
+ right_parts = cut_components(right, right_bond)
330
+ for permutation in ((0, 1), (1, 0)):
331
+ if all(
332
+ left_parts[index][1] == right_parts[permutation[index]][1]
333
+ and exact_isomorphic(
334
+ left_parts[index][0], right_parts[permutation[index]][0]
335
+ )
336
+ for index in (0, 1)
337
+ ):
338
+ return True
339
+ return False
340
+
341
+
342
+ def synthetic_graph(
343
+ labels: list[str], edges: list[tuple[int, int, str, str]]
344
+ ) -> nx.Graph:
345
+ graph = nx.Graph()
346
+ for index, label in enumerate(labels):
347
+ graph.add_node(
348
+ ("residue", index),
349
+ kind="residue",
350
+ label=label,
351
+ signature_label=hash_text(label),
352
+ )
353
+ for index, (left, right, left_pos, right_pos) in enumerate(edges):
354
+ bond = ("bond", index)
355
+ graph.add_node(
356
+ bond,
357
+ kind="bond",
358
+ label="BOND",
359
+ signature_label=hash_text("BOND"),
360
+ )
361
+ graph.add_edge(("residue", left), bond, position=left_pos)
362
+ graph.add_edge(("residue", right), bond, position=right_pos)
363
+ graph.graph["residue_count"] = len(labels)
364
+ if graph and not nx.is_tree(graph):
365
+ raise ValueError("synthetic graph is not a tree")
366
+ return graph
367
+
368
+
369
+ def self_test() -> None:
370
+ anchor = synthetic_graph(
371
+ ["A", "B", "C"],
372
+ [(0, 1, "4", "1"), (1, 2, "3", "1")],
373
+ )
374
+ substituted = synthetic_graph(
375
+ ["A", "B", "D"],
376
+ [(0, 1, "4", "1"), (1, 2, "3", "1")],
377
+ )
378
+ assert residue_wildcard_signature(anchor, ("residue", 2)) == residue_wildcard_signature(
379
+ substituted, ("residue", 2)
380
+ )
381
+ assert verify_residue_substitution(
382
+ anchor, ("residue", 2), substituted, ("residue", 2)
383
+ )
384
+
385
+ relinked = synthetic_graph(
386
+ ["A", "B", "C"],
387
+ [(0, 1, "4", "1"), (1, 2, "6", "1")],
388
+ )
389
+ assert bond_wildcard_signature(anchor, ("bond", 1)) == bond_wildcard_signature(
390
+ relinked, ("bond", 1)
391
+ )
392
+ assert verify_linkage_change(anchor, ("bond", 1), relinked, ("bond", 1))
393
+
394
+ extended = synthetic_graph(
395
+ ["A", "B", "C", "D"],
396
+ [
397
+ (0, 1, "4", "1"),
398
+ (1, 2, "3", "1"),
399
+ (2, 3, "2", "1"),
400
+ ],
401
+ )
402
+ assert signature(anchor) == signature(remove_terminal_residue(extended, ("residue", 3)))
403
+ assert verify_terminal_edit(anchor, extended, ("residue", 3))
404
+
405
+ chain = extended
406
+ relocated = synthetic_graph(
407
+ ["A", "B", "C", "D"],
408
+ [
409
+ (0, 1, "4", "1"),
410
+ (1, 2, "3", "1"),
411
+ (1, 3, "2", "1"),
412
+ ],
413
+ )
414
+ assert branch_cut_signature(chain, ("bond", 2)) == branch_cut_signature(
415
+ relocated, ("bond", 2)
416
+ )
417
+ assert verify_branch_relocation(chain, ("bond", 2), relocated, ("bond", 2))
418
+ assert not exact_isomorphic(anchor, substituted)
419
+ assert exact_isomorphic(anchor, anchor.copy())
420
+ unicode_graph = synthetic_graph(["alpha", "beta-\u03b2"], [(0, 1, "4", "1")])
421
+ assert signature(unicode_graph)
422
+
423
+
424
+ def load_anchor_rows(path: Path) -> list[dict[str, str]]:
425
+ with gzip.open(path, "rt", newline="", encoding="utf-8") as handle:
426
+ rows = list(csv.DictReader(handle, delimiter="\t"))
427
+ if not rows or len({row["canonical_wurcs"] for row in rows}) != len(rows):
428
+ raise RuntimeError("Anchor manifest is empty or has duplicate WURCS")
429
+ return rows
430
+
431
+
432
+ def build_anchor_indices(anchor_rows: list[dict[str, str]]) -> tuple[
433
+ list[AnchorGraph],
434
+ dict[str, list[int]],
435
+ dict[str, list[tuple[int, object]]],
436
+ dict[str, list[tuple[int, object]]],
437
+ dict[str, list[tuple[int, object]]],
438
+ dict[str, list[int]],
439
+ dict[str, list[tuple[int, object]]],
440
+ ]:
441
+ anchors: list[AnchorGraph] = []
442
+ exact_index: dict[str, list[int]] = defaultdict(list)
443
+ residue_index: dict[str, list[tuple[int, object]]] = defaultdict(list)
444
+ bond_index: dict[str, list[tuple[int, object]]] = defaultdict(list)
445
+ leaf_deleted_index: dict[str, list[tuple[int, object]]] = defaultdict(list)
446
+ terminal_extension_anchor_index: dict[str, list[int]] = defaultdict(list)
447
+ branch_index: dict[str, list[tuple[int, object]]] = defaultdict(list)
448
+
449
+ for row in anchor_rows:
450
+ if row["fingerprint_tier"] != FIXED_TIER:
451
+ continue
452
+ graph = parse_fixed_tree(row["canonical_wurcs"])
453
+ index = len(anchors)
454
+ anchor = AnchorGraph(
455
+ index=index,
456
+ wurcs=row["canonical_wurcs"],
457
+ sources=row["anchor_sources"],
458
+ tier=row["fingerprint_tier"],
459
+ residue_count=int(graph.graph["residue_count"]),
460
+ graph=graph,
461
+ exact_signature=signature(graph),
462
+ )
463
+ anchors.append(anchor)
464
+ exact_index[anchor.exact_signature].append(index)
465
+
466
+ if anchor.residue_count >= 3:
467
+ terminal_extension_anchor_index[anchor.exact_signature].append(index)
468
+ for residue in residue_nodes(graph):
469
+ residue_index[residue_wildcard_signature(graph, residue)].append(
470
+ (index, residue)
471
+ )
472
+ for leaf in terminal_leaf_nodes(graph):
473
+ leaf_deleted_index[signature(remove_terminal_residue(graph, leaf))].append(
474
+ (index, leaf)
475
+ )
476
+ for bond in bond_nodes(graph):
477
+ branch_index[branch_cut_signature(graph, bond)].append((index, bond))
478
+
479
+ if anchor.residue_count >= 2:
480
+ for bond in bond_nodes(graph):
481
+ bond_index[bond_wildcard_signature(graph, bond)].append((index, bond))
482
+
483
+ return (
484
+ anchors,
485
+ exact_index,
486
+ residue_index,
487
+ bond_index,
488
+ leaf_deleted_index,
489
+ terminal_extension_anchor_index,
490
+ branch_index,
491
+ )
492
+
493
+
494
+ def add_relation(
495
+ relations: dict[tuple[int, str], list[tuple[object, object]]],
496
+ anchor_index: int,
497
+ relation: str,
498
+ anchor_element: object = "",
499
+ candidate_element: object = "",
500
+ ) -> None:
501
+ pair = (anchor_element, candidate_element)
502
+ values = relations.setdefault((anchor_index, relation), [])
503
+ if pair not in values:
504
+ values.append(pair)
505
+
506
+
507
+ def confirmed_relations(
508
+ graph: nx.Graph,
509
+ anchors: list[AnchorGraph],
510
+ exact_index: dict[str, list[int]],
511
+ residue_index: dict[str, list[tuple[int, object]]],
512
+ bond_index: dict[str, list[tuple[int, object]]],
513
+ leaf_deleted_index: dict[str, list[tuple[int, object]]],
514
+ terminal_extension_anchor_index: dict[str, list[int]],
515
+ branch_index: dict[str, list[tuple[int, object]]],
516
+ ) -> list[tuple[int, str]]:
517
+ candidates: dict[tuple[int, str], list[tuple[object, object]]] = {}
518
+ exact_key = signature(graph)
519
+ for anchor_index in exact_index.get(exact_key, []):
520
+ add_relation(candidates, anchor_index, "graph_identity")
521
+
522
+ residue_count = int(graph.graph["residue_count"])
523
+ if residue_count >= 3:
524
+ for residue in residue_nodes(graph):
525
+ key = residue_wildcard_signature(graph, residue)
526
+ for anchor_index, anchor_residue in residue_index.get(key, []):
527
+ add_relation(
528
+ candidates,
529
+ anchor_index,
530
+ "residue_substitution",
531
+ anchor_residue,
532
+ residue,
533
+ )
534
+ for bond in bond_nodes(graph):
535
+ key = branch_cut_signature(graph, bond)
536
+ for anchor_index, anchor_bond in branch_index.get(key, []):
537
+ add_relation(
538
+ candidates,
539
+ anchor_index,
540
+ "branch_attachment_relocation",
541
+ anchor_bond,
542
+ bond,
543
+ )
544
+
545
+ if residue_count >= 2:
546
+ for bond in bond_nodes(graph):
547
+ key = bond_wildcard_signature(graph, bond)
548
+ for anchor_index, anchor_bond in bond_index.get(key, []):
549
+ add_relation(
550
+ candidates,
551
+ anchor_index,
552
+ "linkage_change",
553
+ anchor_bond,
554
+ bond,
555
+ )
556
+
557
+ for anchor_index, anchor_leaf in leaf_deleted_index.get(exact_key, []):
558
+ add_relation(
559
+ candidates,
560
+ anchor_index,
561
+ "terminal_insertion_or_deletion",
562
+ anchor_leaf,
563
+ "",
564
+ )
565
+ for candidate_leaf in terminal_leaf_nodes(graph):
566
+ key = signature(remove_terminal_residue(graph, candidate_leaf))
567
+ for anchor_index in terminal_extension_anchor_index.get(key, []):
568
+ add_relation(
569
+ candidates,
570
+ anchor_index,
571
+ "terminal_insertion_or_deletion",
572
+ "",
573
+ candidate_leaf,
574
+ )
575
+
576
+ confirmed: list[tuple[int, str]] = []
577
+ for (anchor_index, relation), element_pairs in sorted(
578
+ candidates.items(),
579
+ key=lambda item: (
580
+ RELATION_ORDER[item[0][1]],
581
+ anchors[item[0][0]].wurcs,
582
+ ),
583
+ ):
584
+ anchor = anchors[anchor_index]
585
+ passes = False
586
+ for anchor_element, candidate_element in element_pairs:
587
+ if relation == "graph_identity":
588
+ passes = exact_isomorphic(anchor.graph, graph)
589
+ elif relation == "residue_substitution":
590
+ passes = verify_residue_substitution(
591
+ anchor.graph, anchor_element, graph, candidate_element
592
+ )
593
+ elif relation == "linkage_change":
594
+ passes = verify_linkage_change(
595
+ anchor.graph, anchor_element, graph, candidate_element
596
+ )
597
+ elif relation == "terminal_insertion_or_deletion":
598
+ if anchor_element:
599
+ passes = verify_terminal_edit(graph, anchor.graph, anchor_element)
600
+ else:
601
+ passes = verify_terminal_edit(anchor.graph, graph, candidate_element)
602
+ elif relation == "branch_attachment_relocation":
603
+ passes = verify_branch_relocation(
604
+ anchor.graph, anchor_element, graph, candidate_element
605
+ )
606
+ else:
607
+ raise AssertionError(f"Unknown relation: {relation}")
608
+ if passes:
609
+ break
610
+ if passes:
611
+ confirmed.append((anchor_index, relation))
612
+ return confirmed
613
+
614
+
615
+ def main() -> None:
616
+ parser = argparse.ArgumentParser()
617
+ parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
618
+ parser.add_argument("--anchor-manifest", type=Path, default=ANCHORS)
619
+ parser.add_argument(
620
+ "--anchor-policy",
621
+ default="role-derived non-fit endpoint union",
622
+ )
623
+ parser.add_argument("--final-anchor-authority", action="store_true")
624
+ parser.add_argument(
625
+ "--limit",
626
+ type=int,
627
+ default=0,
628
+ help="Process only the first N sequence parents for a smoke run; 0 is full.",
629
+ )
630
+ parser.add_argument("--self-test-only", action="store_true")
631
+ args = parser.parse_args()
632
+
633
+ self_test()
634
+ if args.self_test_only:
635
+ print("self-test: PASS")
636
+ return
637
+
638
+ args.output_dir.mkdir(parents=True, exist_ok=True)
639
+ anchor_rows = load_anchor_rows(args.anchor_manifest)
640
+ anchor_wurcs = {row["canonical_wurcs"] for row in anchor_rows}
641
+ source_by_wurcs = {
642
+ row["canonical_wurcs"]: row["anchor_sources"] for row in anchor_rows
643
+ }
644
+ (
645
+ fixed_anchors,
646
+ exact_index,
647
+ residue_index,
648
+ bond_index,
649
+ leaf_deleted_index,
650
+ terminal_extension_anchor_index,
651
+ branch_index,
652
+ ) = build_anchor_indices(anchor_rows)
653
+
654
+ parent_fields = [
655
+ "parent_glycorpus_id",
656
+ "canonical_wurcs",
657
+ "fingerprint_tier",
658
+ "residue_count",
659
+ "exact_wurcs_anchor",
660
+ "graph_identity_anchor_count",
661
+ "direct_local_neighbor_anchor_count",
662
+ "confirmed_relation_types",
663
+ "confirmed_anchor_wurcs",
664
+ "provisional_excluded",
665
+ "provisional_exclusion_reason",
666
+ "fixed_anchor_relation_status",
667
+ "unresolved_reason",
668
+ ]
669
+ edge_fields = [
670
+ "anchor_wurcs",
671
+ "anchor_sources",
672
+ "anchor_residue_count",
673
+ "parent_glycorpus_id",
674
+ "parent_wurcs",
675
+ "parent_residue_count",
676
+ "relation_type",
677
+ "candidate_generator",
678
+ "exact_verifier",
679
+ ]
680
+ parent_output = args.output_dir / "review_p1_direct_local_neighbor_manifest.tsv.gz"
681
+ edge_output = args.output_dir / "review_p1_direct_local_neighbor_edges.tsv.gz"
682
+
683
+ processed_parent_ids: set[str] = set()
684
+ excluded_parent_ids: set[str] = set()
685
+ exact_parent_ids: set[str] = set()
686
+ graph_identity_parent_ids: set[str] = set()
687
+ local_neighbor_parent_ids: set[str] = set()
688
+ parent_status_counts: Counter[str] = Counter()
689
+ relation_edge_counts: Counter[str] = Counter()
690
+ tier_counts: Counter[str] = Counter()
691
+ parse_failures = 0
692
+
693
+ with gzip_tsv_writer(parent_output, parent_fields) as parent_writer:
694
+ with gzip_tsv_writer(edge_output, edge_fields) as edge_writer:
695
+ with gzip.open(FINGERPRINT, "rt", newline="", encoding="utf-8") as handle:
696
+ for row in csv.DictReader(handle, delimiter="\t"):
697
+ if not (
698
+ row["graph_record_role"] == "sequence_eligible_parent"
699
+ and as_bool(row["sequence_eligible_parent"])
700
+ ):
701
+ continue
702
+ if args.limit and len(processed_parent_ids) >= args.limit:
703
+ break
704
+
705
+ parent_id = row["parent_glycorpus_id"]
706
+ wurcs = row["canonical_wurcs"]
707
+ tier = row["fingerprint_tier"]
708
+ residue_count = int(row["residue_count"])
709
+ processed_parent_ids.add(parent_id)
710
+ tier_counts[tier] += 1
711
+ exact_wurcs = wurcs in anchor_wurcs
712
+ relations: list[tuple[int, str]] = []
713
+ unresolved_reason = ""
714
+
715
+ if tier == FIXED_TIER:
716
+ try:
717
+ graph = parse_fixed_tree(wurcs)
718
+ relations = confirmed_relations(
719
+ graph,
720
+ fixed_anchors,
721
+ exact_index,
722
+ residue_index,
723
+ bond_index,
724
+ leaf_deleted_index,
725
+ terminal_extension_anchor_index,
726
+ branch_index,
727
+ )
728
+ except ValueError as exc:
729
+ parse_failures += 1
730
+ unresolved_reason = f"fixed_tier_parse_failure:{exc}"
731
+ else:
732
+ unresolved_reason = (
733
+ "nonfixed_wurcs_exact_identity_only_in_this_pass"
734
+ )
735
+
736
+ relation_by_anchor: dict[int, set[str]] = defaultdict(set)
737
+ for anchor_index, relation in relations:
738
+ relation_by_anchor[anchor_index].add(relation)
739
+
740
+ graph_identity_indices = {
741
+ index
742
+ for index, values in relation_by_anchor.items()
743
+ if "graph_identity" in values
744
+ }
745
+ local_indices = {
746
+ index
747
+ for index, values in relation_by_anchor.items()
748
+ if values
749
+ - {"graph_identity"}
750
+ }
751
+ relation_types = sorted(
752
+ {
753
+ relation
754
+ for values in relation_by_anchor.values()
755
+ for relation in values
756
+ },
757
+ key=lambda value: RELATION_ORDER[value],
758
+ )
759
+ confirmed_anchor_indices = sorted(
760
+ relation_by_anchor,
761
+ key=lambda index: fixed_anchors[index].wurcs,
762
+ )
763
+
764
+ if exact_wurcs:
765
+ exact_parent_ids.add(parent_id)
766
+ excluded_parent_ids.add(parent_id)
767
+ exclusion_reason = "exact_role_anchor"
768
+ elif graph_identity_indices:
769
+ graph_identity_parent_ids.add(parent_id)
770
+ excluded_parent_ids.add(parent_id)
771
+ exclusion_reason = "validated_graph_identity"
772
+ elif local_indices:
773
+ local_neighbor_parent_ids.add(parent_id)
774
+ excluded_parent_ids.add(parent_id)
775
+ exclusion_reason = "exact_confirmed_direct_local_neighbor"
776
+ else:
777
+ exclusion_reason = "retained_in_provisional_direct_rule"
778
+
779
+ if tier != FIXED_TIER:
780
+ relation_status = "not_evaluable_nonfixed_except_exact"
781
+ elif unresolved_reason:
782
+ relation_status = "unresolved_parser_failure"
783
+ elif graph_identity_indices or local_indices:
784
+ relation_status = "confirmed"
785
+ else:
786
+ relation_status = "ruled_out_against_fixed_anchor_relations"
787
+ parent_status_counts[relation_status] += 1
788
+
789
+ parent_writer.writerow(
790
+ {
791
+ "parent_glycorpus_id": parent_id,
792
+ "canonical_wurcs": wurcs,
793
+ "fingerprint_tier": tier,
794
+ "residue_count": residue_count,
795
+ "exact_wurcs_anchor": exact_wurcs,
796
+ "graph_identity_anchor_count": len(graph_identity_indices),
797
+ "direct_local_neighbor_anchor_count": len(local_indices),
798
+ "confirmed_relation_types": ";".join(relation_types),
799
+ "confirmed_anchor_wurcs": ";".join(
800
+ fixed_anchors[index].wurcs
801
+ for index in confirmed_anchor_indices
802
+ ),
803
+ "provisional_excluded": parent_id in excluded_parent_ids,
804
+ "provisional_exclusion_reason": exclusion_reason,
805
+ "fixed_anchor_relation_status": relation_status,
806
+ "unresolved_reason": unresolved_reason,
807
+ }
808
+ )
809
+
810
+ for anchor_index in confirmed_anchor_indices:
811
+ anchor = fixed_anchors[anchor_index]
812
+ for relation in sorted(
813
+ relation_by_anchor[anchor_index],
814
+ key=lambda value: RELATION_ORDER[value],
815
+ ):
816
+ relation_edge_counts[relation] += 1
817
+ edge_writer.writerow(
818
+ {
819
+ "anchor_wurcs": anchor.wurcs,
820
+ "anchor_sources": source_by_wurcs[anchor.wurcs],
821
+ "anchor_residue_count": anchor.residue_count,
822
+ "parent_glycorpus_id": parent_id,
823
+ "parent_wurcs": wurcs,
824
+ "parent_residue_count": residue_count,
825
+ "relation_type": relation,
826
+ "candidate_generator": (
827
+ "wl_hash_signature_v1_exact_verified"
828
+ ),
829
+ "exact_verifier": (
830
+ "networkx_labelled_isomorphism_v1"
831
+ ),
832
+ }
833
+ )
834
+
835
+ objective_total: Counter[str] = Counter()
836
+ objective_excluded: Counter[str] = Counter()
837
+ with gzip.open(OBJECTIVE, "rt", newline="", encoding="utf-8") as handle:
838
+ for row in csv.DictReader(handle, delimiter="\t"):
839
+ parent_id = row["parent_glycorpus_id"]
840
+ if parent_id not in processed_parent_ids or not as_bool(
841
+ row["eligible_before_partition"]
842
+ ):
843
+ continue
844
+ objective = row["objective"]
845
+ objective_total[objective] += 1
846
+ if parent_id in excluded_parent_ids:
847
+ objective_excluded[objective] += 1
848
+
849
+ retention_rows: list[dict[str, object]] = []
850
+ for objective in ("sequence", "topology", "ms", "static", "md"):
851
+ eligible = objective_total[objective]
852
+ excluded = objective_excluded[objective]
853
+ retained = eligible - excluded
854
+ retention_rows.append(
855
+ {
856
+ "objective": objective,
857
+ "eligible_in_processed_scope": eligible,
858
+ "provisional_excluded": excluded,
859
+ "provisional_retained": retained,
860
+ "retained_percent": (
861
+ f"{100.0 * retained / eligible:.6f}" if eligible else ""
862
+ ),
863
+ }
864
+ )
865
+ retention_output = args.output_dir / "review_p1_direct_local_neighbor_retention.tsv"
866
+ with retention_output.open("w", newline="", encoding="utf-8") as handle:
867
+ writer = csv.DictWriter(
868
+ handle,
869
+ fieldnames=list(retention_rows[0]),
870
+ delimiter="\t",
871
+ lineterminator="\n",
872
+ )
873
+ writer.writeheader()
874
+ writer.writerows(retention_rows)
875
+
876
+ fixed_anchor_count = sum(row["fingerprint_tier"] == FIXED_TIER for row in anchor_rows)
877
+ summary = {
878
+ "schema": "bertose_p1_direct_local_neighbor_audit_v1",
879
+ "status": (
880
+ (
881
+ "FULL_FINAL_ROLE_UNION_DIAGNOSTIC"
882
+ if args.final_anchor_authority
883
+ else "FULL_ROLE_UNION_DIAGNOSTIC"
884
+ )
885
+ if not args.limit
886
+ else "PARTIAL_SMOKE_DIAGNOSTIC"
887
+ ),
888
+ "training_authorized": False,
889
+ "official_submitted_artifacts_modified": False,
890
+ "anchor_policy": args.anchor_policy,
891
+ "binary_relation_policy": (
892
+ "exact WURCS or exact-labelled graph identity; for fixed exact-linkage "
893
+ "trees only, direct one-residue substitution, one-linkage change, "
894
+ "one terminal insertion/deletion, or one branch attachment relocation"
895
+ ),
896
+ "candidate_hash_is_final_evidence": False,
897
+ "exact_verifier": "networkx labelled graph isomorphism v1",
898
+ "transitive_closure_used": False,
899
+ "numeric_similarity_cutoff_used": False,
900
+ "anchor_count": len(anchor_rows),
901
+ "fixed_tree_exact_linkage_anchor_count": fixed_anchor_count,
902
+ "nonfixed_anchor_count": len(anchor_rows) - fixed_anchor_count,
903
+ "processed_sequence_parent_count": len(processed_parent_ids),
904
+ "processed_tier_counts": dict(sorted(tier_counts.items())),
905
+ "exact_wurcs_parent_count": len(exact_parent_ids),
906
+ "additional_graph_identity_parent_count": len(
907
+ graph_identity_parent_ids - exact_parent_ids
908
+ ),
909
+ "direct_local_neighbor_parent_count": len(
910
+ local_neighbor_parent_ids
911
+ - exact_parent_ids
912
+ - graph_identity_parent_ids
913
+ ),
914
+ "total_provisional_excluded_parent_count": len(excluded_parent_ids),
915
+ "parent_relation_status_counts": dict(sorted(parent_status_counts.items())),
916
+ "confirmed_relation_edge_counts": dict(sorted(relation_edge_counts.items())),
917
+ "fixed_tier_parse_failure_count": parse_failures,
918
+ "objective_retention": retention_rows,
919
+ "input_sha256": {
920
+ "anchor_manifest": sha256(args.anchor_manifest),
921
+ "fingerprint_manifest": sha256(FINGERPRINT),
922
+ "objective_manifest": sha256(OBJECTIVE),
923
+ "parser_source": sha256(PARSER_SOURCE),
924
+ "script": sha256(Path(__file__).resolve()),
925
+ },
926
+ "outputs": {
927
+ "parent_manifest": parent_output.name,
928
+ "parent_manifest_sha256": sha256(parent_output),
929
+ "relation_edges": edge_output.name,
930
+ "relation_edges_sha256": sha256(edge_output),
931
+ "retention": retention_output.name,
932
+ "retention_sha256": sha256(retention_output),
933
+ },
934
+ "validation_checks": {
935
+ "self_test_passed": True,
936
+ "all_candidate_relations_exact_verified": True,
937
+ "candidate_hash_used_only_for_retrieval": True,
938
+ "downstream_labels_or_model_outputs_read": False,
939
+ "all_exclusions_direct_anchor_to_parent": True,
940
+ "no_numeric_cutoff": True,
941
+ "no_transitive_closure": True,
942
+ "fixed_anchor_count_matches_manifest": fixed_anchor_count
943
+ == sum(row["fingerprint_tier"] == FIXED_TIER for row in anchor_rows),
944
+ "processed_parent_count_matches_requested_scope": (
945
+ len(processed_parent_ids) == args.limit
946
+ if args.limit
947
+ else len(processed_parent_ids) == 281208
948
+ ),
949
+ "fixed_tier_parse_failures_zero": parse_failures == 0,
950
+ },
951
+ "open_gates": ([] if args.final_anchor_authority else [
952
+ "final role-derived anchor authority",
953
+ ]) + [
954
+ "derivative propagation audit",
955
+ "validated nomenclature-alias authority",
956
+ "finite-completion handling for resolvable ambiguous WURCS",
957
+ "independent brute-force candidate-recall audit",
958
+ "post-implementation independent reviewer sign-off",
959
+ ],
960
+ }
961
+ summary_output = args.output_dir / "review_p1_direct_local_neighbor_summary.json"
962
+ summary_output.write_text(
963
+ json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
964
+ )
965
+ print(json.dumps(summary, indent=2, sort_keys=True))
966
+
967
+
968
+ if __name__ == "__main__":
969
+ main()
releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_p1_non_iar_exact_graph_authority_20260711.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build the non-IAR A5 anchor and exact/graph-identity exclusion authority."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import gzip
9
+ import hashlib
10
+ import io
11
+ import json
12
+ from collections import Counter, defaultdict
13
+ from contextlib import contextmanager
14
+ from pathlib import Path
15
+ from typing import Iterator
16
+
17
+
18
+ WRITING_ROOT = Path(__file__).resolve().parents[3]
19
+ MM = WRITING_ROOT / "review/multimodal"
20
+ ANCHORS = (
21
+ MM
22
+ / "outputs/p1_role_complete_anchor_union_20260711/"
23
+ "review_p1_role_complete_anchor_manifest.tsv.gz"
24
+ )
25
+ GRAPH_EDGES = (
26
+ MM
27
+ / "outputs/p1_role_complete_direct_local_neighbor_audit_20260711/"
28
+ "review_p1_direct_local_neighbor_edges.tsv.gz"
29
+ )
30
+ PARENTS = (
31
+ MM
32
+ / "outputs/p0_p1_freeze_20260710/"
33
+ "review_p1_parent_identity_manifest.tsv.gz"
34
+ )
35
+ DEFAULT_OUTPUT_DIR = MM / "outputs/p1_non_iar_exact_graph_authority_20260711"
36
+ OBJECTIVES = ("sequence", "topology", "ms", "static", "md")
37
+
38
+
39
+ def sha256(path: Path) -> str:
40
+ digest = hashlib.sha256()
41
+ with path.open("rb") as handle:
42
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
43
+ digest.update(chunk)
44
+ return digest.hexdigest()
45
+
46
+
47
+ def as_bool(value: object) -> bool:
48
+ return str(value).strip().lower() in {"1", "true", "yes"}
49
+
50
+
51
+ def split_sources(value: str) -> tuple[str, ...]:
52
+ return tuple(source for source in value.split(";") if source)
53
+
54
+
55
+ def is_iar_source(source: str) -> bool:
56
+ return (
57
+ source.startswith("glycosmos_fully_defined_iar_")
58
+ or source == "grey_seal_external_iar"
59
+ )
60
+
61
+
62
+ @contextmanager
63
+ def gzip_tsv_writer(path: Path, fieldnames: list[str]) -> Iterator[csv.DictWriter]:
64
+ with path.open("wb") as raw:
65
+ with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as zipped:
66
+ with io.TextIOWrapper(zipped, encoding="utf-8", newline="") as text:
67
+ writer = csv.DictWriter(
68
+ text,
69
+ fieldnames=fieldnames,
70
+ delimiter="\t",
71
+ lineterminator="\n",
72
+ )
73
+ writer.writeheader()
74
+ yield writer
75
+
76
+
77
+ def objective_flags(row: dict[str, str]) -> dict[str, bool]:
78
+ return {
79
+ objective: as_bool(row[f"{objective}_eligible_before_partition"])
80
+ for objective in OBJECTIVES
81
+ }
82
+
83
+
84
+ def intersection_flags(flags: dict[str, bool]) -> dict[str, bool]:
85
+ return {
86
+ "sequence_ms": flags["sequence"] and flags["ms"],
87
+ "sequence_static": flags["sequence"] and flags["static"],
88
+ "sequence_topology": flags["sequence"] and flags["topology"],
89
+ "ms_static": flags["ms"] and flags["static"],
90
+ "sequence_ms_static": (
91
+ flags["sequence"] and flags["ms"] and flags["static"]
92
+ ),
93
+ "topology_ms_static": (
94
+ flags["topology"] and flags["ms"] and flags["static"]
95
+ ),
96
+ "topology_ms_static_md": (
97
+ flags["topology"]
98
+ and flags["ms"]
99
+ and flags["static"]
100
+ and flags["md"]
101
+ ),
102
+ "ms_md": flags["ms"] and flags["md"],
103
+ "static_md": flags["static"] and flags["md"],
104
+ }
105
+
106
+
107
+ def main() -> None:
108
+ parser = argparse.ArgumentParser()
109
+ parser.add_argument("--anchor-manifest", type=Path, default=ANCHORS)
110
+ parser.add_argument("--graph-edges", type=Path, default=GRAPH_EDGES)
111
+ parser.add_argument("--parent-manifest", type=Path, default=PARENTS)
112
+ parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
113
+ args = parser.parse_args()
114
+ args.output_dir.mkdir(parents=True, exist_ok=True)
115
+
116
+ active_anchors: dict[str, dict[str, object]] = {}
117
+ active_source_memberships: Counter[str] = Counter()
118
+ inactive_source_memberships: Counter[str] = Counter()
119
+ with gzip.open(
120
+ args.anchor_manifest, "rt", newline="", encoding="utf-8"
121
+ ) as handle:
122
+ for row in csv.DictReader(handle, delimiter="\t"):
123
+ sources = split_sources(row["anchor_sources"])
124
+ active_sources = tuple(source for source in sources if not is_iar_source(source))
125
+ inactive_sources = tuple(source for source in sources if is_iar_source(source))
126
+ if not active_sources:
127
+ continue
128
+ wurcs = row["canonical_wurcs"]
129
+ if wurcs in active_anchors:
130
+ raise RuntimeError(f"Duplicate anchor WURCS: {wurcs}")
131
+ active_source_memberships.update(active_sources)
132
+ inactive_source_memberships.update(inactive_sources)
133
+ active_anchors[wurcs] = {
134
+ "row": row,
135
+ "active_sources": active_sources,
136
+ "inactive_sources": inactive_sources,
137
+ }
138
+
139
+ if len(active_anchors) != 4532:
140
+ raise RuntimeError(
141
+ f"Expected 4,532 non-IAR anchors, found {len(active_anchors):,}"
142
+ )
143
+
144
+ graph_identity_by_parent: dict[str, set[str]] = defaultdict(set)
145
+ with gzip.open(args.graph_edges, "rt", newline="", encoding="utf-8") as handle:
146
+ for row in csv.DictReader(handle, delimiter="\t"):
147
+ if row["relation_type"] != "graph_identity":
148
+ continue
149
+ if row["anchor_wurcs"] not in active_anchors:
150
+ continue
151
+ graph_identity_by_parent[row["parent_glycorpus_id"]].add(
152
+ row["anchor_wurcs"]
153
+ )
154
+
155
+ anchor_output = args.output_dir / "review_p1_non_iar_anchor_manifest.tsv.gz"
156
+ with gzip_tsv_writer(
157
+ anchor_output,
158
+ [
159
+ "canonical_wurcs",
160
+ "active_anchor_sources",
161
+ "active_source_count",
162
+ "inactive_iar_context_sources",
163
+ "in_submitted_exact_exclusion_list",
164
+ "observed_in_frozen_graph",
165
+ "sequence_parent_id",
166
+ "review_parent_id",
167
+ "fingerprint_tier",
168
+ "registry_family_id",
169
+ ],
170
+ ) as writer:
171
+ for wurcs in sorted(active_anchors):
172
+ item = active_anchors[wurcs]
173
+ row = item["row"]
174
+ active_sources = item["active_sources"]
175
+ inactive_sources = item["inactive_sources"]
176
+ writer.writerow(
177
+ {
178
+ "canonical_wurcs": wurcs,
179
+ "active_anchor_sources": ";".join(active_sources),
180
+ "active_source_count": len(active_sources),
181
+ "inactive_iar_context_sources": ";".join(inactive_sources),
182
+ "in_submitted_exact_exclusion_list": row[
183
+ "in_submitted_exact_exclusion_list"
184
+ ],
185
+ "observed_in_frozen_graph": row["observed_in_frozen_graph"],
186
+ "sequence_parent_id": row["sequence_parent_id"],
187
+ "review_parent_id": row["review_parent_id"],
188
+ "fingerprint_tier": row["fingerprint_tier"],
189
+ "registry_family_id": row["registry_family_id"],
190
+ }
191
+ )
192
+
193
+ eligible_counts: Counter[str] = Counter()
194
+ excluded_counts: Counter[str] = Counter()
195
+ retained_counts: Counter[str] = Counter()
196
+ raw_intersections: Counter[str] = Counter()
197
+ retained_intersections: Counter[str] = Counter()
198
+ sequence_exact_count = 0
199
+ sequence_graph_count = 0
200
+ sequence_graph_only_count = 0
201
+ sequence_excluded_count = 0
202
+ sequence_parent_count = 0
203
+
204
+ parent_output = (
205
+ args.output_dir / "review_p1_non_iar_exact_graph_parent_manifest.tsv.gz"
206
+ )
207
+ with gzip_tsv_writer(
208
+ parent_output,
209
+ [
210
+ "parent_glycorpus_id",
211
+ "canonical_wurcs",
212
+ "exact_wurcs_anchor",
213
+ "graph_identity_anchor_count",
214
+ "graph_identity_anchor_wurcs",
215
+ "a5_excluded",
216
+ "a5_exclusion_reason",
217
+ "sequence_eligible_before_partition",
218
+ "topology_eligible_before_partition",
219
+ "ms_eligible_before_partition",
220
+ "static_eligible_before_partition",
221
+ "md_eligible_before_partition",
222
+ ],
223
+ ) as writer:
224
+ with gzip.open(
225
+ args.parent_manifest, "rt", newline="", encoding="utf-8"
226
+ ) as handle:
227
+ for row in csv.DictReader(handle, delimiter="\t"):
228
+ flags = objective_flags(row)
229
+ if not flags["sequence"]:
230
+ continue
231
+ sequence_parent_count += 1
232
+ parent = row["parent_glycorpus_id"]
233
+ exact = row["canonical_wurcs"] in active_anchors
234
+ graph_anchors = graph_identity_by_parent.get(parent, set())
235
+ graph = bool(graph_anchors)
236
+ excluded = exact or graph
237
+ if exact:
238
+ sequence_exact_count += 1
239
+ if graph:
240
+ sequence_graph_count += 1
241
+ if graph and not exact:
242
+ sequence_graph_only_count += 1
243
+ if excluded:
244
+ sequence_excluded_count += 1
245
+
246
+ for objective, eligible in flags.items():
247
+ if not eligible:
248
+ continue
249
+ eligible_counts[objective] += 1
250
+ if excluded:
251
+ excluded_counts[objective] += 1
252
+ else:
253
+ retained_counts[objective] += 1
254
+
255
+ for name, available in intersection_flags(flags).items():
256
+ if not available:
257
+ continue
258
+ raw_intersections[name] += 1
259
+ if not excluded:
260
+ retained_intersections[name] += 1
261
+
262
+ if exact and graph:
263
+ reason = "exact_wurcs_and_verified_graph_identity"
264
+ elif exact:
265
+ reason = "exact_wurcs_anchor"
266
+ elif graph:
267
+ reason = "verified_serialized_graph_identity"
268
+ else:
269
+ reason = "retained_no_exact_or_graph_identity"
270
+ writer.writerow(
271
+ {
272
+ "parent_glycorpus_id": parent,
273
+ "canonical_wurcs": row["canonical_wurcs"],
274
+ "exact_wurcs_anchor": exact,
275
+ "graph_identity_anchor_count": len(graph_anchors),
276
+ "graph_identity_anchor_wurcs": ";".join(
277
+ sorted(graph_anchors)
278
+ ),
279
+ "a5_excluded": excluded,
280
+ "a5_exclusion_reason": reason,
281
+ **{
282
+ f"{objective}_eligible_before_partition": flags[objective]
283
+ for objective in OBJECTIVES
284
+ },
285
+ }
286
+ )
287
+
288
+ expected_retained = {
289
+ "sequence": 277448,
290
+ "topology": 89457,
291
+ "ms": 5315,
292
+ "static": 36941,
293
+ "md": 27719,
294
+ }
295
+ if dict(retained_counts) != expected_retained:
296
+ raise RuntimeError(
297
+ f"Retention mismatch: expected {expected_retained}, got {dict(retained_counts)}"
298
+ )
299
+ if sequence_parent_count != 281208 or sequence_excluded_count != 3760:
300
+ raise RuntimeError(
301
+ "Expected 281,208 sequence parents with 3,760 exact/graph exclusions"
302
+ )
303
+
304
+ summary = {
305
+ "schema": "bertose_p1_non_iar_exact_graph_authority_v1",
306
+ "status": "FROZEN_REVIEW_CANDIDATE_NOT_TRAINING_AUTHORIZATION",
307
+ "training_authorized": False,
308
+ "official_submitted_artifacts_modified": False,
309
+ "policy": (
310
+ "Use established GlycanML/interaction/AFFINose non-fit roles plus "
311
+ "predeclared AFFINose component and motif/scaffold stress roles as "
312
+ "anchors. Exclude sequence parents only for exact canonical WURCS "
313
+ "or exact-verified serialized graph identity. IAR roles are not A5 "
314
+ "pretraining exclusions. One-edit, larger-substructure, motif, and "
315
+ "nearest-neighbour relations remain analysis strata."
316
+ ),
317
+ "non_iar_anchor_count": len(active_anchors),
318
+ "active_source_memberships": dict(sorted(active_source_memberships.items())),
319
+ "inactive_iar_context_memberships_on_retained_anchors": dict(
320
+ sorted(inactive_source_memberships.items())
321
+ ),
322
+ "sequence_parent_counts": {
323
+ "eligible": sequence_parent_count,
324
+ "exact_wurcs_anchor": sequence_exact_count,
325
+ "verified_graph_identity": sequence_graph_count,
326
+ "additional_graph_identity": sequence_graph_only_count,
327
+ "excluded_union": sequence_excluded_count,
328
+ "retained": retained_counts["sequence"],
329
+ },
330
+ "objective_retention": {
331
+ objective: {
332
+ "eligible": eligible_counts[objective],
333
+ "excluded": excluded_counts[objective],
334
+ "retained": retained_counts[objective],
335
+ "retained_percent": round(
336
+ 100.0 * retained_counts[objective] / eligible_counts[objective], 6
337
+ ),
338
+ }
339
+ for objective in OBJECTIVES
340
+ },
341
+ "raw_intersections": dict(sorted(raw_intersections.items())),
342
+ "retained_intersections": dict(sorted(retained_intersections.items())),
343
+ "input_sha256": {
344
+ "role_complete_anchor_manifest": sha256(args.anchor_manifest),
345
+ "role_complete_graph_identity_edges": sha256(args.graph_edges),
346
+ "parent_identity_manifest": sha256(args.parent_manifest),
347
+ "script": sha256(Path(__file__).resolve()),
348
+ },
349
+ "outputs": {
350
+ "anchor_manifest": anchor_output.name,
351
+ "anchor_manifest_sha256": sha256(anchor_output),
352
+ "parent_manifest": parent_output.name,
353
+ "parent_manifest_sha256": sha256(parent_output),
354
+ },
355
+ "validation_checks": {
356
+ "non_iar_anchor_count_is_4532": len(active_anchors) == 4532,
357
+ "sequence_parent_count_is_281208": sequence_parent_count == 281208,
358
+ "sequence_excluded_count_is_3760": sequence_excluded_count == 3760,
359
+ "no_numeric_similarity_threshold_used": True,
360
+ "no_one_edit_or_substructure_neighbour_excluded": True,
361
+ "no_downstream_labels_or_model_outputs_read": True,
362
+ "all_expected_retention_counts_match": dict(retained_counts)
363
+ == expected_retained,
364
+ },
365
+ "claim_boundary": (
366
+ "This is an exact-glycan/serialized-graph-excluded pretraining "
367
+ "authority, not a leakage-proof claim. Similarity, motif, family, "
368
+ "substructure, and nearest-neighbour evidence must be reported as "
369
+ "separate stress analyses."
370
+ ),
371
+ }
372
+ summary_output = args.output_dir / "review_p1_non_iar_authority_summary.json"
373
+ summary_output.write_text(
374
+ json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
375
+ )
376
+ print(json.dumps(summary, indent=2, sort_keys=True))
377
+
378
+
379
+ if __name__ == "__main__":
380
+ main()
releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/build_review_a5_non_iar_later_role_successor_20260720.py ADDED
@@ -0,0 +1,770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build an audit-only A5 exclusion successor for later downstream roles.
3
+
4
+ This builder is deliberately no-overwrite and training-disabled. It preserves
5
+ every unaffected frozen 80/20 parent role, adds current paper-facing non-fit
6
+ glycans to the exact/known-graph anchor union, and propagates newly excluded
7
+ parents into the base objective contributor ledger. It does not inspect any
8
+ downstream target or model output.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import csv
15
+ import gzip
16
+ import hashlib
17
+ import importlib.util
18
+ import io
19
+ import json
20
+ import os
21
+ import stat
22
+ import sys
23
+ from collections import Counter, defaultdict
24
+ from contextlib import contextmanager
25
+ from pathlib import Path
26
+ from typing import Any, Iterable, Iterator
27
+
28
+
29
+ WRITING_ROOT = Path(__file__).resolve().parents[3]
30
+ PROJECT_ROOT = WRITING_ROOT.parent
31
+ MM = WRITING_ROOT / "review/multimodal"
32
+
33
+ PREDECESSOR_ANCHORS = (
34
+ MM / "outputs/p1_non_iar_exact_graph_authority_20260711/"
35
+ "review_p1_non_iar_anchor_manifest.tsv.gz"
36
+ )
37
+ PREDECESSOR_PARENTS = (
38
+ MM / "outputs/p1_non_iar_exact_graph_authority_20260711/"
39
+ "review_p1_non_iar_exact_graph_parent_manifest.tsv.gz"
40
+ )
41
+ PREDECESSOR_PARTITION = (
42
+ MM / "outputs/p1_non_iar_pretraining_partition_80_20_20260712/"
43
+ "review_p1_non_iar_pretraining_parent_partition.tsv.gz"
44
+ )
45
+ PREDECESSOR_OBJECTIVES = (
46
+ MM / "outputs/p1_non_iar_pretraining_partition_80_20_20260712/"
47
+ "review_p1_non_iar_pretraining_objective_partition.tsv.gz"
48
+ )
49
+ FINGERPRINTS = (
50
+ MM / "outputs/p0_p1_freeze_20260710/"
51
+ "review_p1_wurcs_motif_fingerprint_manifest.tsv.gz"
52
+ )
53
+ PARENT_IDENTITIES = (
54
+ MM / "outputs/p0_p1_freeze_20260710/"
55
+ "review_p1_parent_identity_manifest.tsv.gz"
56
+ )
57
+ PREDECESSOR_GRAPH_EDGES = (
58
+ MM / "outputs/p1_role_complete_direct_local_neighbor_audit_20260711/"
59
+ "review_p1_direct_local_neighbor_edges.tsv.gz"
60
+ )
61
+ GRAPH_VERIFIER = MM / "scripts/build_p1_direct_local_neighbor_audit_20260710.py"
62
+ STRICT3 = (
63
+ MM / "outputs/review_a5_downstream_strict3_authority_20260720_r2/"
64
+ "review_a5_downstream_strict3_authority.json"
65
+ )
66
+ K20_ANCHORS = (
67
+ MM / "outputs/p1_affinose_motif_scaffold_balanced_k20_20260711/"
68
+ "review_p1_affinose_balanced_k20_a5_anchors.tsv.gz"
69
+ )
70
+ STRICT_ANCHORS = (
71
+ MM / "outputs/review_a5_affinose_strict_glycan_components_20260720_r1/"
72
+ "review_a5_affinose_strict_glycan_nonfit_anchors_20260720.tsv.gz"
73
+ )
74
+ STRICT_SUMMARY = (
75
+ MM / "outputs/review_a5_affinose_strict_glycan_components_20260720_r1/"
76
+ "review_a5_affinose_strict_glycan_candidate_summary_20260720.json"
77
+ )
78
+ STRICT_RECEIPT = (
79
+ MM / "outputs/review_a5_affinose_strict_glycan_components_20260720_r1/"
80
+ "review_a5_affinose_strict_glycan_candidate_verification_receipt_20260720.json"
81
+ )
82
+ SEQUENCE_V3 = (
83
+ PROJECT_ROOT / "data/"
84
+ "review_sequences_a5_non_iar_80_20_parser_v3_failclosed_20260720_r3/"
85
+ "review_sequence_parent_index.tsv.gz"
86
+ )
87
+ P6_AUTHORITY = (
88
+ MM / "outputs/review_a5_affinose_p6_observation_authority_20260720_r2/"
89
+ "review_a5_affinose_p6_failclosed_authority_20260720.json"
90
+ )
91
+ P6_RECEIPT = (
92
+ MM / "outputs/review_a5_affinose_p6_observation_authority_20260720_r2/"
93
+ "review_a5_affinose_p6_failclosed_receipt_20260720.json"
94
+ )
95
+ CORRECTION = MM / "P0_P1_non_iar_authority_correction_20260711.md"
96
+ REFIT_CONTRACT = MM / "P1_parent_split_refit_contract_20260710.md"
97
+ GENERALIZATION_PROTOCOL = MM / "P1_reviewer_complete_generalization_protocol_20260710.md"
98
+ ENDPOINT_ADJUDICATION = MM / "review_a5_interaction_endpoint_adjudication_20260720.md"
99
+ MS_PARENTS = (
100
+ MM / "outputs/p1_a5_ms_harmonized_identity_repair_20260720_r1/"
101
+ "review_a5_ms_repaired_parent_manifest.tsv.gz"
102
+ )
103
+ STATIC_CHILD = (
104
+ MM / "outputs/p1_a5_static_repair_child_authority_20260720_r1/"
105
+ "review_a5_static_repair_child_authority.tsv.gz"
106
+ )
107
+ TEST_FILE = (
108
+ MM
109
+ / "tests/test_build_review_a5_non_iar_later_role_successor_20260720.py"
110
+ )
111
+
112
+ DEFAULT_OUTPUT = (
113
+ MM / "outputs/review_a5_non_iar_later_role_successor_20260720_r1"
114
+ )
115
+
116
+ EXPECTED_SHA256 = {
117
+ PREDECESSOR_ANCHORS: "048facdfd9a5ff15f1295d28f1d07b4dae2c656ca8714c3ed06df9e284ac199a",
118
+ PREDECESSOR_PARENTS: "088971d5dbe61f840a45a67a84df39478edd7a80089fd6528fd9fda8063b26b9",
119
+ PREDECESSOR_PARTITION: "7c4ed33a6cbf50df8d3264421c652c480d3a5eff0eace10c3c958e6dac6b2160",
120
+ PREDECESSOR_OBJECTIVES: "d09eebb3f7ac47058d2d0cabfd42905f624581bf4c942dceac9cad9c1973e5a0",
121
+ FINGERPRINTS: "69994cbc859146e97867e62669fd9f71b8d02140ecd9ae8056375af608afdcab",
122
+ PARENT_IDENTITIES: "55b830d839370c0cec0aab2b61b4721e6ace9ba3ee49e4c93e7be6e486d1c5c4",
123
+ PREDECESSOR_GRAPH_EDGES: "e95d8122449f4b91271fd297be41a7912244f2398739e349064fc55b781e3ae0",
124
+ GRAPH_VERIFIER: "a9160769759c1df56238caad6a9f4ed6ec68477d44435ebbc81d64bbf541b64c",
125
+ STRICT3: "c1bbcfe410c0ee78824657da84a4055e58fdf196624d8981e1aa239ae629f1d8",
126
+ K20_ANCHORS: "cdeb7b4075f00efc50c0649ac4be11560154e29a6a728c4ddeb54dd6bc26f6c9",
127
+ STRICT_ANCHORS: "85c8f760dba0e11a4346cd07bc7eb09f5f31d6bf1b0407658130da4173f8a5f9",
128
+ STRICT_SUMMARY: "d1a56f0bb7f77535c619cc2739933bd751cb5d3e3fcd2e8175fc88671e83e6ee",
129
+ STRICT_RECEIPT: "a92f442f2b74e630783b5a0eb6fb16738d6aa4c86f71b28c487cc42622d3eee1",
130
+ SEQUENCE_V3: "c61e766dbffc4a1d88c7c7fb94deb683943c0861403350e79ab09c70b317fa52",
131
+ P6_AUTHORITY: "a77e486e200e50e9becb3ad45f70ef97272b5a339267c636d746af8d9bf2b2b5",
132
+ P6_RECEIPT: "d6fb00bd5a79d5a1caa9d06f440357202d85bb6cb3f33c7732f063b4721cf23f",
133
+ CORRECTION: "f7ba54c4ec66b74e0061ffa75bd963af9589f28a7124e95208099f169657936f",
134
+ REFIT_CONTRACT: "2533d651ccbf6d86111709676bf528d7c805d1df214ada62fd45772dda0f388e",
135
+ GENERALIZATION_PROTOCOL: "9556f456490bdd7e03926cc78d9c0a73146386a9b7ac7188a556b1bc23c37b4a",
136
+ ENDPOINT_ADJUDICATION: "614a4e2144dda63538a26dcbd8fce0a9d604a97c2b4ad07cf80820407ca29fdb",
137
+ MS_PARENTS: "f4abd4b186f5f4758918b89f19870bc112a7c6be6085f870208235cd03005e26",
138
+ STATIC_CHILD: "2ca981b78bca56928d449d35467671b03db515496ac61162e68f8a1381a3661f",
139
+ }
140
+
141
+ OBJECTIVES = ("sequence", "topology", "ms", "static", "md")
142
+ ACTIVE_ROLES = ("pretraining_train", "pretraining_development")
143
+
144
+
145
+ def require(condition: bool, message: str) -> None:
146
+ if not condition:
147
+ raise RuntimeError(message)
148
+
149
+
150
+ def as_bool(value: object) -> bool:
151
+ return str(value).strip().lower() in {"1", "true", "yes"}
152
+
153
+
154
+ def sha256_file(path: Path) -> str:
155
+ digest = hashlib.sha256()
156
+ with path.open("rb") as handle:
157
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
158
+ digest.update(chunk)
159
+ return digest.hexdigest()
160
+
161
+
162
+ def validate_input(path: Path, expected: str) -> dict[str, object]:
163
+ info = path.lstat()
164
+ require(stat.S_ISREG(info.st_mode), f"input is not a regular file: {path}")
165
+ require(not path.is_symlink(), f"input is a symlink: {path}")
166
+ actual = sha256_file(path)
167
+ require(actual == expected, f"input SHA-256 changed: {path}: {actual}")
168
+ return {"name": path.name, "bytes": info.st_size, "sha256": actual}
169
+
170
+
171
+ def read_tsv(path: Path) -> tuple[list[str], list[dict[str, str]]]:
172
+ with gzip.open(path, "rt", newline="", encoding="utf-8") as handle:
173
+ reader = csv.DictReader(handle, delimiter="\t")
174
+ return list(reader.fieldnames or []), list(reader)
175
+
176
+
177
+ def iter_tsv(path: Path) -> Iterator[dict[str, str]]:
178
+ with gzip.open(path, "rt", newline="", encoding="utf-8") as handle:
179
+ yield from csv.DictReader(handle, delimiter="\t")
180
+
181
+
182
+ @contextmanager
183
+ def deterministic_tsv(path: Path, fields: list[str]) -> Iterator[csv.DictWriter]:
184
+ with path.open("wb") as raw:
185
+ with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as zipped:
186
+ with io.TextIOWrapper(zipped, encoding="utf-8", newline="") as text:
187
+ writer = csv.DictWriter(
188
+ text, fieldnames=fields, delimiter="\t", lineterminator="\n"
189
+ )
190
+ writer.writeheader()
191
+ yield writer
192
+
193
+
194
+ def json_bytes(value: object) -> bytes:
195
+ return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
196
+
197
+
198
+ def output_identity(path: Path) -> dict[str, object]:
199
+ return {"name": path.name, "bytes": path.stat().st_size, "sha256": sha256_file(path)}
200
+
201
+
202
+ def load_graph_verifier() -> Any:
203
+ spec = importlib.util.spec_from_file_location(
204
+ "review_a5_bound_graph_verifier_20260720", GRAPH_VERIFIER
205
+ )
206
+ require(spec is not None and spec.loader is not None, "cannot load graph verifier")
207
+ module = importlib.util.module_from_spec(spec)
208
+ sys.modules[spec.name] = module
209
+ spec.loader.exec_module(module)
210
+ return module
211
+
212
+
213
+ def strict3_nonfit_wurcs(authority: dict[str, Any]) -> tuple[set[str], set[str], dict[str, int]]:
214
+ primary: set[str] = set()
215
+ secondary: set[str] = set()
216
+ per_task: dict[str, int] = {}
217
+ for task in authority["task_authorities"]:
218
+ task_primary: set[str] = set()
219
+ task_secondary: set[str] = set()
220
+ for section, target in (("roles", task_primary), ("secondary_roles", task_secondary)):
221
+ for role in ("method_development", "locked_confirmation"):
222
+ item = task[section][role]
223
+ path = PROJECT_ROOT / item["path"]
224
+ require(sha256_file(path) == item["sha256"], f"strict3 role changed: {path}")
225
+ with path.open("rt", newline="", encoding="utf-8") as handle:
226
+ for row in csv.DictReader(handle):
227
+ wurcs = row["WURCS"]
228
+ if wurcs:
229
+ target.add(wurcs)
230
+ require(task_secondary <= task_primary, f"secondary strict3 not subset: {task['task']}")
231
+ primary.update(task_primary)
232
+ secondary.update(task_secondary)
233
+ per_task[task["task"]] = len(task_primary)
234
+ return primary, secondary, per_task
235
+
236
+
237
+ def main() -> None:
238
+ parser = argparse.ArgumentParser()
239
+ parser.add_argument("--output-directory", type=Path, default=DEFAULT_OUTPUT)
240
+ args = parser.parse_args()
241
+
242
+ output_names = (
243
+ "review_a5_non_iar_successor_anchor_manifest.tsv.gz",
244
+ "review_a5_non_iar_successor_exact_known_graph_parent_manifest.tsv.gz",
245
+ "review_a5_non_iar_successor_parent_partition.tsv.gz",
246
+ "review_a5_non_iar_successor_objective_partition.tsv.gz",
247
+ "review_a5_non_iar_successor_later_role_gap.tsv.gz",
248
+ "review_a5_non_iar_successor_changed_parent_roles.tsv.gz",
249
+ "review_a5_non_iar_successor_summary.json",
250
+ "review_a5_non_iar_successor_receipt.json",
251
+ )
252
+ require(
253
+ not args.output_directory.exists(),
254
+ f"refusing to overwrite output directory: {args.output_directory}",
255
+ )
256
+
257
+ input_receipts = {
258
+ str(path.relative_to(PROJECT_ROOT)): validate_input(path, expected)
259
+ for path, expected in EXPECTED_SHA256.items()
260
+ }
261
+
262
+ anchor_fields, predecessor_anchor_rows = read_tsv(PREDECESSOR_ANCHORS)
263
+ parent_fields, predecessor_parent_rows = read_tsv(PREDECESSOR_PARENTS)
264
+ partition_fields, predecessor_partition_rows = read_tsv(PREDECESSOR_PARTITION)
265
+ objective_fields, predecessor_objective_rows = read_tsv(PREDECESSOR_OBJECTIVES)
266
+ require(len(predecessor_anchor_rows) == 4532, "predecessor anchor count changed")
267
+ require(len(predecessor_parent_rows) == 281208, "predecessor parent count changed")
268
+ require(len(predecessor_partition_rows) == 281208, "predecessor partition count changed")
269
+ require(len(predecessor_objective_rows) == 281208 * 5, "objective row count changed")
270
+
271
+ predecessor_anchors = {row["canonical_wurcs"]: row for row in predecessor_anchor_rows}
272
+ require(len(predecessor_anchors) == 4532, "duplicate predecessor anchors")
273
+ predecessor_parents = {
274
+ row["parent_glycorpus_id"]: row for row in predecessor_parent_rows
275
+ }
276
+ predecessor_partition = {
277
+ row["parent_glycorpus_id"]: row for row in predecessor_partition_rows
278
+ }
279
+ require(len(predecessor_parents) == len(predecessor_partition) == 281208, "duplicate parents")
280
+
281
+ strict3_authority = json.loads(STRICT3.read_text(encoding="utf-8"))
282
+ class_primary, class_secondary, class_per_task = strict3_nonfit_wurcs(strict3_authority)
283
+ _, k20_rows = read_tsv(K20_ANCHORS)
284
+ _, strict_rows = read_tsv(STRICT_ANCHORS)
285
+ k20 = {row["glycan_wurcs"]: row for row in k20_rows}
286
+ strict = {row["glycan_wurcs"]: row for row in strict_rows}
287
+ require(len(class_primary) == 1967, "strict3 primary nonfit union changed")
288
+ require(len(k20) == 704, "K20 nonfit count changed")
289
+ require(len(strict) == 786, "strict AFFINose nonfit count changed")
290
+ require(class_primary <= set(predecessor_anchors), "strict3 role absent from predecessor anchors")
291
+ require(class_secondary <= set(predecessor_anchors), "secondary strict3 role absent from predecessor anchors")
292
+ require(set(k20) <= set(predecessor_anchors), "K20 role absent from predecessor anchors")
293
+
294
+ strict_gap = set(strict) - set(predecessor_anchors)
295
+ require(len(set(strict) & set(predecessor_anchors)) == 693, "strict overlap changed")
296
+ require(len(strict_gap) == 93, "strict later-role gap changed")
297
+ current_nonfit_union = class_primary | set(k20) | set(strict)
298
+ require(len(current_nonfit_union) == 3341, "current nonfit union changed")
299
+
300
+ fingerprint_parent: dict[str, dict[str, str]] = {}
301
+ fingerprint_rows_by_wurcs: dict[str, list[dict[str, str]]] = defaultdict(list)
302
+ for row in iter_tsv(FINGERPRINTS):
303
+ wurcs = row["canonical_wurcs"]
304
+ if wurcs in strict_gap:
305
+ fingerprint_rows_by_wurcs[wurcs].append(row)
306
+ if row["graph_record_role"] == "sequence_eligible_parent" and as_bool(
307
+ row["sequence_eligible_parent"]
308
+ ):
309
+ parent = row["parent_glycorpus_id"]
310
+ require(parent not in fingerprint_parent, f"duplicate fingerprint parent: {parent}")
311
+ fingerprint_parent[parent] = {
312
+ "parent_glycorpus_id": parent,
313
+ "canonical_wurcs": wurcs,
314
+ "fingerprint_tier": row["fingerprint_tier"],
315
+ "raw_feature_sha256": row["raw_feature_sha256"],
316
+ "precluster_union_block_id": row["precluster_union_block_id"],
317
+ "registry_family_id": row["registry_family_id"],
318
+ "exact_benchmark_excluded": row["exact_benchmark_excluded"],
319
+ }
320
+ require(len(fingerprint_parent) == 281208, "fingerprint parent coverage changed")
321
+ require(set(fingerprint_rows_by_wurcs) == strict_gap, "strict gap fingerprint coverage incomplete")
322
+ require(set(fingerprint_parent) == set(predecessor_parents), "parent/fingerprint mismatch")
323
+
324
+ graph_by_parent: dict[str, set[str]] = defaultdict(set)
325
+ for row in iter_tsv(PREDECESSOR_GRAPH_EDGES):
326
+ if row["relation_type"] == "graph_identity" and row["anchor_wurcs"] in predecessor_anchors:
327
+ graph_by_parent[row["parent_glycorpus_id"]].add(row["anchor_wurcs"])
328
+ for parent, row in predecessor_parents.items():
329
+ expected_graph = set(filter(None, row["graph_identity_anchor_wurcs"].split(";")))
330
+ require(expected_graph == graph_by_parent.get(parent, set()), f"predecessor graph mismatch: {parent}")
331
+ exact = row["canonical_wurcs"] in predecessor_anchors
332
+ require(exact == as_bool(row["exact_wurcs_anchor"]), f"predecessor exact mismatch: {parent}")
333
+ require(
334
+ (exact or bool(expected_graph)) == as_bool(row["a5_excluded"]),
335
+ f"predecessor exclusion mismatch: {parent}",
336
+ )
337
+
338
+ graph_module = load_graph_verifier()
339
+ fixed_gap_graphs: dict[str, Any] = {}
340
+ signatures: dict[str, list[str]] = defaultdict(list)
341
+ tier_by_gap: dict[str, str] = {}
342
+ for wurcs in sorted(strict_gap):
343
+ tiers = {row["fingerprint_tier"] for row in fingerprint_rows_by_wurcs[wurcs]}
344
+ require(len(tiers) == 1, f"inconsistent fingerprint tier: {wurcs}")
345
+ tier = next(iter(tiers))
346
+ tier_by_gap[wurcs] = tier
347
+ if tier == graph_module.FIXED_TIER:
348
+ graph = graph_module.parse_fixed_tree(wurcs)
349
+ fixed_gap_graphs[wurcs] = graph
350
+ signatures[graph_module.signature(graph)].append(wurcs)
351
+
352
+ new_graph_by_parent: dict[str, set[str]] = defaultdict(set)
353
+ for parent, row in fingerprint_parent.items():
354
+ if row["fingerprint_tier"] != graph_module.FIXED_TIER:
355
+ continue
356
+ graph = graph_module.parse_fixed_tree(row["canonical_wurcs"])
357
+ for anchor in signatures.get(graph_module.signature(graph), []):
358
+ if graph_module.exact_isomorphic(fixed_gap_graphs[anchor], graph):
359
+ new_graph_by_parent[parent].add(anchor)
360
+
361
+ successor_anchors = dict(predecessor_anchors)
362
+ for wurcs in sorted(strict_gap):
363
+ source = f"affinose_strict_direct_edit_component_{strict[wurcs]['component_role']}_20260720"
364
+ metadata = fingerprint_rows_by_wurcs[wurcs]
365
+ sequence_rows = [row for row in metadata if row["graph_record_role"] == "sequence_eligible_parent"]
366
+ parent_rows = [row for row in metadata if row["parent_glycorpus_id"]]
367
+ registry_ids = {row["registry_family_id"] for row in metadata if row["registry_family_id"]}
368
+ require(len(registry_ids) <= 1, f"inconsistent registry family: {wurcs}")
369
+ successor_anchors[wurcs] = {
370
+ "canonical_wurcs": wurcs,
371
+ "active_anchor_sources": source,
372
+ "active_source_count": "1",
373
+ "inactive_iar_context_sources": "",
374
+ "in_submitted_exact_exclusion_list": str(
375
+ any(as_bool(row["exact_benchmark_excluded"]) for row in metadata)
376
+ ),
377
+ "observed_in_frozen_graph": str(bool(parent_rows)),
378
+ "sequence_parent_id": sequence_rows[0]["parent_glycorpus_id"] if sequence_rows else "",
379
+ "review_parent_id": parent_rows[0]["parent_glycorpus_id"] if parent_rows else "",
380
+ "fingerprint_tier": tier_by_gap[wurcs],
381
+ "registry_family_id": next(iter(registry_ids), ""),
382
+ }
383
+ require(len(successor_anchors) == 4625, "successor anchor count changed")
384
+
385
+ successor_parent_rows: list[dict[str, str]] = []
386
+ newly_excluded: set[str] = set()
387
+ new_exact_parents: set[str] = set()
388
+ new_graph_alias_parents: set[str] = set()
389
+ for source in predecessor_parent_rows:
390
+ parent = source["parent_glycorpus_id"]
391
+ canonical = source["canonical_wurcs"]
392
+ exact = canonical in successor_anchors
393
+ graph_anchors = graph_by_parent.get(parent, set()) | new_graph_by_parent.get(parent, set())
394
+ graph = bool(graph_anchors)
395
+ excluded = exact or graph
396
+ if canonical in strict_gap:
397
+ new_exact_parents.add(parent)
398
+ if new_graph_by_parent.get(parent) and canonical not in strict_gap:
399
+ new_graph_alias_parents.add(parent)
400
+ if excluded and not as_bool(source["a5_excluded"]):
401
+ newly_excluded.add(parent)
402
+ if exact and graph:
403
+ reason = "exact_wurcs_and_verified_graph_identity"
404
+ elif exact:
405
+ reason = "exact_wurcs_anchor"
406
+ elif graph:
407
+ reason = "verified_serialized_graph_identity"
408
+ else:
409
+ reason = "retained_no_exact_or_graph_identity"
410
+ row = dict(source)
411
+ row.update(
412
+ {
413
+ "exact_wurcs_anchor": str(exact),
414
+ "graph_identity_anchor_count": str(len(graph_anchors)),
415
+ "graph_identity_anchor_wurcs": ";".join(sorted(graph_anchors)),
416
+ "a5_excluded": str(excluded),
417
+ "a5_exclusion_reason": reason,
418
+ }
419
+ )
420
+ successor_parent_rows.append(row)
421
+ require(len(new_exact_parents) == 73, "new exact parent count changed")
422
+ require(len(new_graph_alias_parents) == 0, "new graph-only alias count changed")
423
+ require(len(newly_excluded) == 73, "new exclusion union changed")
424
+ successor_exact_count = sum(
425
+ as_bool(row["exact_wurcs_anchor"]) for row in successor_parent_rows
426
+ )
427
+ successor_graph_count = sum(
428
+ int(row["graph_identity_anchor_count"]) > 0 for row in successor_parent_rows
429
+ )
430
+ successor_graph_only_count = sum(
431
+ not as_bool(row["exact_wurcs_anchor"])
432
+ and int(row["graph_identity_anchor_count"]) > 0
433
+ for row in successor_parent_rows
434
+ )
435
+ require(
436
+ (successor_exact_count, successor_graph_count, successor_graph_only_count)
437
+ == (3673, 3587, 160),
438
+ "successor exact/known-graph counts changed",
439
+ )
440
+
441
+ successor_parent_by_id = {
442
+ row["parent_glycorpus_id"]: row for row in successor_parent_rows
443
+ }
444
+ role_by_parent: dict[str, str] = {}
445
+ successor_partition_rows: list[dict[str, str]] = []
446
+ role_transitions: Counter[tuple[str, str]] = Counter()
447
+ for source in predecessor_partition_rows:
448
+ parent = source["parent_glycorpus_id"]
449
+ old_role = source["pretraining_partition"]
450
+ new_role = (
451
+ "excluded_exact_or_verified_graph_identity"
452
+ if parent in newly_excluded
453
+ else old_role
454
+ )
455
+ role_by_parent[parent] = new_role
456
+ role_transitions[(old_role, new_role)] += 1
457
+ row = dict(source)
458
+ row["a5_excluded"] = str(new_role not in ACTIVE_ROLES)
459
+ row["a5_exclusion_reason"] = successor_parent_by_id[parent][
460
+ "a5_exclusion_reason"
461
+ ]
462
+ row["pretraining_partition"] = new_role
463
+ row["learned_preprocessing_fit_contributor"] = str(new_role == "pretraining_train")
464
+ row["model_pretraining_contributor"] = str(new_role == "pretraining_train")
465
+ successor_partition_rows.append(row)
466
+
467
+ role_counts = Counter(role_by_parent.values())
468
+ require(
469
+ role_counts
470
+ == {
471
+ "pretraining_train": 221899,
472
+ "pretraining_development": 55476,
473
+ "excluded_exact_or_verified_graph_identity": 3833,
474
+ },
475
+ f"successor role counts changed: {role_counts}",
476
+ )
477
+ require(
478
+ role_transitions[("pretraining_train", "excluded_exact_or_verified_graph_identity")] == 59
479
+ and role_transitions[("pretraining_development", "excluded_exact_or_verified_graph_identity")] == 14,
480
+ "strict role-transition counts changed",
481
+ )
482
+ require(
483
+ all(old == new or new == "excluded_exact_or_verified_graph_identity" for old, new in role_transitions),
484
+ "unaffected 80/20 role moved",
485
+ )
486
+
487
+ objective_counts: Counter[tuple[str, str]] = Counter()
488
+ objective_impacts: Counter[tuple[str, str]] = Counter()
489
+ successor_objective_rows: list[dict[str, str]] = []
490
+ flags_by_parent: dict[str, dict[str, bool]] = defaultdict(dict)
491
+ for source in predecessor_objective_rows:
492
+ parent = source["parent_glycorpus_id"]
493
+ objective = source["objective"]
494
+ eligible = as_bool(source["eligible_before_partition"])
495
+ old_role = source["pretraining_partition"]
496
+ new_role = role_by_parent[parent]
497
+ row = dict(source)
498
+ row["pretraining_partition"] = new_role
499
+ row["objective_train_contributor"] = str(eligible and new_role == "pretraining_train")
500
+ row["objective_development_contributor"] = str(
501
+ eligible and new_role == "pretraining_development"
502
+ )
503
+ successor_objective_rows.append(row)
504
+ flags_by_parent[parent][objective] = eligible
505
+ if eligible and new_role in ACTIVE_ROLES:
506
+ objective_counts[(new_role, objective)] += 1
507
+ if eligible and parent in newly_excluded:
508
+ objective_impacts[(old_role, objective)] += 1
509
+ require(all(set(flags) == set(OBJECTIVES) for flags in flags_by_parent.values()), "objective coverage incomplete")
510
+
511
+ registry_by_parent: dict[str, str] = {}
512
+ for row in iter_tsv(PARENT_IDENTITIES):
513
+ parent = row["parent_glycorpus_id"]
514
+ if parent in role_by_parent:
515
+ registry_by_parent[parent] = row["registry_family_id"]
516
+ require(set(registry_by_parent) == set(role_by_parent), "registry-family coverage incomplete")
517
+ crossings: dict[str, int] = {}
518
+ for name, grouping in (
519
+ ("precluster_union_block_id", {p: fingerprint_parent[p]["precluster_union_block_id"] for p in role_by_parent}),
520
+ ("raw_feature_sha256", {p: fingerprint_parent[p]["raw_feature_sha256"] for p in role_by_parent}),
521
+ ("registry_family_id", registry_by_parent),
522
+ ):
523
+ roles_by_group: dict[str, set[str]] = defaultdict(set)
524
+ for parent, group in grouping.items():
525
+ role = role_by_parent[parent]
526
+ if role in ACTIVE_ROLES:
527
+ roles_by_group[group].add(role)
528
+ crossings[name] = sum(len(roles) > 1 for roles in roles_by_group.values())
529
+ require(not any(crossings.values()), f"retained grouping crosses roles: {crossings}")
530
+
531
+ sequence_v3 = {row["parent_glycorpus_id"]: row for row in iter_tsv(SEQUENCE_V3)}
532
+ require(set(sequence_v3) == set(role_by_parent), "sequence-v3 parent coverage changed")
533
+ strict_sequence_roles = Counter(
534
+ sequence_v3[parent]["pretraining_partition"] for parent in newly_excluded
535
+ )
536
+
537
+ ms_impacted = [row for row in iter_tsv(MS_PARENTS) if row["parent_glycorpus_id"] in newly_excluded]
538
+ ms_impacted_roles = Counter(row["pretraining_partition"] for row in ms_impacted)
539
+ static_impacted = [row for row in iter_tsv(STATIC_CHILD) if row["parent_glycorpus_id"] in newly_excluded]
540
+ static_active_states = Counter(
541
+ row["a5_static_partition"] for row in static_impacted if row["a5_static_partition"] in ACTIVE_ROLES
542
+ )
543
+ static_active_parents = Counter()
544
+ static_parent_roles: dict[str, str] = {}
545
+ for row in static_impacted:
546
+ if row["a5_static_partition"] in ACTIVE_ROLES:
547
+ static_parent_roles[row["parent_glycorpus_id"]] = row["a5_static_partition"]
548
+ static_active_parents.update(static_parent_roles.values())
549
+
550
+ gap_fields = [
551
+ "canonical_wurcs",
552
+ "strict_component_role",
553
+ "fingerprint_tier",
554
+ "fingerprint_graph_record_roles",
555
+ "sequence_parent_id",
556
+ "sequence_v3_pretraining_partition",
557
+ "successor_pretraining_partition",
558
+ "known_graph_identity_parent_count",
559
+ "known_graph_only_alias_parent_count",
560
+ *[f"{objective}_eligible_before_partition" for objective in OBJECTIVES],
561
+ ]
562
+ gap_rows: list[dict[str, object]] = []
563
+ for wurcs in sorted(strict_gap):
564
+ parent_candidates = [
565
+ row["parent_glycorpus_id"]
566
+ for row in fingerprint_rows_by_wurcs[wurcs]
567
+ if row["graph_record_role"] == "sequence_eligible_parent"
568
+ ]
569
+ parent = parent_candidates[0] if parent_candidates else ""
570
+ identity_parents = sorted(
571
+ candidate for candidate, anchors in new_graph_by_parent.items() if wurcs in anchors
572
+ )
573
+ graph_only = [candidate for candidate in identity_parents if fingerprint_parent[candidate]["canonical_wurcs"] != wurcs]
574
+ flags = flags_by_parent.get(parent, {})
575
+ gap_rows.append(
576
+ {
577
+ "canonical_wurcs": wurcs,
578
+ "strict_component_role": strict[wurcs]["component_role"],
579
+ "fingerprint_tier": tier_by_gap[wurcs],
580
+ "fingerprint_graph_record_roles": ";".join(
581
+ sorted({row["graph_record_role"] for row in fingerprint_rows_by_wurcs[wurcs]})
582
+ ),
583
+ "sequence_parent_id": parent,
584
+ "sequence_v3_pretraining_partition": sequence_v3[parent]["pretraining_partition"] if parent else "absent",
585
+ "successor_pretraining_partition": role_by_parent[parent] if parent else "absent",
586
+ "known_graph_identity_parent_count": len(identity_parents),
587
+ "known_graph_only_alias_parent_count": len(graph_only),
588
+ **{f"{objective}_eligible_before_partition": str(flags.get(objective, False)) for objective in OBJECTIVES},
589
+ }
590
+ )
591
+
592
+ changed_fields = [
593
+ "parent_glycorpus_id",
594
+ "canonical_wurcs",
595
+ "predecessor_pretraining_partition",
596
+ "successor_pretraining_partition",
597
+ "strict_component_role",
598
+ "successor_exclusion_reason",
599
+ ]
600
+ changed_rows = []
601
+ for parent in sorted(newly_excluded):
602
+ canonical = predecessor_parents[parent]["canonical_wurcs"]
603
+ changed_rows.append(
604
+ {
605
+ "parent_glycorpus_id": parent,
606
+ "canonical_wurcs": canonical,
607
+ "predecessor_pretraining_partition": predecessor_partition[parent]["pretraining_partition"],
608
+ "successor_pretraining_partition": role_by_parent[parent],
609
+ "strict_component_role": strict[canonical]["component_role"],
610
+ "successor_exclusion_reason": successor_parent_by_id[parent]["a5_exclusion_reason"],
611
+ }
612
+ )
613
+
614
+ args.output_directory.mkdir(parents=True, exist_ok=False)
615
+ paths = {name: args.output_directory / name for name in output_names}
616
+ with deterministic_tsv(paths[output_names[0]], anchor_fields) as writer:
617
+ writer.writerows(successor_anchors[w] for w in sorted(successor_anchors))
618
+ with deterministic_tsv(paths[output_names[1]], parent_fields) as writer:
619
+ writer.writerows(successor_parent_rows)
620
+ with deterministic_tsv(paths[output_names[2]], partition_fields) as writer:
621
+ writer.writerows(successor_partition_rows)
622
+ with deterministic_tsv(paths[output_names[3]], objective_fields) as writer:
623
+ writer.writerows(successor_objective_rows)
624
+ with deterministic_tsv(paths[output_names[4]], gap_fields) as writer:
625
+ writer.writerows(gap_rows)
626
+ with deterministic_tsv(paths[output_names[5]], changed_fields) as writer:
627
+ writer.writerows(changed_rows)
628
+
629
+ active_total = role_counts["pretraining_train"] + role_counts["pretraining_development"]
630
+ summary: dict[str, Any] = {
631
+ "schema": "bertose_a5_non_iar_later_role_successor_v1",
632
+ "status": "PREPARED_FAIL_CLOSED_REQUIRES_ALL_LEARNED_REFITS_AND_NEW_PRETRAINING",
633
+ "training_authorized": False,
634
+ "scientific_launch_authorized": False,
635
+ "official_submitted_or_predecessor_artifacts_modified": False,
636
+ "implementation": {
637
+ "builder": {
638
+ "name": Path(__file__).name,
639
+ "bytes": Path(__file__).stat().st_size,
640
+ "sha256": sha256_file(Path(__file__)),
641
+ },
642
+ "bound_graph_verifier": input_receipts[
643
+ str(GRAPH_VERIFIER.relative_to(PROJECT_ROOT))
644
+ ],
645
+ "tests": {
646
+ "name": TEST_FILE.name,
647
+ "bytes": TEST_FILE.stat().st_size,
648
+ "sha256": sha256_file(TEST_FILE),
649
+ },
650
+ },
651
+ "policy": {
652
+ "source": "P0/P1 exact canonical WURCS or known retrieved and exact-verified serialized graph identity",
653
+ "later_role_rule": "any later paper-facing endpoint must add non-fit glycans through a versioned authority rebuild",
654
+ "allocation": "preserve every unaffected frozen 80/20 role; move only newly matched parents to transform-only exclusion",
655
+ "graph_retrieval_boundary": "fixed-tree exact-linkage WL retrieval plus exact labelled isomorphism; not exhaustive for nonfixed WURCS",
656
+ },
657
+ "endpoint_adjudication": {
658
+ "strict_affinose_status": "selected_primary_interaction_double_cold_endpoint",
659
+ "corrected_k20_status": "mandatory_companion",
660
+ "generic_unlabelled_pretraining_overlap_note": "may be acceptable in other protocols, but conflicts with this frozen project-specific exclusion rule",
661
+ "target_or_outcome_values_read": False,
662
+ },
663
+ "current_nonfit_coverage": {
664
+ "strict3_classification_primary_unique_wurcs": len(class_primary),
665
+ "strict3_classification_secondary_unique_wurcs": len(class_secondary),
666
+ "strict3_primary_per_task_unique_wurcs": dict(sorted(class_per_task.items())),
667
+ "strict3_primary_missing_from_predecessor_4532": len(class_primary - set(predecessor_anchors)),
668
+ "k20_nonfit_wurcs": len(k20),
669
+ "k20_missing_from_predecessor_4532": len(set(k20) - set(predecessor_anchors)),
670
+ "strict_affinose_nonfit_wurcs": len(strict),
671
+ "strict_affinose_predecessor_overlap": len(set(strict) & set(predecessor_anchors)),
672
+ "strict_affinose_missing_from_predecessor": len(strict_gap),
673
+ "all_current_nonfit_unique_wurcs": len(current_nonfit_union),
674
+ },
675
+ "successor_anchor_and_parent_counts": {
676
+ "predecessor_active_anchors": len(predecessor_anchors),
677
+ "successor_active_anchors": len(successor_anchors),
678
+ "added_strict_anchors": len(strict_gap),
679
+ "added_fixed_tree_exact_linkage_anchors": sum(t == graph_module.FIXED_TIER for t in tier_by_gap.values()),
680
+ "new_exact_sequence_parents": len(new_exact_parents),
681
+ "new_known_graph_identity_edges": sum(len(v) for v in new_graph_by_parent.values()),
682
+ "new_known_graph_only_alias_parents": len(new_graph_alias_parents),
683
+ "new_excluded_parent_union": len(newly_excluded),
684
+ "successor_exact_wurcs_sequence_parents": successor_exact_count,
685
+ "successor_known_graph_identity_sequence_parents": successor_graph_count,
686
+ "successor_additional_known_graph_only_sequence_parents": successor_graph_only_count,
687
+ "successor_excluded_parent_union": sum(as_bool(row["a5_excluded"]) for row in successor_parent_rows),
688
+ },
689
+ "preserved_80_20_partition": {
690
+ "parent_role_counts": dict(sorted(role_counts.items())),
691
+ "retained_train_fraction": role_counts["pretraining_train"] / active_total,
692
+ "retained_development_fraction": role_counts["pretraining_development"] / active_total,
693
+ "newly_excluded_predecessor_roles": dict(sorted(strict_sequence_roles.items())),
694
+ "unaffected_train_development_role_moves": 0,
695
+ "retained_group_crossings": crossings,
696
+ },
697
+ "base_objective_successor_counts": {
698
+ role: {objective: objective_counts[(role, objective)] for objective in OBJECTIVES}
699
+ for role in ACTIVE_ROLES
700
+ },
701
+ "base_objective_removed_counts": {
702
+ role: {objective: objective_impacts[(role, objective)] for objective in OBJECTIVES}
703
+ for role in ACTIVE_ROLES
704
+ },
705
+ "current_repaired_derivative_snapshot_impact": {
706
+ "ms_parent_rows_to_move_transform_only": len(ms_impacted),
707
+ "ms_parent_rows_by_predecessor_role": dict(sorted(ms_impacted_roles.items())),
708
+ "static_child_active_parent_rows_to_move_transform_only": sum(static_active_parents.values()),
709
+ "static_child_active_parents_by_predecessor_role": dict(sorted(static_active_parents.items())),
710
+ "static_child_active_state_rows_to_move_transform_only": sum(static_active_states.values()),
711
+ "static_child_active_states_by_predecessor_role": dict(sorted(static_active_states.items())),
712
+ "static_child_snapshot_is_not_final_training_authority": True,
713
+ },
714
+ "required_successors_before_any_compute": [
715
+ "fit-only BPE vocabulary and complete sequence tensor package",
716
+ "topology-r3 parent/pair authority and train-only null",
717
+ "repaired no-RT MS roles/tokens and train-only null",
718
+ "final static graph materializer plus GNN/RVQ/codebooks/tokens and train-only null",
719
+ "multimodal dataset/planner/masker/sampler manifests",
720
+ "all enabled-objective null scales and validation selection ledger",
721
+ "A000, A111, and every other paper-facing A5 factorial pretraining run",
722
+ "downstream strict/K20 evaluators bound to the resulting frozen A5 exports",
723
+ ],
724
+ "reuse_adjudication": {
725
+ "historical_a000_a111_reusable_for_strict_primary_claim": False,
726
+ "reason": "59 newly excluded parents influenced fitting and fit-only transforms; 14 influenced development selection; downstream outcomes cannot repair upstream exposure",
727
+ "exploratory_alternative": "only if strict is demoted from selected primary and exposure is explicitly reported; contradicted by the bound endpoint adjudication",
728
+ },
729
+ "input_artifacts": input_receipts,
730
+ "output_artifacts": {},
731
+ "validation_checks": {
732
+ "all_current_strict3_primary_nonfit_wurcs_in_successor": class_primary <= set(successor_anchors),
733
+ "all_current_strict3_secondary_nonfit_wurcs_in_successor": class_secondary <= set(successor_anchors),
734
+ "all_current_k20_nonfit_wurcs_in_successor": set(k20) <= set(successor_anchors),
735
+ "all_current_strict_affinose_nonfit_wurcs_in_successor": set(strict) <= set(successor_anchors),
736
+ "predecessor_graph_identity_replayed_exactly": True,
737
+ "new_graph_candidates_exact_isomorphism_verified": True,
738
+ "all_new_exact_or_known_graph_parents_excluded": len(newly_excluded) == 73,
739
+ "no_unaffected_80_20_parent_role_changed": True,
740
+ "no_retained_group_crossing": not any(crossings.values()),
741
+ "no_downstream_targets_or_model_outputs_read": True,
742
+ "training_fail_closed": True,
743
+ },
744
+ }
745
+ summary["output_artifacts"] = {
746
+ name: output_identity(paths[name]) for name in output_names[:6]
747
+ }
748
+ paths[output_names[6]].write_bytes(json_bytes(summary))
749
+ receipt = {
750
+ "schema": "bertose_a5_non_iar_later_role_successor_receipt_v1",
751
+ "status": "PASS_AUDIT_ONLY_FAIL_CLOSED",
752
+ "training_authorized": False,
753
+ "summary": output_identity(paths[output_names[6]]),
754
+ "payloads": {name: output_identity(paths[name]) for name in output_names[:6]},
755
+ "checks": summary["validation_checks"],
756
+ "determinism_contract": {
757
+ "gzip_mtime": 0,
758
+ "json_keys_sorted": True,
759
+ "absolute_output_paths_embedded": False,
760
+ "input_hashes_rechecked_after_build": True,
761
+ },
762
+ }
763
+ for path, expected in EXPECTED_SHA256.items():
764
+ require(sha256_file(path) == expected, f"input changed during build: {path}")
765
+ paths[output_names[7]].write_bytes(json_bytes(receipt))
766
+ print(json.dumps({"summary": output_identity(paths[output_names[6]]), "receipt": output_identity(paths[output_names[7]]), "role_counts": dict(role_counts)}, indent=2, sort_keys=True))
767
+
768
+
769
+ if __name__ == "__main__":
770
+ main()
releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/scripts/repartition_a5_non_iar_80_20_20260712.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Reassign the frozen non-IAR A5 authority to a grouped 80/20 split."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import gzip
9
+ import hashlib
10
+ import io
11
+ import json
12
+ from collections import Counter, defaultdict
13
+ from contextlib import contextmanager
14
+ from pathlib import Path
15
+ from typing import Iterator
16
+
17
+
18
+ WRITING_ROOT = Path(__file__).resolve().parents[3]
19
+ MM = WRITING_ROOT / "review/multimodal"
20
+ SOURCE = MM / "outputs/p1_non_iar_pretraining_partition_20260711"
21
+ SOURCE_PARENT = SOURCE / "review_p1_non_iar_pretraining_parent_partition.tsv.gz"
22
+ SOURCE_OBJECTIVE = SOURCE / "review_p1_non_iar_pretraining_objective_partition.tsv.gz"
23
+ FINGERPRINTS = (
24
+ MM
25
+ / "outputs/p0_p1_freeze_20260710/"
26
+ "review_p1_wurcs_motif_fingerprint_manifest.tsv.gz"
27
+ )
28
+ IDENTITY = (
29
+ MM
30
+ / "outputs/p0_p1_freeze_20260710/"
31
+ "review_p1_parent_identity_manifest.tsv.gz"
32
+ )
33
+ DEFAULT_OUTPUT = MM / "outputs/p1_non_iar_pretraining_partition_80_20_20260712"
34
+ SOURCE_PARENT_SHA256 = "1bcbd764f0970e664547692a9bb2c39442a9faecd01faa2def4dff7303687fcc"
35
+ SOURCE_OBJECTIVE_SHA256 = "5bf36799bfa96f5a275782d046570bcec54932b0d7aee04693080151e6b1710c"
36
+ TRAIN_FRACTION = 0.80
37
+ SPLIT_SALT = "BERTose-local-split-audit-v2-20260712|block|0.80"
38
+ ACTIVE_ROLES = ("pretraining_train", "pretraining_development")
39
+ OBJECTIVES = ("sequence", "topology", "ms", "static", "md")
40
+
41
+
42
+ def sha256_file(path: Path) -> str:
43
+ digest = hashlib.sha256()
44
+ with path.open("rb") as handle:
45
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
46
+ digest.update(chunk)
47
+ return digest.hexdigest()
48
+
49
+
50
+ def stable_hash(value: str) -> str:
51
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
52
+
53
+
54
+ def as_bool(value: object) -> bool:
55
+ return str(value).strip().lower() in {"1", "true", "yes"}
56
+
57
+
58
+ def read_tsv(path: Path) -> tuple[list[str], list[dict[str, str]]]:
59
+ with gzip.open(path, "rt", newline="", encoding="utf-8") as handle:
60
+ reader = csv.DictReader(handle, delimiter="\t")
61
+ return list(reader.fieldnames or []), list(reader)
62
+
63
+
64
+ @contextmanager
65
+ def deterministic_tsv(path: Path, fields: list[str]) -> Iterator[csv.DictWriter]:
66
+ with path.open("wb") as raw:
67
+ with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as zipped:
68
+ with io.TextIOWrapper(zipped, encoding="utf-8", newline="") as text:
69
+ writer = csv.DictWriter(
70
+ text,
71
+ fieldnames=fields,
72
+ delimiter="\t",
73
+ lineterminator="\n",
74
+ )
75
+ writer.writeheader()
76
+ yield writer
77
+
78
+
79
+ def main() -> None:
80
+ parser = argparse.ArgumentParser()
81
+ parser.add_argument("--source-parent", type=Path, default=SOURCE_PARENT)
82
+ parser.add_argument("--source-objective", type=Path, default=SOURCE_OBJECTIVE)
83
+ parser.add_argument("--fingerprints", type=Path, default=FINGERPRINTS)
84
+ parser.add_argument("--identity", type=Path, default=IDENTITY)
85
+ parser.add_argument("--output-directory", type=Path, default=DEFAULT_OUTPUT)
86
+ args = parser.parse_args()
87
+
88
+ if sha256_file(args.source_parent) != SOURCE_PARENT_SHA256:
89
+ raise RuntimeError("source A5 parent authority changed")
90
+ if sha256_file(args.source_objective) != SOURCE_OBJECTIVE_SHA256:
91
+ raise RuntimeError("source A5 objective authority changed")
92
+
93
+ output_parent = (
94
+ args.output_directory / "review_p1_non_iar_pretraining_parent_partition.tsv.gz"
95
+ )
96
+ output_objective = (
97
+ args.output_directory
98
+ / "review_p1_non_iar_pretraining_objective_partition.tsv.gz"
99
+ )
100
+ output_summary = (
101
+ args.output_directory / "review_p1_non_iar_partition_summary.json"
102
+ )
103
+ outputs = (output_parent, output_objective, output_summary)
104
+ if any(path.exists() for path in outputs):
105
+ raise FileExistsError(
106
+ f"refusing to overwrite existing 80/20 outputs: "
107
+ f"{[str(path) for path in outputs if path.exists()]}"
108
+ )
109
+
110
+ parent_fields, parent_rows = read_tsv(args.source_parent)
111
+ objective_fields, objective_rows = read_tsv(args.source_objective)
112
+ if len(parent_rows) != 281208 or len(objective_rows) != 281208 * 5:
113
+ raise RuntimeError("source non-IAR authority has unexpected row counts")
114
+ parents = {row["parent_glycorpus_id"]: row for row in parent_rows}
115
+ if len(parents) != len(parent_rows):
116
+ raise RuntimeError("source parent authority repeats a parent")
117
+
118
+ retained_by_block: dict[str, list[str]] = defaultdict(list)
119
+ for parent, row in parents.items():
120
+ if not as_bool(row["a5_excluded"]):
121
+ retained_by_block[row["precluster_union_block_id"]].append(parent)
122
+ retained_count = sum(map(len, retained_by_block.values()))
123
+ excluded_count = len(parent_rows) - retained_count
124
+ if (retained_count, excluded_count) != (277448, 3760):
125
+ raise RuntimeError("source retained/excluded counts changed")
126
+
127
+ ordered_blocks = sorted(
128
+ retained_by_block,
129
+ key=lambda block: stable_hash(f"{SPLIT_SALT}|{block}"),
130
+ )
131
+ target_train_rows = TRAIN_FRACTION * retained_count
132
+ cumulative = 0
133
+ role_by_block: dict[str, str] = {}
134
+ for block in ordered_blocks:
135
+ block_size = len(retained_by_block[block])
136
+ midpoint = cumulative + block_size / 2
137
+ role_by_block[block] = (
138
+ "pretraining_train"
139
+ if midpoint < target_train_rows
140
+ else "pretraining_development"
141
+ )
142
+ cumulative += block_size
143
+
144
+ role_by_parent = {
145
+ parent: (
146
+ "excluded_exact_or_verified_graph_identity"
147
+ if as_bool(row["a5_excluded"])
148
+ else role_by_block[row["precluster_union_block_id"]]
149
+ )
150
+ for parent, row in parents.items()
151
+ }
152
+
153
+ parent_allowed_changes = {
154
+ "pretraining_partition",
155
+ "learned_preprocessing_fit_contributor",
156
+ "model_pretraining_contributor",
157
+ }
158
+ new_parent_rows: list[dict[str, str]] = []
159
+ for source in parent_rows:
160
+ row = dict(source)
161
+ role = role_by_parent[source["parent_glycorpus_id"]]
162
+ is_train = role == "pretraining_train"
163
+ row["pretraining_partition"] = role
164
+ row["learned_preprocessing_fit_contributor"] = str(is_train)
165
+ row["model_pretraining_contributor"] = str(is_train)
166
+ changed = {field for field in row if row[field] != source[field]}
167
+ if not changed <= parent_allowed_changes:
168
+ raise RuntimeError(f"80/20 repartition changed parent authority fields: {changed}")
169
+ new_parent_rows.append(row)
170
+
171
+ objective_allowed_changes = {
172
+ "pretraining_partition",
173
+ "objective_train_contributor",
174
+ "objective_development_contributor",
175
+ }
176
+ new_objective_rows: list[dict[str, str]] = []
177
+ objective_counts: Counter[tuple[str, str]] = Counter()
178
+ flags_by_parent: dict[str, dict[str, bool]] = defaultdict(dict)
179
+ for source in objective_rows:
180
+ row = dict(source)
181
+ parent = source["parent_glycorpus_id"]
182
+ objective = source["objective"]
183
+ role = role_by_parent[parent]
184
+ eligible = as_bool(source["eligible_before_partition"])
185
+ row["pretraining_partition"] = role
186
+ row["objective_train_contributor"] = str(
187
+ eligible and role == "pretraining_train"
188
+ )
189
+ row["objective_development_contributor"] = str(
190
+ eligible and role == "pretraining_development"
191
+ )
192
+ changed = {field for field in row if row[field] != source[field]}
193
+ if not changed <= objective_allowed_changes:
194
+ raise RuntimeError(
195
+ f"80/20 repartition changed objective authority fields: {changed}"
196
+ )
197
+ if role in ACTIVE_ROLES and eligible:
198
+ objective_counts[(role, objective)] += 1
199
+ flags_by_parent[parent][objective] = eligible
200
+ new_objective_rows.append(row)
201
+
202
+ if any(set(flags) != set(OBJECTIVES) for flags in flags_by_parent.values()):
203
+ raise RuntimeError("objective authority is incomplete for at least one parent")
204
+
205
+ raw_feature_by_parent: dict[str, str] = {}
206
+ with gzip.open(args.fingerprints, "rt", newline="", encoding="utf-8") as handle:
207
+ for row in csv.DictReader(handle, delimiter="\t"):
208
+ parent = row["parent_glycorpus_id"]
209
+ if parent in parents and row.get("graph_record_role") == "sequence_eligible_parent":
210
+ raw_feature_by_parent[parent] = row["raw_feature_sha256"]
211
+ family_by_parent: dict[str, str] = {}
212
+ with gzip.open(args.identity, "rt", newline="", encoding="utf-8") as handle:
213
+ for row in csv.DictReader(handle, delimiter="\t"):
214
+ parent = row["parent_glycorpus_id"]
215
+ if parent in parents:
216
+ family_by_parent[parent] = row["registry_family_id"]
217
+ if set(raw_feature_by_parent) != set(parents) or set(family_by_parent) != set(parents):
218
+ raise RuntimeError("grouping authorities do not cover the parent authority")
219
+
220
+ crossings: dict[str, int] = {}
221
+ for name, grouping in (
222
+ ("precluster_union_block_id", {p: parents[p]["precluster_union_block_id"] for p in parents}),
223
+ ("raw_feature_sha256", raw_feature_by_parent),
224
+ ("registry_family_id", family_by_parent),
225
+ ):
226
+ roles_by_group: dict[str, set[str]] = defaultdict(set)
227
+ for parent, group in grouping.items():
228
+ role = role_by_parent[parent]
229
+ if role in ACTIVE_ROLES:
230
+ roles_by_group[group].add(role)
231
+ crossings[name] = sum(len(roles) > 1 for roles in roles_by_group.values())
232
+ if any(crossings.values()):
233
+ raise RuntimeError(f"80/20 grouping authority crosses roles: {crossings}")
234
+
235
+ intersection_counts: Counter[tuple[str, str]] = Counter()
236
+ for parent, flags in flags_by_parent.items():
237
+ role = role_by_parent[parent]
238
+ if role not in ACTIVE_ROLES:
239
+ continue
240
+ for name, enabled in {
241
+ "topology_ms": flags["topology"] and flags["ms"],
242
+ "topology_static": flags["topology"] and flags["static"],
243
+ "ms_static": flags["ms"] and flags["static"],
244
+ "topology_ms_static": flags["topology"] and flags["ms"] and flags["static"],
245
+ "topology_ms_static_md": flags["topology"] and flags["ms"] and flags["static"] and flags["md"],
246
+ }.items():
247
+ intersection_counts[(role, name)] += int(enabled)
248
+
249
+ args.output_directory.mkdir(parents=True, exist_ok=True)
250
+ with deterministic_tsv(output_parent, parent_fields) as writer:
251
+ writer.writerows(new_parent_rows)
252
+ with deterministic_tsv(output_objective, objective_fields) as writer:
253
+ writer.writerows(new_objective_rows)
254
+
255
+ role_counts = Counter(role_by_parent.values())
256
+ summary = {
257
+ "schema": "bertose_p1_non_iar_pretraining_partition_v2",
258
+ "status": "FROZEN_REVIEW_CANDIDATE_NOT_TRAINING_AUTHORIZATION",
259
+ "training_authorized": False,
260
+ "official_submitted_artifacts_modified": False,
261
+ "change_scope": "role_only_repartition_from_frozen_non_iar_authority",
262
+ "split_method": "deterministic_whole_precluster_block_weighted_midpoint",
263
+ "split_salt": SPLIT_SALT,
264
+ "target_train_fraction": TRAIN_FRACTION,
265
+ "realized_train_fraction": role_counts["pretraining_train"] / retained_count,
266
+ "parent_role_counts": dict(sorted(role_counts.items())),
267
+ "retained_block_count": len(retained_by_block),
268
+ "largest_retained_block": max(map(len, retained_by_block.values())),
269
+ "objective_partitions": {
270
+ role: {
271
+ objective: objective_counts[(role, objective)]
272
+ for objective in OBJECTIVES
273
+ }
274
+ for role in ACTIVE_ROLES
275
+ },
276
+ "intersection_partitions": {
277
+ role: {
278
+ name: intersection_counts[(role, name)]
279
+ for name in (
280
+ "topology_ms",
281
+ "topology_static",
282
+ "ms_static",
283
+ "topology_ms_static",
284
+ "topology_ms_static_md",
285
+ )
286
+ }
287
+ for role in ACTIVE_ROLES
288
+ },
289
+ "group_crossings": crossings,
290
+ "validation_checks": {
291
+ "all_281208_parent_rows_preserved": len(new_parent_rows) == 281208,
292
+ "all_1406040_objective_rows_preserved": len(new_objective_rows) == 1406040,
293
+ "only_parent_role_fields_changed": True,
294
+ "only_objective_role_fields_changed": True,
295
+ "no_declared_group_crosses_train_development": not any(crossings.values()),
296
+ "no_downstream_outcome_used": True,
297
+ "submitted_artifacts_left_unchanged": True,
298
+ },
299
+ "inputs": {
300
+ "source_parent_partition": {
301
+ "path": str(args.source_parent),
302
+ "sha256": sha256_file(args.source_parent),
303
+ },
304
+ "source_objective_partition": {
305
+ "path": str(args.source_objective),
306
+ "sha256": sha256_file(args.source_objective),
307
+ },
308
+ "fingerprints": {
309
+ "path": str(args.fingerprints),
310
+ "sha256": sha256_file(args.fingerprints),
311
+ },
312
+ "parent_identity": {
313
+ "path": str(args.identity),
314
+ "sha256": sha256_file(args.identity),
315
+ },
316
+ "implementation": {
317
+ "path": str(Path(__file__)),
318
+ "sha256": sha256_file(Path(__file__)),
319
+ },
320
+ },
321
+ "outputs": {
322
+ "parent_partition": {
323
+ "path": str(output_parent),
324
+ "sha256": sha256_file(output_parent),
325
+ },
326
+ "objective_partition": {
327
+ "path": str(output_objective),
328
+ "sha256": sha256_file(output_objective),
329
+ },
330
+ },
331
+ }
332
+ output_summary.write_text(
333
+ json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
334
+ )
335
+ print(json.dumps(summary, indent=2, sort_keys=True))
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()
releases/2026-10-02/benchmark_exclusion/writing/review/multimodal/tests/test_build_review_a5_non_iar_later_role_successor_20260720.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import gzip
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import unittest
9
+ from collections import Counter
10
+ from pathlib import Path
11
+
12
+
13
+ WRITING_ROOT = Path(__file__).resolve().parents[3]
14
+ OUTPUT = Path(
15
+ os.environ.get(
16
+ "REVIEW_A5_NON_IAR_SUCCESSOR_OUTPUT",
17
+ str(
18
+ WRITING_ROOT
19
+ / "review/multimodal/outputs/"
20
+ "review_a5_non_iar_later_role_successor_20260720_r1"
21
+ ),
22
+ )
23
+ )
24
+ PREDECESSOR = (
25
+ WRITING_ROOT
26
+ / "review/multimodal/outputs/"
27
+ "p1_non_iar_pretraining_partition_80_20_20260712/"
28
+ "review_p1_non_iar_pretraining_parent_partition.tsv.gz"
29
+ )
30
+
31
+
32
+ def sha256(path: Path) -> str:
33
+ digest = hashlib.sha256()
34
+ with path.open("rb") as handle:
35
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
36
+ digest.update(chunk)
37
+ return digest.hexdigest()
38
+
39
+
40
+ def read_tsv(path: Path) -> list[dict[str, str]]:
41
+ with gzip.open(path, "rt", newline="", encoding="utf-8") as handle:
42
+ return list(csv.DictReader(handle, delimiter="\t"))
43
+
44
+
45
+ class LaterRoleSuccessorTests(unittest.TestCase):
46
+ @classmethod
47
+ def setUpClass(cls) -> None:
48
+ cls.summary = json.loads(
49
+ (OUTPUT / "review_a5_non_iar_successor_summary.json").read_text(
50
+ encoding="utf-8"
51
+ )
52
+ )
53
+ cls.receipt = json.loads(
54
+ (OUTPUT / "review_a5_non_iar_successor_receipt.json").read_text(
55
+ encoding="utf-8"
56
+ )
57
+ )
58
+
59
+ def test_01_fail_closed(self) -> None:
60
+ self.assertFalse(self.summary["training_authorized"])
61
+ self.assertFalse(self.summary["scientific_launch_authorized"])
62
+ self.assertEqual(self.receipt["status"], "PASS_AUDIT_ONLY_FAIL_CLOSED")
63
+ self.assertFalse(self.receipt["training_authorized"])
64
+
65
+ def test_02_receipt_reopens_every_payload(self) -> None:
66
+ for item in [self.receipt["summary"], *self.receipt["payloads"].values()]:
67
+ path = OUTPUT / item["name"]
68
+ self.assertEqual(path.stat().st_size, item["bytes"])
69
+ self.assertEqual(sha256(path), item["sha256"])
70
+
71
+ def test_03_all_outputs_are_review_prefixed_and_deterministic_gzip(self) -> None:
72
+ for path in OUTPUT.iterdir():
73
+ self.assertTrue(path.name.startswith("review_"))
74
+ if path.suffix == ".gz":
75
+ header = path.read_bytes()[:10]
76
+ self.assertEqual(header[:2], b"\x1f\x8b")
77
+ self.assertEqual(header[4:8], b"\x00\x00\x00\x00")
78
+
79
+ def test_04_anchor_union(self) -> None:
80
+ rows = read_tsv(OUTPUT / "review_a5_non_iar_successor_anchor_manifest.tsv.gz")
81
+ self.assertEqual(len(rows), 4625)
82
+ self.assertEqual(len({row["canonical_wurcs"] for row in rows}), 4625)
83
+ coverage = self.summary["current_nonfit_coverage"]
84
+ self.assertEqual(coverage["strict3_primary_missing_from_predecessor_4532"], 0)
85
+ self.assertEqual(coverage["k20_missing_from_predecessor_4532"], 0)
86
+ self.assertEqual(coverage["strict_affinose_missing_from_predecessor"], 93)
87
+ self.assertTrue(
88
+ self.summary["validation_checks"][
89
+ "all_current_strict_affinose_nonfit_wurcs_in_successor"
90
+ ]
91
+ )
92
+
93
+ def test_05_parent_exact_and_known_graph_counts(self) -> None:
94
+ rows = read_tsv(
95
+ OUTPUT
96
+ / "review_a5_non_iar_successor_exact_known_graph_parent_manifest.tsv.gz"
97
+ )
98
+ self.assertEqual(len(rows), 281208)
99
+ self.assertEqual(len({row["parent_glycorpus_id"] for row in rows}), 281208)
100
+ exact = sum(row["exact_wurcs_anchor"] == "True" for row in rows)
101
+ graph = sum(int(row["graph_identity_anchor_count"]) > 0 for row in rows)
102
+ graph_only = sum(
103
+ row["exact_wurcs_anchor"] == "False"
104
+ and int(row["graph_identity_anchor_count"]) > 0
105
+ for row in rows
106
+ )
107
+ excluded = sum(row["a5_excluded"] == "True" for row in rows)
108
+ self.assertEqual((exact, graph, graph_only, excluded), (3673, 3587, 160, 3833))
109
+
110
+ def test_06_only_newly_matched_parents_change_role(self) -> None:
111
+ old = {row["parent_glycorpus_id"]: row for row in read_tsv(PREDECESSOR)}
112
+ new = {
113
+ row["parent_glycorpus_id"]: row
114
+ for row in read_tsv(
115
+ OUTPUT / "review_a5_non_iar_successor_parent_partition.tsv.gz"
116
+ )
117
+ }
118
+ self.assertEqual(set(old), set(new))
119
+ transitions = Counter(
120
+ (old[parent]["pretraining_partition"], new[parent]["pretraining_partition"])
121
+ for parent in old
122
+ )
123
+ self.assertEqual(
124
+ transitions[(
125
+ "pretraining_train",
126
+ "excluded_exact_or_verified_graph_identity",
127
+ )],
128
+ 59,
129
+ )
130
+ self.assertEqual(
131
+ transitions[(
132
+ "pretraining_development",
133
+ "excluded_exact_or_verified_graph_identity",
134
+ )],
135
+ 14,
136
+ )
137
+ self.assertEqual(
138
+ sum(count for (before, after), count in transitions.items() if before != after),
139
+ 73,
140
+ )
141
+
142
+ def test_07_successor_role_counts_and_group_guards(self) -> None:
143
+ expected = {
144
+ "excluded_exact_or_verified_graph_identity": 3833,
145
+ "pretraining_development": 55476,
146
+ "pretraining_train": 221899,
147
+ }
148
+ self.assertEqual(self.summary["preserved_80_20_partition"]["parent_role_counts"], expected)
149
+ self.assertEqual(
150
+ self.summary["preserved_80_20_partition"]["retained_group_crossings"],
151
+ {
152
+ "precluster_union_block_id": 0,
153
+ "raw_feature_sha256": 0,
154
+ "registry_family_id": 0,
155
+ },
156
+ )
157
+
158
+ def test_08_gap_ledger(self) -> None:
159
+ rows = read_tsv(OUTPUT / "review_a5_non_iar_successor_later_role_gap.tsv.gz")
160
+ self.assertEqual(len(rows), 93)
161
+ self.assertEqual(Counter(row["strict_component_role"] for row in rows), {"validation": 62, "test": 31})
162
+ self.assertEqual(
163
+ Counter(row["sequence_v3_pretraining_partition"] for row in rows),
164
+ {"pretraining_train": 59, "pretraining_development": 14, "absent": 20},
165
+ )
166
+ self.assertEqual(
167
+ Counter(row["fingerprint_tier"] for row in rows),
168
+ {
169
+ "fixed_tree_exact_linkage": 81,
170
+ "composition_only": 6,
171
+ "fixed_tree_unknown_linkage": 3,
172
+ "partial_or_ambiguous_graph": 3,
173
+ },
174
+ )
175
+ self.assertTrue(all(row["known_graph_only_alias_parent_count"] == "0" for row in rows))
176
+
177
+ def test_09_changed_parent_ledger(self) -> None:
178
+ rows = read_tsv(OUTPUT / "review_a5_non_iar_successor_changed_parent_roles.tsv.gz")
179
+ self.assertEqual(len(rows), 73)
180
+ self.assertTrue(
181
+ all(
182
+ row["successor_pretraining_partition"]
183
+ == "excluded_exact_or_verified_graph_identity"
184
+ for row in rows
185
+ )
186
+ )
187
+
188
+ def test_10_objective_counts(self) -> None:
189
+ rows = read_tsv(OUTPUT / "review_a5_non_iar_successor_objective_partition.tsv.gz")
190
+ self.assertEqual(len(rows), 281208 * 5)
191
+ observed: Counter[tuple[str, str]] = Counter()
192
+ for row in rows:
193
+ if row["eligible_before_partition"] == "True" and row[
194
+ "pretraining_partition"
195
+ ] in {"pretraining_train", "pretraining_development"}:
196
+ observed[(row["pretraining_partition"], row["objective"])] += 1
197
+ for role, values in self.summary["base_objective_successor_counts"].items():
198
+ for objective, count in values.items():
199
+ self.assertEqual(observed[(role, objective)], count)
200
+
201
+
202
+ if __name__ == "__main__":
203
+ unittest.main()
releases/2026-10-02/bertose.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Portable loading of the unchanged BERTose sequence encoder and tokenizer.
2
+
3
+ This is a release adapter, not a new model implementation. The encoder class,
4
+ embedding/layer implementations and tokenizer are imported from preserved source.
5
+ """
6
+ from pathlib import Path
7
+ import hashlib
8
+ import importlib.util
9
+ import json
10
+ import sys
11
+
12
+ import torch
13
+
14
+ ROOT = Path(__file__).resolve().parent
15
+ RUNTIME = ROOT / "bertose_source/writing/review/multimodal/runtime"
16
+ TOKENIZER_DIR = ROOT / "bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1"
17
+
18
+
19
+ def sha256_file(path):
20
+ digest = hashlib.sha256()
21
+ with Path(path).open("rb") as handle:
22
+ for block in iter(lambda: handle.read(1024 * 1024), b""):
23
+ digest.update(block)
24
+ return digest.hexdigest()
25
+
26
+
27
+ def _module(name, path):
28
+ if name in sys.modules:
29
+ return sys.modules[name]
30
+ spec = importlib.util.spec_from_file_location(name, path)
31
+ module = importlib.util.module_from_spec(spec)
32
+ sys.modules[name] = module
33
+ spec.loader.exec_module(module)
34
+ return module
35
+
36
+
37
+ def load_tokenizer():
38
+ """Load the 2,114-token vocabulary and its frozen tree-channel tokenizer."""
39
+ vocabulary = TOKENIZER_DIR / "review_bpe_vocabulary.json"
40
+ manifest = json.loads((ROOT / "provenance/sequence_variants.json").read_text())
41
+ if sha256_file(vocabulary) != manifest["vocabulary_sha256"]:
42
+ raise ValueError("Vocabulary differs from the released model vocabulary")
43
+ shim = _module("_bertose_tokenizer_shim", ROOT / "affinose/a5_tokenizer_shim.py")
44
+ module = shim.install()["sequence_tree_runtime"]
45
+ return module.ReviewA5WURCSBPETokenizerV3(vocabulary)
46
+
47
+
48
+ def tokenize(wurcs, tokenizer=None):
49
+ """Encode WURCS 2.0 with the training envelope; reject overlength inputs.
50
+
51
+ Unknown characters remain [UNK], as in the frozen tokenizer. The returned
52
+ diagnostics expose their number and whether exact tree channels are available.
53
+ """
54
+ if not isinstance(wurcs, str) or not wurcs.startswith("WURCS=2.0/"):
55
+ raise ValueError("Provide a WURCS 2.0 string, not an IUPAC name or sequence")
56
+ tokenizer = tokenizer or load_tokenizer()
57
+ units = tokenizer.pre_tokenizer.pre_tokenize(wurcs)
58
+ length = sum(1 if unit["type"] == "special" else len(tokenizer.apply_bpe(unit["text"])) for unit in units)
59
+ if length > 256:
60
+ raise ValueError(f"Input has {length} tokens; the trained position limit is 256. No truncation was applied.")
61
+ result = tokenizer.tokenize_sequence(wurcs, max_length=256)
62
+ result["unknown_token_count"] = sum(i == tokenizer.token_to_id["[UNK]"] for i in result["token_ids"])
63
+ return result
64
+
65
+
66
+ def load_encoder(checkpoint, variant="3", device="cpu"):
67
+ """Strictly load a pinned sequence export using the original 209 state keys.
68
+
69
+ Returns (encoder, metadata). These exports have no task head or MLM head.
70
+ """
71
+ manifest = json.loads((ROOT / "provenance/sequence_variants.json").read_text())
72
+ variant = str(variant)
73
+ if variant in {"1", "2", "3", "4", "5"}:
74
+ variant = "WA" + variant
75
+ record = next((x for x in manifest["variants"] if x["variant"] == variant), None)
76
+ if record is None:
77
+ raise ValueError("variant must be 1, 2, 3, 4 or 5 (legacy WA1-WA5 aliases are also accepted)")
78
+ checkpoint = Path(checkpoint)
79
+ if checkpoint.stat().st_size != record["bytes"] or sha256_file(checkpoint) != record["sha256"]:
80
+ raise ValueError("Checkpoint bytes do not match the selected variant")
81
+ payload = torch.load(checkpoint, map_location="cpu", weights_only=True)
82
+ if (payload.get("schema") != "bertose_review_a5_native_md_on_sequence_export_20260808_r1"
83
+ or payload.get("cell") != "A1111" or payload.get("upstream_seed") != 42
84
+ or payload.get("selected_epoch") != record["selected_epoch"]):
85
+ raise ValueError("Unexpected sequence-export identity")
86
+ state = payload["model_state"]
87
+ checkpoint_module = _module("_bertose_exact_checkpoint", ROOT / "executed/downstream/review_a5_native_checkpoint.py")
88
+ checkpoint_module.validate_encoder_schema_fingerprint(state)
89
+ native = _module("_bertose_native_r4", RUNTIME / "a5_pretraining_failclosed_integration_20260721_r4/review_a5_native_eight_composite_r4.py")
90
+ config = native.NativeEightConfig(cell_id="A111", seq_vocab_size=2114)
91
+ # R5 inherits these exact R4 sequence modules. Allocate on meta so there is
92
+ # no random-initialization pass and no allocation of unused modality heads.
93
+ with torch.device("meta"):
94
+ embeddings = native.ExactSequenceEmbeddings(config)
95
+ layers = torch.nn.ModuleList([
96
+ native.EncoderLayer(config.seq_hidden_size, config.seq_num_heads,
97
+ config.hidden_dropout_prob, config.attention_dropout_prob,
98
+ config.layer_norm_eps)
99
+ for _ in range(config.seq_num_layers)
100
+ ])
101
+ encoder = checkpoint_module.ExactNativeSequenceEncoder(embeddings, layers)
102
+ encoder.load_state_dict(state, strict=True, assign=True)
103
+ encoder.to(device).eval().requires_grad_(False)
104
+ metadata = dict(record, state_entries=len(state), vocabulary_sha256=manifest["vocabulary_sha256"],
105
+ output="sequence-token states before any task-specific pooling/head")
106
+ return encoder, metadata
107
+
108
+
109
+ @torch.inference_mode()
110
+ def embed(wurcs_strings, encoder, tokenizer=None, pooling="first"):
111
+ """Return [N,768] embeddings and tokenizer diagnostics.
112
+
113
+ `first` uses the START envelope token. `mean` averages all attended tokens,
114
+ including the START and END envelope. Neither is a fitted benchmark head.
115
+ """
116
+ if not wurcs_strings or pooling not in {"first", "mean"}:
117
+ raise ValueError("Supply at least one WURCS string and pooling first or mean")
118
+ tokenizer = tokenizer or load_tokenizer()
119
+ records = [tokenize(text, tokenizer) for text in wurcs_strings]
120
+ device = next(encoder.parameters()).device
121
+ tensors = {key: torch.tensor([r[key] for r in records], dtype=torch.long, device=device)
122
+ for key in ("token_ids", "attention_mask", "branch_depths", "linkage_types")}
123
+ hidden = encoder(**tensors)
124
+ if pooling == "first":
125
+ vectors = hidden[:, 0]
126
+ else:
127
+ mask = tensors["attention_mask"].unsqueeze(-1)
128
+ vectors = (hidden * mask).sum(1) / mask.sum(1)
129
+ if not torch.isfinite(vectors).all():
130
+ raise ValueError("Encoder returned nonfinite embeddings")
131
+ return vectors.cpu().numpy(), [{k: r[k] for k in (
132
+ "tree_channels_valid", "tree_channel_failure_reason", "unknown_token_count")} for r in records]
releases/2026-10-02/bertose_source/bert_training_v4/downstream_tasks/utils/wurcs_bpe_tokenizer.py ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ WURCS-BPE Tokenizer
4
+
5
+ A hybrid tokenizer that learns semantic subwords from WURCS while preserving
6
+ the ability to handle rare/novel glycan structures character-by-character.
7
+
8
+ Key features:
9
+ 1. Pre-tokenization: Split WURCS into semantic units (residues, linkages, mods)
10
+ 2. BPE: Learn subword merges from corpus
11
+ 3. Character fallback: Handle novel structures
12
+ 4. Tree embeddings: Preserve branch_depth and linkage_type per token
13
+
14
+ Usage:
15
+ # Train BPE on corpus
16
+ tokenizer = WURCSBPETokenizer.train_from_corpus(
17
+ wurcs_strings,
18
+ num_merges=500,
19
+ output_path="bpe_vocabulary.json"
20
+ )
21
+
22
+ # Tokenize
23
+ result = tokenizer.tokenize(wurcs_string)
24
+ """
25
+
26
+ import json
27
+ import re
28
+ from collections import Counter, defaultdict
29
+ from pathlib import Path
30
+ from typing import Dict, List, Optional, Tuple, Set
31
+ import pickle
32
+
33
+
34
+ class WURCSPreTokenizer:
35
+ """
36
+ Pre-tokenize WURCS into semantic units before BPE.
37
+
38
+ WURCS format: WURCS=2.0/count/[residues]/indices/linkages
39
+
40
+ We split into:
41
+ - Residues: [a2122h-1b_1-5_2*NCC/3=O] -> one unit per []
42
+ - Linkages: a4-b1 -> one unit per linkage
43
+ - Special markers: [BRANCH_OPEN], [BRANCH_CLOSE], etc.
44
+ """
45
+
46
+ # Residue patterns for common monosaccharides
47
+ RESIDUE_PATTERN = re.compile(r'\[([^\]]+)\]')
48
+ LINKAGE_PATTERN = re.compile(r'([a-z])(\d+|\?)-([a-z])(\d+|\?)')
49
+
50
+ def __init__(self):
51
+ self.special_tokens = {
52
+ '[PAD]': 0,
53
+ '[UNK]': 1,
54
+ '[START]': 2,
55
+ '[END]': 3,
56
+ '[MASK]': 4,
57
+ '[BRANCH_OPEN]': 5,
58
+ '[BRANCH_CLOSE]': 6,
59
+ '[LINK]': 7,
60
+ '[MOD]': 8,
61
+ '[RESIDUE_ERROR]': 9,
62
+ }
63
+
64
+ def pre_tokenize(self, wurcs: str) -> List[Dict]:
65
+ """
66
+ Pre-tokenize WURCS into semantic units.
67
+
68
+ Returns list of dicts with:
69
+ - text: The unit text
70
+ - type: 'special', 'residue', 'linkage', 'mod', 'index'
71
+ - residue_id: Which residue this belongs to (-1 for special, -2 for linkage)
72
+ - branch_depth: Tree depth (computed later)
73
+ """
74
+ units = []
75
+
76
+ # Add start token
77
+ units.append({
78
+ 'text': '[START]',
79
+ 'type': 'special',
80
+ 'residue_id': -1,
81
+ 'branch_depth': 0,
82
+ 'linkage_type': 0,
83
+ })
84
+
85
+ # Parse WURCS sections
86
+ if not wurcs.startswith('WURCS='):
87
+ units.append({'text': '[RESIDUE_ERROR]', 'type': 'special', 'residue_id': -1, 'branch_depth': 0, 'linkage_type': 0})
88
+ units.append({'text': '[END]', 'type': 'special', 'residue_id': -1, 'branch_depth': 0, 'linkage_type': 0})
89
+ return units
90
+
91
+ try:
92
+ parts = self._split_wurcs_sections(wurcs)
93
+ if len(parts) < 4:
94
+ return [{'text': '[ERROR]', 'type': 'special', 'residue_id': -1, 'branch_depth': 0, 'linkage_type': 0}]
95
+
96
+ # parts: WURCS=2.0/3,3,2/[a2122h-1b_1-5][a2122h-1a_1-5][a1122h-1b_1-5]/1-2-3-1/a4-b1_b3-c1_c4-d1
97
+ # section 2: residue definitions
98
+ # section 3: indices
99
+ # section 4: linkages (optional)
100
+
101
+ version = parts[0] # WURCS=2.0
102
+ counts = parts[1] # residue_count,node_count,link_count
103
+ residue_defs = parts[2] # [res1][res2]...
104
+ indices = parts[3] # 1-2-3-1
105
+ linkages = parts[4] if len(parts) > 4 else "" # a4-b1_b3-c1
106
+
107
+ # Parse residue definitions
108
+ residue_list = self.RESIDUE_PATTERN.findall(residue_defs)
109
+
110
+ # Parse linkages to compute branch structure
111
+ linkage_list = linkages.split('_') if linkages else []
112
+ branch_points, residue_depths, linkage_types_map, adj = self._analyze_tree_structure(linkage_list, num_residues=len(residue_list))
113
+
114
+ # Compute distance matrix and cache it based on the linkage string (structure)
115
+ # This is the most expensive part, so we cache it
116
+ if not hasattr(self, '_dist_cache'): self._dist_cache = {}
117
+ if linkages not in self._dist_cache:
118
+ self._dist_cache[linkages] = self._compute_distance_matrix(adj, len(residue_list))
119
+ dist_matrix_raw = self._dist_cache[linkages]
120
+
121
+ # Parse indices to map positions to residue definitions
122
+ index_list = indices.split('-') if indices else []
123
+
124
+ # Process each residue instance
125
+ residue_letter = ord('a')
126
+ for idx, res_idx in enumerate(index_list):
127
+ current_residue_id = idx
128
+ res_letter = chr(residue_letter + idx)
129
+
130
+ # Check if this is a branch point - add branch marker before
131
+ if res_letter in branch_points and branch_points[res_letter] > 0:
132
+ for _ in range(branch_points[res_letter]):
133
+ units.append({
134
+ 'text': '[BRANCH_OPEN]',
135
+ 'type': 'special',
136
+ 'residue_id': -1,
137
+ 'branch_depth': residue_depths.get(res_letter, 0),
138
+ 'linkage_type': 0,
139
+ })
140
+
141
+ # Get residue definition
142
+ try:
143
+ res_def_idx = int(res_idx) - 1 # 1-indexed to 0-indexed
144
+ res_def = residue_list[res_def_idx] if res_def_idx < len(residue_list) else ""
145
+ except (ValueError, IndexError):
146
+ res_def = ""
147
+
148
+ # Split residue into base and modifications
149
+ res_parts = res_def.split('_')
150
+ base = res_parts[0] if res_parts else res_def
151
+ mods = res_parts[1:] if len(res_parts) > 1 else []
152
+
153
+ # Add residue base as a single unit
154
+ depth = residue_depths.get(res_letter, 0)
155
+ units.append({
156
+ 'text': base,
157
+ 'type': 'residue',
158
+ 'residue_id': current_residue_id,
159
+ 'branch_depth': depth,
160
+ 'linkage_type': 0,
161
+ })
162
+
163
+ # Add modifications
164
+ for mod in mods:
165
+ units.append({
166
+ 'text': mod,
167
+ 'type': 'mod',
168
+ 'residue_id': current_residue_id,
169
+ 'branch_depth': depth,
170
+ 'linkage_type': 0,
171
+ })
172
+
173
+ # Store distance matrix in units for easy access in tokenizer
174
+ if units:
175
+ # Find first residue unit or just use START
176
+ units[0]['distance_matrix'] = dist_matrix_raw
177
+
178
+ # Add linkages
179
+ for link in linkage_list:
180
+ if not link:
181
+ continue
182
+ # Parse linkage type
183
+ lt = self._parse_linkage_type(link)
184
+ units.append({
185
+ 'text': link,
186
+ 'type': 'linkage',
187
+ 'residue_id': -2,
188
+ 'branch_depth': 0,
189
+ 'linkage_type': lt,
190
+ })
191
+ except Exception:
192
+ # Fallback for truly broken WURCS
193
+ pass
194
+
195
+ # Add end token
196
+ units.append({
197
+ 'text': '[END]',
198
+ 'type': 'special',
199
+ 'residue_id': -1,
200
+ 'branch_depth': 0,
201
+ 'linkage_type': 0,
202
+ })
203
+
204
+ return units
205
+
206
+ def _split_wurcs_sections(self, wurcs: str) -> List[str]:
207
+ """Split WURCS string into sections, handling nested brackets."""
208
+ # Remove WURCS= prefix
209
+ if wurcs.startswith('WURCS='):
210
+ wurcs = wurcs[6:]
211
+
212
+ sections = []
213
+ current = ""
214
+ bracket_depth = 0
215
+
216
+ for char in wurcs:
217
+ if char == '[':
218
+ bracket_depth += 1
219
+ current += char
220
+ elif char == ']':
221
+ bracket_depth -= 1
222
+ current += char
223
+ elif char == '/' and bracket_depth == 0:
224
+ sections.append(current)
225
+ current = ""
226
+ else:
227
+ current += char
228
+
229
+ if current:
230
+ sections.append(current)
231
+
232
+ return sections
233
+
234
+ def _analyze_tree_structure(self, linkages: List[str], num_residues: int) -> Tuple[Dict, Dict, Dict, Dict]:
235
+ """Analyze linkages to compute branch points and residue depths."""
236
+ branch_points = defaultdict(int) # residue -> number of children
237
+ children = defaultdict(list)
238
+ all_residues = set()
239
+ linkage_types = {}
240
+
241
+ for link in linkages:
242
+ match = self.LINKAGE_PATTERN.match(link)
243
+ if match:
244
+ from_res, from_pos, to_res, to_pos = match.groups()
245
+ children[from_res].append(to_res)
246
+ all_residues.add(from_res)
247
+ all_residues.add(to_res)
248
+
249
+ # Store linkage type
250
+ linkage_types[link] = self._parse_linkage_type(link)
251
+
252
+ # Build adjacency list for BFS
253
+ adj = defaultdict(list)
254
+ for link in linkages:
255
+ match = self.LINKAGE_PATTERN.match(link)
256
+ if match:
257
+ u = ord(match.group(1)) - ord('a')
258
+ v = ord(match.group(3)) - ord('a')
259
+ if 0 <= u < num_residues and 0 <= v < num_residues:
260
+ adj[u].append(v)
261
+ adj[v].append(u)
262
+
263
+ # Find branch points (residues with >1 child)
264
+ for res, kids in children.items():
265
+ if len(kids) > 1:
266
+ branch_points[res] = len(kids) - 1 # Number of extra branches
267
+
268
+ # Compute depths using BFS
269
+ # Find root (residue with no parent)
270
+ child_set = set()
271
+ for kids in children.values():
272
+ child_set.update(kids)
273
+ roots = all_residues - child_set
274
+ root = min(roots) if roots else 'a'
275
+
276
+ depths = {root: 0}
277
+ queue = [root]
278
+ while queue:
279
+ current = queue.pop(0)
280
+ for child in children.get(current, []):
281
+ if child not in depths:
282
+ depths[child] = depths[current] + 1
283
+ queue.append(child)
284
+
285
+ return branch_points, depths, linkage_types, adj
286
+
287
+ def _compute_distance_matrix(self, adj: Dict[int, List[int]], num_residues: int) -> List[List[int]]:
288
+ """
289
+ Compute shortest path distance (number of bonds) between all residue pairs using BFS.
290
+ """
291
+ if num_residues == 0:
292
+ return []
293
+
294
+ dist_matrix = [[-1] * num_residues for _ in range(num_residues)]
295
+
296
+ for i in range(num_residues):
297
+ dist_matrix[i][i] = 0
298
+ queue = [(i, 0)]
299
+ visited = {i}
300
+
301
+ while queue:
302
+ curr, d = queue.pop(0)
303
+ dist_matrix[i][curr] = d
304
+
305
+ for neighbor in adj[curr]:
306
+ if neighbor not in visited:
307
+ visited.add(neighbor)
308
+ queue.append((neighbor, d + 1))
309
+
310
+ return dist_matrix
311
+
312
+ def _compute_distance_matrix_OLD(self, linkages: List[str], num_residues: int) -> List[List[int]]:
313
+ """
314
+ Compute shortest path distance (number of bonds) between all residue pairs.
315
+ Returns a symmetric N x N matrix where N is num_residues.
316
+ Values are integers (number of steps). 0 on diagonal. -1 if unreachable (shouldn't happen in single tree).
317
+ """
318
+ if num_residues == 0:
319
+ return []
320
+
321
+ # Initialize adjacency list
322
+ adj = defaultdict(list)
323
+ for link in linkages:
324
+ match = self.LINKAGE_PATTERN.match(link)
325
+ if match:
326
+ # WURCS indices are 1-based letters (a=1, b=2...)
327
+ from_res_char, _, to_res_char, _ = match.groups()
328
+ # Convert char to 0-based index
329
+ u = ord(from_res_char) - ord('a')
330
+ v = ord(to_res_char) - ord('a')
331
+
332
+ # Undirected graph for structural distance
333
+ if 0 <= u < num_residues and 0 <= v < num_residues:
334
+ adj[u].append(v)
335
+ adj[v].append(u)
336
+
337
+ # Compute All-Pairs Shortest Path (BFS from each node is fine for small N)
338
+ # Glycans are small (N ~ 5-20 usually), so O(N^2) BFS is cheap.
339
+ dist_matrix = [[-1] * num_residues for _ in range(num_residues)]
340
+
341
+ for i in range(num_residues):
342
+ dist_matrix[i][i] = 0
343
+ queue = [(i, 0)]
344
+ visited = {i}
345
+
346
+ while queue:
347
+ curr, d = queue.pop(0)
348
+ dist_matrix[i][curr] = d
349
+
350
+ for neighbor in adj[curr]:
351
+ if neighbor not in visited:
352
+ visited.add(neighbor)
353
+ queue.append((neighbor, d + 1))
354
+
355
+ return dist_matrix
356
+
357
+ def _parse_linkage_type(self, link: str) -> int:
358
+ """Parse linkage string to get type ID."""
359
+ LINKAGE_TYPES = {
360
+ (1, 2): 0, (2, 1): 0,
361
+ (1, 3): 1, (3, 1): 1,
362
+ (1, 4): 2, (4, 1): 2,
363
+ (1, 6): 3, (6, 1): 3,
364
+ (2, 3): 4, (3, 2): 4,
365
+ (2, 6): 5, (6, 2): 5,
366
+ (3, 6): 6, (6, 3): 6,
367
+ }
368
+
369
+ match = self.LINKAGE_PATTERN.match(link)
370
+ if match:
371
+ _, from_pos, _, to_pos = match.groups()
372
+ try:
373
+ pos_tuple = (int(from_pos), int(to_pos))
374
+ return LINKAGE_TYPES.get(pos_tuple, 7)
375
+ except ValueError:
376
+ return 8 # Unknown
377
+ return 8
378
+
379
+
380
+ class WURCSBPETokenizer:
381
+ """
382
+ BPE tokenizer for WURCS with tree-aware embeddings.
383
+ """
384
+
385
+ def __init__(self, vocab_path: Optional[str] = None):
386
+ self.pre_tokenizer = WURCSPreTokenizer()
387
+
388
+ # Special tokens (fixed)
389
+ self.special_tokens = self.pre_tokenizer.special_tokens
390
+
391
+ # BPE vocabulary
392
+ self.token_to_id: Dict[str, int] = {}
393
+ self.id_to_token: Dict[int, str] = {}
394
+ self.merges: List[Tuple[str, str]] = []
395
+
396
+ if vocab_path:
397
+ self.load_vocab(vocab_path)
398
+ else:
399
+ # Initialize with special tokens only
400
+ self.token_to_id = dict(self.special_tokens)
401
+ self.id_to_token = {v: k for k, v in self.token_to_id.items()}
402
+
403
+ @classmethod
404
+ def train_from_corpus(
405
+ cls,
406
+ wurcs_strings: List[str],
407
+ num_merges: int = 500,
408
+ output_path: Optional[str] = None,
409
+ min_frequency: int = 2,
410
+ max_token_length: Optional[int] = None,
411
+ ) -> 'WURCSBPETokenizer':
412
+ """
413
+ Train BPE on a corpus of WURCS strings.
414
+
415
+ Args:
416
+ wurcs_strings: List of WURCS strings
417
+ num_merges: Number of BPE merge operations
418
+ output_path: Optional path to save vocabulary
419
+ min_frequency: Minimum frequency for a token to be kept
420
+ max_token_length: Maximum length of a merged token (None = no limit)
421
+
422
+ Returns:
423
+ Trained tokenizer
424
+ """
425
+ tokenizer = cls()
426
+ pre_tok = WURCSPreTokenizer()
427
+
428
+ print(f"Training BPE on {len(wurcs_strings)} WURCS strings...")
429
+
430
+ # Step 1: Pre-tokenize all strings to get semantic units
431
+ all_units = []
432
+ for wurcs in wurcs_strings:
433
+ units = pre_tok.pre_tokenize(wurcs)
434
+ for unit in units:
435
+ if unit['type'] != 'special':
436
+ all_units.append(unit['text'])
437
+
438
+ # Step 2: Count unit frequencies
439
+ unit_counts = Counter(all_units)
440
+ print(f"Found {len(unit_counts)} unique units")
441
+
442
+ # Step 3: Initialize vocabulary with characters from all units
443
+ char_vocab = set()
444
+ for unit in unit_counts:
445
+ for char in unit:
446
+ char_vocab.add(char)
447
+
448
+ # Build initial vocab: special tokens + characters
449
+ vocab_id = len(tokenizer.special_tokens)
450
+ for char in sorted(char_vocab):
451
+ tokenizer.token_to_id[char] = vocab_id
452
+ tokenizer.id_to_token[vocab_id] = char
453
+ vocab_id += 1
454
+
455
+ print(f"Initial vocab size: {vocab_id} (special + characters)")
456
+
457
+ # Step 4: Convert units to character sequences
458
+ word_freqs = {}
459
+ for unit, count in unit_counts.items():
460
+ if count >= min_frequency:
461
+ # Split into characters with space separator
462
+ chars = tuple(unit)
463
+ word_freqs[chars] = count
464
+
465
+ # Step 5: BPE merging
466
+ merges = []
467
+
468
+ for merge_idx in range(num_merges):
469
+ # Count pairs
470
+ pair_counts = Counter()
471
+ for word, freq in word_freqs.items():
472
+ for i in range(len(word) - 1):
473
+ pair = (word[i], word[i + 1])
474
+ pair_counts[pair] += freq
475
+
476
+ if not pair_counts:
477
+ break
478
+
479
+ # Find most frequent pair
480
+ best_pair = pair_counts.most_common(1)[0][0]
481
+ best_count = pair_counts[best_pair]
482
+
483
+ if best_count < min_frequency:
484
+ break
485
+
486
+ # Merge pair
487
+ new_token = best_pair[0] + best_pair[1]
488
+
489
+ # Check length constraint
490
+ if max_token_length and len(new_token) > max_token_length:
491
+ # remove this pair from consideration for this iteration and future?
492
+ # Actually, skipping it here is tricky because we need to ignore it in pair_counts next time
493
+ # Simpler: Just skip adding it to merges and modify word_freqs?
494
+ # No, if we don't merge, we just continue to the next best pair in THIS iteration.
495
+ # But pair_counts is already computed.
496
+ # We need to loop until we find a valid pair or run out
497
+
498
+ # In this simple implementation, let's just skip this merge efficiently
499
+ # We need to find the NEXT most common pair.
500
+
501
+ # Re-do finding best pair loop
502
+ found_valid_pair = False
503
+ for pair, count in pair_counts.most_common():
504
+ token_candidate = pair[0] + pair[1]
505
+ if max_token_length and len(token_candidate) > max_token_length:
506
+ continue # Skip too long
507
+
508
+ if count < min_frequency:
509
+ break # Stop if frequency too low
510
+
511
+ # Found valid pair
512
+ best_pair = pair
513
+ best_count = count
514
+ new_token = token_candidate
515
+ found_valid_pair = True
516
+ break
517
+
518
+ if not found_valid_pair:
519
+ print(f" Stopping early: No more pairs satisfy max_token_length={max_token_length}")
520
+ break
521
+
522
+ # Final check before merging (in case we didn't enter the if block but updated vars)
523
+ # Actually the logic above handles it. If we entered the block, we either found a new best_pair or broke.
524
+
525
+ merges.append(best_pair)
526
+
527
+ # Add to vocab
528
+ tokenizer.token_to_id[new_token] = vocab_id
529
+ tokenizer.id_to_token[vocab_id] = new_token
530
+ vocab_id += 1
531
+
532
+ # Update word_freqs
533
+ new_word_freqs = {}
534
+ for word, freq in word_freqs.items():
535
+ new_word = []
536
+ i = 0
537
+ while i < len(word):
538
+ if i < len(word) - 1 and word[i] == best_pair[0] and word[i + 1] == best_pair[1]:
539
+ new_word.append(new_token)
540
+ i += 2
541
+ else:
542
+ new_word.append(word[i])
543
+ i += 1
544
+ new_word_freqs[tuple(new_word)] = freq
545
+ word_freqs = new_word_freqs
546
+
547
+ if (merge_idx + 1) % 100 == 0:
548
+ print(f" Merge {merge_idx + 1}/{num_merges}: '{best_pair[0]}' + '{best_pair[1]}' -> '{new_token}' (count={best_count})")
549
+
550
+ tokenizer.merges = merges
551
+ print(f"Final vocab size: {len(tokenizer.token_to_id)}")
552
+
553
+ # Save if requested
554
+ if output_path:
555
+ tokenizer.save_vocab(output_path)
556
+
557
+ return tokenizer
558
+
559
+ def apply_bpe(self, text: str) -> List[str]:
560
+ """Apply BPE merges to a text string."""
561
+ if text in self.token_to_id:
562
+ return [text]
563
+
564
+ # Split into characters
565
+ tokens = list(text)
566
+
567
+ # Apply merges
568
+ for pair in self.merges:
569
+ new_tokens = []
570
+ i = 0
571
+ while i < len(tokens):
572
+ if i < len(tokens) - 1 and tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
573
+ new_tokens.append(pair[0] + pair[1])
574
+ i += 2
575
+ else:
576
+ new_tokens.append(tokens[i])
577
+ i += 1
578
+ tokens = new_tokens
579
+
580
+ return tokens
581
+
582
+ def tokenize(self, wurcs: str, max_length: int = 256) -> Dict:
583
+ """
584
+ Tokenize a WURCS string.
585
+
586
+ Returns:
587
+ Dict with:
588
+ - tokens: List of token strings
589
+ - token_ids: List of token IDs
590
+ - residue_ids: List of residue IDs
591
+ - branch_depths: List of branch depths
592
+ - linkage_types: List of linkage types
593
+ - attention_mask: Attention mask
594
+ """
595
+ # Pre-tokenize
596
+ units = self.pre_tokenizer.pre_tokenize(wurcs)
597
+
598
+ tokens = []
599
+ token_ids = []
600
+ residue_ids = []
601
+ branch_depths = []
602
+ linkage_types = []
603
+
604
+ for unit in units:
605
+ if unit['type'] == 'special':
606
+ # Special tokens stay as-is
607
+ tok = unit['text']
608
+ tokens.append(tok)
609
+ token_ids.append(self.token_to_id.get(tok, self.token_to_id['[UNK]']))
610
+ residue_ids.append(unit['residue_id'])
611
+ branch_depths.append(unit['branch_depth'])
612
+ linkage_types.append(unit['linkage_type'])
613
+ else:
614
+ # Apply BPE to this unit
615
+ bpe_tokens = self.apply_bpe(unit['text'])
616
+ for tok in bpe_tokens:
617
+ tokens.append(tok)
618
+ token_ids.append(self.token_to_id.get(tok, self.token_to_id['[UNK]']))
619
+ residue_ids.append(unit['residue_id'])
620
+ branch_depths.append(unit['branch_depth'])
621
+ linkage_types.append(unit['linkage_type'])
622
+
623
+ # Truncate if needed
624
+ if len(tokens) > max_length:
625
+ tokens = tokens[:max_length - 1] + ['[END]']
626
+ token_ids = token_ids[:max_length - 1] + [self.token_to_id['[END]']]
627
+ residue_ids = residue_ids[:max_length - 1] + [-1]
628
+ branch_depths = branch_depths[:max_length - 1] + [0]
629
+ linkage_types = linkage_types[:max_length - 1] + [0]
630
+
631
+ # Create attention mask and pad
632
+ length = len(tokens)
633
+ attention_mask = [1] * length
634
+
635
+ while len(tokens) < max_length:
636
+ tokens.append('[PAD]')
637
+ token_ids.append(self.token_to_id['[PAD]'])
638
+ residue_ids.append(-1)
639
+ branch_depths.append(0)
640
+ linkage_types.append(0)
641
+ attention_mask.append(0)
642
+ # Pre-tokenize
643
+ units = self.pre_tokenizer.pre_tokenize(wurcs)
644
+
645
+ # Extract distance matrix from pre-tokenizer result
646
+ dist_matrix_raw = units[0].get('distance_matrix', [])
647
+ num_residues = len(dist_matrix_raw)
648
+
649
+ # Map token-to-token distances using residue_ids
650
+ # token_i is associated with residue_ids[i].
651
+ # residue_ids[i] is index into dist_matrix_raw.
652
+ # If residue_ids[i] == -1 (special), distance is undefined (use -1 or 999)
653
+
654
+ # Use UNPADDED length for distance matrix to save massive memory
655
+ # distance_matrix will be e.g. 20x20, while tokens are padded to 256
656
+ token_len = length
657
+ distance_matrix = [[-1] * token_len for _ in range(token_len)]
658
+
659
+ for i in range(token_len):
660
+ for j in range(token_len):
661
+ r_i = residue_ids[i]
662
+ r_j = residue_ids[j]
663
+
664
+ if r_i >= 0 and r_j >= 0 and r_i < num_residues and r_j < num_residues:
665
+ distance_matrix[i][j] = dist_matrix_raw[r_i][r_j]
666
+ else:
667
+ distance_matrix[i][j] = -1 # Special/Padding
668
+
669
+ # MEMORY OPTIMIZATION: Do NOT pad matrix here.
670
+ # Pad on-the-fly in Dataset class instead.
671
+ # This saves massive memory (0.2GB vs 66GB).
672
+
673
+ return {
674
+ 'tokens': tokens,
675
+ 'token_ids': token_ids,
676
+ 'residue_ids': residue_ids,
677
+ 'branch_depths': branch_depths,
678
+ 'linkage_types': linkage_types,
679
+ 'attention_mask': attention_mask,
680
+ 'distance_matrix': distance_matrix, # New Output
681
+ 'length': length,
682
+ }
683
+
684
+ def save_vocab(self, path: str):
685
+ """Save vocabulary to JSON file."""
686
+ data = {
687
+ 'special_tokens': self.special_tokens,
688
+ 'token_to_id': self.token_to_id,
689
+ 'merges': [list(m) for m in self.merges],
690
+ 'metadata': {
691
+ 'vocab_size': len(self.token_to_id),
692
+ 'num_merges': len(self.merges),
693
+ }
694
+ }
695
+ with open(path, 'w') as f:
696
+ json.dump(data, f, indent=2)
697
+ print(f"Saved vocabulary to {path}")
698
+
699
+ def load_vocab(self, path: str):
700
+ """Load vocabulary from JSON file."""
701
+ with open(path, 'r') as f:
702
+ data = json.load(f)
703
+
704
+ self.special_tokens = data['special_tokens']
705
+ self.token_to_id = data['token_to_id']
706
+ self.id_to_token = {int(v): k for k, v in self.token_to_id.items()}
707
+ self.merges = [tuple(m) for m in data['merges']]
708
+
709
+ print(f"Loaded vocabulary with {len(self.token_to_id)} tokens")
710
+
711
+ @property
712
+ def vocab_size(self) -> int:
713
+ return len(self.token_to_id)
714
+
715
+
716
+ # ============================================================================
717
+ # Testing
718
+ # ============================================================================
719
+
720
+ if __name__ == '__main__':
721
+ # Test pre-tokenizer
722
+ print("="*80)
723
+ print("Testing WURCSPreTokenizer")
724
+ print("="*80)
725
+
726
+ pre_tok = WURCSPreTokenizer()
727
+
728
+ test_wurcs = [
729
+ "WURCS=2.0/2,2,1/[a2122h-1b_1-5][a2211m-1a_1-5]/1-2/a4-b1",
730
+ "WURCS=2.0/3,3,2/[a2122h-1b_1-5_2*NCC/3=O][a2112h-1a_1-5][a2211m-1a_1-5]/1-2-3/a4-b1_b3-c1",
731
+ ]
732
+
733
+ for wurcs in test_wurcs:
734
+ print(f"\nWURCS: {wurcs[:60]}...")
735
+ units = pre_tok.pre_tokenize(wurcs)
736
+ print(f"Units ({len(units)}):")
737
+ for u in units[:10]:
738
+ print(f" {u['type']:10} | res={u['residue_id']:2} | depth={u['branch_depth']} | {u['text']}")
739
+ if len(units) > 10:
740
+ print(f" ... and {len(units) - 10} more")
releases/2026-10-02/bertose_source/bert_training_v4/model/__init__.py ADDED
File without changes
releases/2026-10-02/bertose_source/bert_training_v4/model/glycan_bert.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Glycan BERT Model
3
+
4
+ Transformer-based masked language model for glycan structures.
5
+ Based on BERT/ESM2 architecture adapted for atomic glycan tokenization.
6
+ """
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import math
11
+
12
+
13
+ class GlycanBERTConfig:
14
+ """Configuration for GlycanBERT."""
15
+
16
+ def __init__(
17
+ self,
18
+ vocab_size: int = 102,
19
+ hidden_size: int = 384,
20
+ num_hidden_layers: int = 6,
21
+ num_attention_heads: int = 6,
22
+ intermediate_size: int = 1536,
23
+ hidden_dropout_prob: float = 0.1,
24
+ attention_probs_dropout_prob: float = 0.1,
25
+ max_position_embeddings: int = 512,
26
+ layer_norm_eps: float = 1e-12,
27
+ pad_token_id: int = 0,
28
+ mask_token_id: int = 4,
29
+ initializer_range: float = 0.02
30
+ ):
31
+ self.vocab_size = vocab_size
32
+ self.hidden_size = hidden_size
33
+ self.num_hidden_layers = num_hidden_layers
34
+ self.num_attention_heads = num_attention_heads
35
+ self.intermediate_size = intermediate_size
36
+ self.hidden_dropout_prob = hidden_dropout_prob
37
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
38
+ self.max_position_embeddings = max_position_embeddings
39
+ self.layer_norm_eps = layer_norm_eps
40
+ self.pad_token_id = pad_token_id
41
+ self.mask_token_id = mask_token_id
42
+ self.initializer_range = initializer_range
43
+
44
+
45
+ class GlycanBERTEmbeddings(nn.Module):
46
+ """
47
+ Embeddings for glycan tokens including token and positional embeddings.
48
+ """
49
+
50
+ def __init__(self, config: GlycanBERTConfig):
51
+ super().__init__()
52
+ self.token_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
53
+ self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
54
+
55
+ self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
56
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
57
+
58
+ # position_ids (1, max_seq_len) is contiguous in memory and exported when serialized
59
+ self.register_buffer("position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)))
60
+
61
+ def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
62
+ """
63
+ Args:
64
+ input_ids: Tensor of shape (batch_size, seq_len)
65
+
66
+ Returns:
67
+ Embeddings of shape (batch_size, seq_len, hidden_size)
68
+ """
69
+ batch_size, seq_len = input_ids.shape
70
+
71
+ # Token embeddings
72
+ token_embeds = self.token_embeddings(input_ids)
73
+
74
+ # Position embeddings
75
+ position_ids = self.position_ids[:, :seq_len]
76
+ position_embeds = self.position_embeddings(position_ids)
77
+
78
+ # Combine
79
+ embeddings = token_embeds + position_embeds
80
+ embeddings = self.LayerNorm(embeddings)
81
+ embeddings = self.dropout(embeddings)
82
+
83
+ return embeddings
84
+
85
+
86
+ class GlycanBERTAttention(nn.Module):
87
+ """Multi-head self-attention."""
88
+
89
+ def __init__(self, config: GlycanBERTConfig):
90
+ super().__init__()
91
+ assert config.hidden_size % config.num_attention_heads == 0
92
+
93
+ self.num_attention_heads = config.num_attention_heads
94
+ self.attention_head_size = config.hidden_size // config.num_attention_heads
95
+ self.all_head_size = self.num_attention_heads * self.attention_head_size
96
+
97
+ self.query = nn.Linear(config.hidden_size, self.all_head_size)
98
+ self.key = nn.Linear(config.hidden_size, self.all_head_size)
99
+ self.value = nn.Linear(config.hidden_size, self.all_head_size)
100
+
101
+ self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
102
+
103
+ def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor:
104
+ """Reshape for multi-head attention."""
105
+ new_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
106
+ x = x.view(*new_shape)
107
+ return x.permute(0, 2, 1, 3) # (batch, heads, seq_len, head_size)
108
+
109
+ def forward(
110
+ self,
111
+ hidden_states: torch.Tensor,
112
+ attention_mask: torch.Tensor = None
113
+ ) -> torch.Tensor:
114
+ """
115
+ Args:
116
+ hidden_states: (batch_size, seq_len, hidden_size)
117
+ attention_mask: (batch_size, seq_len) - 1 for valid, 0 for padding
118
+
119
+ Returns:
120
+ Attention output: (batch_size, seq_len, hidden_size)
121
+ """
122
+ batch_size, seq_len, _ = hidden_states.shape
123
+
124
+ # Linear projections
125
+ query_layer = self.transpose_for_scores(self.query(hidden_states))
126
+ key_layer = self.transpose_for_scores(self.key(hidden_states))
127
+ value_layer = self.transpose_for_scores(self.value(hidden_states))
128
+
129
+ # Attention scores
130
+ attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
131
+ attention_scores = attention_scores / math.sqrt(self.attention_head_size)
132
+
133
+ # Apply attention mask
134
+ if attention_mask is not None:
135
+ # Convert mask to additive mask
136
+ attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) # (batch, 1, 1, seq_len)
137
+ attention_mask = (1.0 - attention_mask) * -10000.0
138
+ attention_scores = attention_scores + attention_mask
139
+
140
+ # Attention probabilities
141
+ attention_probs = nn.functional.softmax(attention_scores, dim=-1)
142
+ attention_probs = self.dropout(attention_probs)
143
+
144
+ # Apply attention to values
145
+ context_layer = torch.matmul(attention_probs, value_layer)
146
+
147
+ # Reshape back
148
+ context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
149
+ new_shape = context_layer.size()[:-2] + (self.all_head_size,)
150
+ context_layer = context_layer.view(*new_shape)
151
+
152
+ return context_layer
153
+
154
+
155
+ class GlycanBERTLayer(nn.Module):
156
+ """Single transformer layer."""
157
+
158
+ def __init__(self, config: GlycanBERTConfig):
159
+ super().__init__()
160
+ self.attention = GlycanBERTAttention(config)
161
+ self.attention_output = nn.Linear(config.hidden_size, config.hidden_size)
162
+ self.attention_layer_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
163
+
164
+ self.intermediate = nn.Linear(config.hidden_size, config.intermediate_size)
165
+ self.output = nn.Linear(config.intermediate_size, config.hidden_size)
166
+ self.output_layer_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
167
+
168
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
169
+
170
+ def forward(
171
+ self,
172
+ hidden_states: torch.Tensor,
173
+ attention_mask: torch.Tensor = None
174
+ ) -> torch.Tensor:
175
+ """
176
+ Args:
177
+ hidden_states: (batch_size, seq_len, hidden_size)
178
+ attention_mask: (batch_size, seq_len)
179
+
180
+ Returns:
181
+ Output: (batch_size, seq_len, hidden_size)
182
+ """
183
+ # Self-attention
184
+ attention_output = self.attention(hidden_states, attention_mask)
185
+ attention_output = self.attention_output(attention_output)
186
+ attention_output = self.dropout(attention_output)
187
+
188
+ # Add & Norm
189
+ hidden_states = self.attention_layer_norm(hidden_states + attention_output)
190
+
191
+ # Feed-forward
192
+ intermediate_output = self.intermediate(hidden_states)
193
+ intermediate_output = nn.functional.gelu(intermediate_output)
194
+
195
+ layer_output = self.output(intermediate_output)
196
+ layer_output = self.dropout(layer_output)
197
+
198
+ # Add & Norm
199
+ layer_output = self.output_layer_norm(hidden_states + layer_output)
200
+
201
+ return layer_output
202
+
203
+
204
+ class GlycanBERT(nn.Module):
205
+ """
206
+ Glycan BERT model for masked language modeling.
207
+ """
208
+
209
+ def __init__(self, config: GlycanBERTConfig):
210
+ super().__init__()
211
+ self.config = config
212
+
213
+ # Embeddings
214
+ self.embeddings = GlycanBERTEmbeddings(config)
215
+
216
+ # Transformer layers
217
+ self.layers = nn.ModuleList([GlycanBERTLayer(config) for _ in range(config.num_hidden_layers)])
218
+
219
+ # MLM head
220
+ self.mlm_head = nn.Linear(config.hidden_size, config.vocab_size)
221
+
222
+ # Initialize weights
223
+ self.apply(self._init_weights)
224
+
225
+ def _init_weights(self, module):
226
+ """Initialize weights."""
227
+ if isinstance(module, nn.Linear):
228
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
229
+ if module.bias is not None:
230
+ module.bias.data.zero_()
231
+ elif isinstance(module, nn.Embedding):
232
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
233
+ if module.padding_idx is not None:
234
+ module.weight.data[module.padding_idx].zero_()
235
+ elif isinstance(module, nn.LayerNorm):
236
+ module.bias.data.zero_()
237
+ module.weight.data.fill_(1.0)
238
+
239
+ def forward(
240
+ self,
241
+ input_ids: torch.Tensor,
242
+ attention_mask: torch.Tensor = None,
243
+ labels: torch.Tensor = None
244
+ ):
245
+ """
246
+ Args:
247
+ input_ids: (batch_size, seq_len)
248
+ attention_mask: (batch_size, seq_len) - 1 for valid, 0 for padding
249
+ labels: (batch_size, seq_len) - token IDs to predict, -100 for positions to ignore
250
+
251
+ Returns:
252
+ If labels provided: (loss, logits)
253
+ Else: logits
254
+ """
255
+ # Create attention mask if not provided
256
+ if attention_mask is None:
257
+ attention_mask = (input_ids != self.config.pad_token_id).float()
258
+
259
+ # Embeddings
260
+ hidden_states = self.embeddings(input_ids)
261
+
262
+ # Transformer layers
263
+ for layer in self.layers:
264
+ hidden_states = layer(hidden_states, attention_mask)
265
+
266
+ # MLM prediction
267
+ logits = self.mlm_head(hidden_states)
268
+
269
+ # Calculate loss if labels provided
270
+ loss = None
271
+ if labels is not None:
272
+ loss_fct = nn.CrossEntropyLoss() # -100 is ignored
273
+ loss = loss_fct(logits.view(-1, self.config.vocab_size), labels.view(-1))
274
+
275
+ if loss is not None:
276
+ return loss, logits
277
+ return logits
278
+
279
+ def get_embeddings(
280
+ self,
281
+ input_ids: torch.Tensor,
282
+ attention_mask: torch.Tensor = None
283
+ ) -> torch.Tensor:
284
+ """
285
+ Get contextualized embeddings (for downstream tasks).
286
+
287
+ Args:
288
+ input_ids: (batch_size, seq_len)
289
+ attention_mask: (batch_size, seq_len)
290
+
291
+ Returns:
292
+ Embeddings: (batch_size, seq_len, hidden_size)
293
+ """
294
+ if attention_mask is None:
295
+ attention_mask = (input_ids != self.config.pad_token_id).float()
296
+
297
+ hidden_states = self.embeddings(input_ids)
298
+
299
+ for layer in self.layers:
300
+ hidden_states = layer(hidden_states, attention_mask)
301
+
302
+ return hidden_states
303
+
releases/2026-10-02/bertose_source/bert_training_v4/model/multimodal_glycan_bert_v3.py ADDED
@@ -0,0 +1,1108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multimodal Glycan BERT Model v3
3
+
4
+ Extends GlycanBERT to handle three modalities:
5
+ - Sequence (WURCS atomic tokenization)
6
+ - MS (mass spectrometry peaks, RT, intensity)
7
+ - 3D structure (VQ-VAE discrete tokens, 4 per residue)
8
+
9
+ Each modality has its own encoder, with cross-attention for sequence-structure alignment.
10
+ """
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ from typing import Dict, Optional, Tuple
15
+ import math
16
+
17
+ try:
18
+ from .glycan_bert import GlycanBERTConfig, GlycanBERTEmbeddings, GlycanBERTLayer
19
+ except ImportError:
20
+ from glycan_bert import GlycanBERTConfig, GlycanBERTEmbeddings, GlycanBERTLayer
21
+
22
+
23
+ class ConvGlycanBERTEmbeddings(nn.Module):
24
+ """
25
+ Improved Convolutional front-end that mixes local WURCS context before the Transformer.
26
+
27
+ Key improvements over original:
28
+ 1. Position embeddings added BEFORE convolution (provides spatial context to conv)
29
+ 2. Residual connection (conv enriches embeddings rather than replacing them)
30
+ 3. Multi-scale convolutions (kernel sizes 3, 5, 7) for better receptive field
31
+ 4. Proper layer normalization on the residual path
32
+ """
33
+
34
+ def __init__(self, config):
35
+ super().__init__()
36
+ self.token_embeddings = nn.Embedding(
37
+ config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id
38
+ )
39
+ self.position_embeddings = nn.Embedding(
40
+ config.max_position_embeddings, config.hidden_size
41
+ )
42
+
43
+ # NEW: Branch depth embeddings - encodes depth in glycan tree (0=root, 1=child, etc.)
44
+ max_branch_depth = getattr(config, "max_branch_depth", 8)
45
+ self.branch_embeddings = nn.Embedding(max_branch_depth, config.hidden_size)
46
+
47
+ # NEW: Linkage type embeddings - encodes chemistry of glycosidic bond
48
+ # 0=none, 1=1-3, 2=1-4, 3=1-6, etc.
49
+ num_linkage_types = getattr(config, "num_linkage_types", 9)
50
+ self.linkage_embeddings = nn.Embedding(num_linkage_types, config.hidden_size)
51
+
52
+ # Multi-scale convolutions for different receptive fields
53
+ kernel_size = getattr(config, "cnn_kernel_size", 3)
54
+ # Split channels evenly: 256 + 256 + 256 = 768 for hidden_size=768
55
+ channels_per_scale = config.hidden_size // 3
56
+ self.conv_layers = nn.ModuleList([
57
+ nn.Conv1d(
58
+ in_channels=config.hidden_size,
59
+ out_channels=channels_per_scale,
60
+ kernel_size=kernel_size + 2 * i, # Kernels: 3, 5, 7
61
+ padding=(kernel_size + 2 * i) // 2, # Same padding
62
+ )
63
+ for i in range(3)
64
+ ])
65
+ self.conv_activation = nn.GELU()
66
+ self.conv_proj = nn.Linear(channels_per_scale * 3, config.hidden_size) # Project concatenated back
67
+
68
+ self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
69
+ self.conv_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
70
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
71
+ self.register_buffer(
72
+ "position_ids",
73
+ torch.arange(config.max_position_embeddings).expand((1, -1)),
74
+ )
75
+
76
+ self.hidden_size = config.hidden_size
77
+
78
+ def forward(self, input_ids, branch_depths=None, linkage_types=None):
79
+ seq_len = input_ids.shape[1]
80
+
81
+ # Step 1: Token + Position embeddings FIRST (provides spatial context to conv)
82
+ x = self.token_embeddings(input_ids) # (batch, seq, hidden)
83
+ position_ids = self.position_ids[:, :seq_len]
84
+ x = x + self.position_embeddings(position_ids)
85
+
86
+ # NEW: Add branch depth embeddings (encodes tree structure)
87
+ if branch_depths is not None:
88
+ # Clamp to valid range
89
+ branch_depths = branch_depths.clamp(0, self.branch_embeddings.num_embeddings - 1)
90
+ x = x + self.branch_embeddings(branch_depths)
91
+
92
+ # NEW: Add linkage type embeddings (encodes bond chemistry)
93
+ if linkage_types is not None:
94
+ linkage_types = linkage_types.clamp(0, self.linkage_embeddings.num_embeddings - 1)
95
+ x = x + self.linkage_embeddings(linkage_types)
96
+
97
+ x = self.LayerNorm(x)
98
+
99
+ # Step 2: Multi-scale convolution with RESIDUAL connection
100
+ # Convolution expects (batch, hidden, seq)
101
+ conv_in = x.permute(0, 2, 1)
102
+
103
+ # Apply multi-scale convolutions and concatenate
104
+ conv_outputs = []
105
+ for conv in self.conv_layers:
106
+ conv_out = self.conv_activation(conv(conv_in))
107
+ conv_outputs.append(conv_out)
108
+
109
+ # Concatenate multi-scale features and project back
110
+ conv_out = torch.cat(conv_outputs, dim=1) # (batch, hidden, seq)
111
+ conv_out = conv_out.permute(0, 2, 1) # (batch, seq, hidden)
112
+ conv_out = self.conv_proj(conv_out) # Project to correct size
113
+
114
+ # Step 3: Residual connection - conv ENRICHES rather than replaces
115
+ x = self.conv_norm(x + self.dropout(conv_out))
116
+
117
+ return x
118
+
119
+
120
+ def create_residue_level_mask(
121
+ seq_residue_ids: torch.Tensor, # (batch, N_seq)
122
+ struct_residue_ids: torch.Tensor # (batch, N_struct)
123
+ ) -> torch.Tensor:
124
+ """
125
+ Create residue-level attention mask for cross-attention.
126
+
127
+ Maps WURCS tokens to VQ-VAE structural tokens based on residue IDs.
128
+ A WURCS token with residue_id=0 can only attend to VQ-VAE tokens with residue_id=0.
129
+
130
+ Args:
131
+ seq_residue_ids: Residue IDs for sequence tokens (batch, N_seq)
132
+ struct_residue_ids: Residue IDs for structural tokens (batch, N_struct)
133
+
134
+ Returns:
135
+ Boolean mask (batch, N_seq, N_struct) where True = can attend
136
+ """
137
+ # Expand dimensions for broadcasting
138
+ # seq: (batch, N_seq, 1)
139
+ # struct: (batch, 1, N_struct)
140
+ mask = seq_residue_ids.unsqueeze(2) == struct_residue_ids.unsqueeze(1)
141
+ # Shape: (batch, N_seq, N_struct)
142
+
143
+ # Mask out structural tokens (residue_id = -1) and MS tokens (residue_id = -2)
144
+ # Only tokens with residue_id >= 0 can attend
145
+ mask &= (seq_residue_ids.unsqueeze(2) >= 0)
146
+
147
+ return mask # True = can attend, False = cannot attend
148
+
149
+
150
+ class MultimodalGlycanBERTConfig:
151
+ """Configuration for Multimodal GlycanBERT v3."""
152
+
153
+ def __init__(
154
+ self,
155
+ # Sequence modality
156
+ seq_vocab_size: int = 166,
157
+ seq_hidden_size: int = 768,
158
+ seq_num_layers: int = 12,
159
+ seq_num_heads: int = 12,
160
+ seq_max_length: int = 512,
161
+
162
+ # MS modality
163
+ ms_vocab_size: int = 242,
164
+ ms_hidden_size: int = 384,
165
+ ms_num_layers: int = 6,
166
+ ms_num_heads: int = 6,
167
+ ms_max_length: int = 150,
168
+
169
+ # 3D structure modality
170
+ struct_vocab_size: int = 1024, # VQ-VAE codebook size
171
+ struct_hidden_size: int = 512,
172
+ struct_num_layers: int = 8,
173
+ struct_num_heads: int = 8,
174
+ struct_max_length: int = 200,
175
+ use_3d: bool = True,
176
+
177
+ # Cross-attention
178
+ use_cross_attention: bool = True,
179
+ cross_attn_num_heads: int = 8,
180
+
181
+ # Fusion
182
+ fusion_hidden_size: int = 768,
183
+ fusion_num_layers: int = 2,
184
+
185
+ # Training
186
+ hidden_dropout_prob: float = 0.1,
187
+ attention_probs_dropout_prob: float = 0.1,
188
+ layer_norm_eps: float = 1e-12,
189
+ initializer_range: float = 0.02,
190
+
191
+ # Conv front-end
192
+ use_cnn_frontend: bool = True,
193
+ cnn_kernel_size: int = 3,
194
+
195
+ # Loss weights
196
+ seq_loss_weight: float = 0.60,
197
+ ms_loss_weight: float = 0.15,
198
+ struct_loss_weight: float = 0.25,
199
+
200
+ # Token IDs
201
+ pad_token_id: int = 0,
202
+ mask_token_id: int = 1,
203
+ ):
204
+ # Sequence config
205
+ self.seq_vocab_size = seq_vocab_size
206
+ self.seq_hidden_size = seq_hidden_size
207
+ self.seq_num_layers = seq_num_layers
208
+ self.seq_num_heads = seq_num_heads
209
+ self.seq_max_length = seq_max_length
210
+
211
+ # MS config
212
+ self.ms_vocab_size = ms_vocab_size
213
+ self.ms_vocab_offset = seq_vocab_size # MS tokens start at 166
214
+ self.ms_total_vocab_size = seq_vocab_size + ms_vocab_size # 408 total
215
+ self.ms_hidden_size = ms_hidden_size
216
+ self.ms_num_layers = ms_num_layers
217
+ self.ms_num_heads = ms_num_heads
218
+ self.ms_max_length = ms_max_length
219
+
220
+ # Structure config
221
+ self.struct_vocab_size = struct_vocab_size
222
+ self.struct_hidden_size = struct_hidden_size
223
+ self.struct_num_layers = struct_num_layers
224
+ self.struct_num_heads = struct_num_heads
225
+ self.struct_max_length = struct_max_length
226
+ self.use_3d = use_3d
227
+
228
+ # Cross-attention config
229
+ self.use_cross_attention = use_cross_attention
230
+ self.cross_attn_num_heads = cross_attn_num_heads
231
+
232
+ # Fusion config
233
+ self.fusion_hidden_size = fusion_hidden_size
234
+ self.fusion_num_layers = fusion_num_layers
235
+
236
+ # Training config
237
+ self.hidden_dropout_prob = hidden_dropout_prob
238
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
239
+ self.layer_norm_eps = layer_norm_eps
240
+ self.initializer_range = initializer_range
241
+
242
+ # Conv front-end
243
+ self.use_cnn_frontend = use_cnn_frontend
244
+ self.cnn_kernel_size = cnn_kernel_size
245
+
246
+ # Loss weights
247
+ self.seq_loss_weight = seq_loss_weight
248
+ self.ms_loss_weight = ms_loss_weight
249
+ self.struct_loss_weight = struct_loss_weight
250
+
251
+ # Token IDs
252
+ self.pad_token_id = pad_token_id
253
+ self.mask_token_id = mask_token_id
254
+
255
+ def to_seq_config(self) -> GlycanBERTConfig:
256
+ """Convert to sequence-only config."""
257
+ return GlycanBERTConfig(
258
+ vocab_size=self.seq_vocab_size,
259
+ hidden_size=self.seq_hidden_size,
260
+ num_hidden_layers=self.seq_num_layers,
261
+ num_attention_heads=self.seq_num_heads,
262
+ intermediate_size=self.seq_hidden_size * 4,
263
+ hidden_dropout_prob=self.hidden_dropout_prob,
264
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
265
+ max_position_embeddings=self.seq_max_length,
266
+ layer_norm_eps=self.layer_norm_eps,
267
+ pad_token_id=self.pad_token_id,
268
+ mask_token_id=self.mask_token_id,
269
+ initializer_range=self.initializer_range,
270
+ )
271
+
272
+ def to_ms_config(self) -> GlycanBERTConfig:
273
+ """Convert to MS-only config."""
274
+ return GlycanBERTConfig(
275
+ vocab_size=self.ms_total_vocab_size,
276
+ hidden_size=self.ms_hidden_size,
277
+ num_hidden_layers=self.ms_num_layers,
278
+ num_attention_heads=self.ms_num_heads,
279
+ intermediate_size=self.ms_hidden_size * 4,
280
+ hidden_dropout_prob=self.hidden_dropout_prob,
281
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
282
+ max_position_embeddings=self.ms_max_length,
283
+ layer_norm_eps=self.layer_norm_eps,
284
+ pad_token_id=self.pad_token_id,
285
+ mask_token_id=self.mask_token_id,
286
+ initializer_range=self.initializer_range,
287
+ )
288
+
289
+ def to_struct_config(self) -> GlycanBERTConfig:
290
+ """Convert to structure-only config."""
291
+ return GlycanBERTConfig(
292
+ vocab_size=self.struct_vocab_size,
293
+ hidden_size=self.struct_hidden_size,
294
+ num_hidden_layers=self.struct_num_layers,
295
+ num_attention_heads=self.struct_num_heads,
296
+ intermediate_size=self.struct_hidden_size * 4,
297
+ hidden_dropout_prob=self.hidden_dropout_prob,
298
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
299
+ max_position_embeddings=self.struct_max_length,
300
+ layer_norm_eps=self.layer_norm_eps,
301
+ pad_token_id=self.pad_token_id,
302
+ mask_token_id=self.mask_token_id,
303
+ initializer_range=self.initializer_range,
304
+ )
305
+
306
+
307
+ # =============================================================================
308
+ # Improvement #1: Monosaccharide-Level Pooling
309
+ # =============================================================================
310
+
311
+ class MonosaccharidePooling(nn.Module):
312
+ """
313
+ Pool token representations to monosaccharide level, then aggregate.
314
+
315
+ This bridges the gap between token-level BERT and monosaccharide-level CNNs/GNNs.
316
+ Uses monosaccharide_indices from the data to know where each residue starts.
317
+ """
318
+
319
+ def __init__(self, hidden_size: int, num_attention_heads: int = 8, dropout: float = 0.1):
320
+ super().__init__()
321
+ self.hidden_size = hidden_size
322
+
323
+ # Attention pooling over monosaccharide representations
324
+ self.mono_attention = nn.MultiheadAttention(
325
+ embed_dim=hidden_size,
326
+ num_heads=num_attention_heads,
327
+ dropout=dropout,
328
+ batch_first=True
329
+ )
330
+ self.mono_norm = nn.LayerNorm(hidden_size)
331
+
332
+ # Final aggregation to single glycan representation
333
+ self.glycan_query = nn.Parameter(torch.randn(1, 1, hidden_size) * 0.02)
334
+ self.glycan_attention = nn.MultiheadAttention(
335
+ embed_dim=hidden_size,
336
+ num_heads=num_attention_heads,
337
+ dropout=dropout,
338
+ batch_first=True
339
+ )
340
+ self.glycan_norm = nn.LayerNorm(hidden_size)
341
+
342
+ def forward(
343
+ self,
344
+ hidden_states: torch.Tensor, # (batch, seq_len, hidden)
345
+ residue_ids: torch.Tensor, # (batch, seq_len) - which residue each token belongs to
346
+ attention_mask: torch.Tensor = None, # (batch, seq_len)
347
+ ) -> torch.Tensor:
348
+ """
349
+ Pool tokens to monosaccharide level, then to glycan level.
350
+
351
+ Returns:
352
+ Glycan representation: (batch, hidden_size)
353
+ """
354
+ batch_size = hidden_states.size(0)
355
+ device = hidden_states.device
356
+
357
+ # Get unique residue IDs per sample (excluding -1 padding)
358
+ max_residues = 50 # Reasonable max for glycans
359
+
360
+ # Pool tokens within each residue using mean pooling
361
+ mono_reps = torch.zeros(batch_size, max_residues, self.hidden_size, device=device)
362
+ mono_mask = torch.zeros(batch_size, max_residues, dtype=torch.bool, device=device)
363
+
364
+ for b in range(batch_size):
365
+ unique_residues = torch.unique(residue_ids[b][residue_ids[b] >= 0])
366
+ for i, rid in enumerate(unique_residues):
367
+ if i >= max_residues:
368
+ break
369
+ token_mask = residue_ids[b] == rid
370
+ if attention_mask is not None:
371
+ token_mask = token_mask & (attention_mask[b] > 0)
372
+ if token_mask.sum() > 0:
373
+ mono_reps[b, i] = hidden_states[b][token_mask].mean(dim=0)
374
+ mono_mask[b, i] = True
375
+
376
+ # Apply attention over monosaccharide representations
377
+ # Convert mask for attention: True = valid, need to invert for PyTorch
378
+ key_padding_mask = ~mono_mask # True = ignore
379
+
380
+ mono_out, _ = self.mono_attention(
381
+ mono_reps, mono_reps, mono_reps,
382
+ key_padding_mask=key_padding_mask
383
+ )
384
+ mono_out = self.mono_norm(mono_reps + mono_out)
385
+
386
+ # Aggregate to single glycan representation using learned query
387
+ glycan_query = self.glycan_query.expand(batch_size, -1, -1)
388
+ glycan_out, _ = self.glycan_attention(
389
+ glycan_query, mono_out, mono_out,
390
+ key_padding_mask=key_padding_mask
391
+ )
392
+ glycan_out = self.glycan_norm(glycan_query + glycan_out)
393
+
394
+ return glycan_out.squeeze(1) # (batch, hidden)
395
+
396
+
397
+ # =============================================================================
398
+ # Improvement #2: Residue Type Embeddings
399
+ # =============================================================================
400
+
401
+ # Common monosaccharide types vocabulary
402
+ MONOSACCHARIDE_VOCAB = {
403
+ '[PAD_MONO]': 0, '[UNK_MONO]': 1,
404
+ 'Glc': 2, 'GlcNAc': 3, 'GlcA': 4, 'GlcN': 5,
405
+ 'Gal': 6, 'GalNAc': 7, 'GalA': 8, 'GalN': 9,
406
+ 'Man': 10, 'ManNAc': 11, 'ManA': 12, 'ManN': 13,
407
+ 'Fuc': 14, 'Rha': 15, 'Xyl': 16, 'Ara': 17,
408
+ 'Neu5Ac': 18, 'Neu5Gc': 19, 'Kdn': 20, 'Sia': 21,
409
+ 'GalNAcA': 22, 'GlcNAcA': 23, 'IdoA': 24, 'GulA': 25,
410
+ 'Rib': 26, 'Lyx': 27, 'All': 28, 'Alt': 29,
411
+ 'Tal': 30, 'Ido': 31, 'Qui': 32, 'Oli': 33,
412
+ 'Tyv': 34, 'Abe': 35, 'Par': 36, 'Dig': 37,
413
+ 'Col': 38, 'Dha': 39, 'Kdo': 40, 'Hep': 41,
414
+ 'NeuroGc': 42, 'Muramic': 43, 'LDManHep': 44, 'DDManHep': 45,
415
+ 'Bac': 46, 'Pse': 47, 'Leg': 48, 'Aci': 49,
416
+ '6dTal': 50, 'Fru': 51, 'Tag': 52, 'Sor': 53,
417
+ 'Psi': 54, 'Sed': 55, 'MurNAc': 56, 'MurNGc': 57,
418
+ 'Api': 58, 'Erwiniose': 59, 'Yer': 60, 'Thre': 61,
419
+ # Add more as needed, up to ~70
420
+ }
421
+
422
+
423
+ class ResidueTypeEmbeddings(nn.Module):
424
+ """
425
+ Learnable embeddings for monosaccharide types.
426
+
427
+ Instead of the model having to learn that 'a1221m' = Fucose from character patterns,
428
+ we explicitly add a Fucose embedding to all tokens belonging to that residue.
429
+ """
430
+
431
+ def __init__(self, hidden_size: int, num_mono_types: int = 70):
432
+ super().__init__()
433
+ self.mono_embeddings = nn.Embedding(num_mono_types, hidden_size)
434
+ self.mono_vocab = MONOSACCHARIDE_VOCAB
435
+ self.hidden_size = hidden_size
436
+
437
+ def forward(
438
+ self,
439
+ token_embeddings: torch.Tensor, # (batch, seq_len, hidden)
440
+ residue_ids: torch.Tensor, # (batch, seq_len)
441
+ mono_type_ids: torch.Tensor = None, # (batch, max_residues) - monosaccharide type per residue
442
+ ) -> torch.Tensor:
443
+ """
444
+ Add residue type embeddings to token embeddings.
445
+
446
+ Args:
447
+ token_embeddings: Base token embeddings
448
+ residue_ids: Which residue each token belongs to (-1 for special tokens)
449
+ mono_type_ids: Monosaccharide type ID for each residue position
450
+
451
+ Returns:
452
+ Enhanced embeddings with residue type information
453
+ """
454
+ if mono_type_ids is None:
455
+ return token_embeddings
456
+
457
+ batch_size, seq_len, _ = token_embeddings.shape
458
+ enhanced = token_embeddings.clone()
459
+
460
+ # Add mono type embedding to each token based on its residue
461
+ for b in range(batch_size):
462
+ for pos in range(seq_len):
463
+ rid = residue_ids[b, pos].item()
464
+ if rid >= 0 and rid < mono_type_ids.size(1):
465
+ mono_id = mono_type_ids[b, rid]
466
+ enhanced[b, pos] = enhanced[b, pos] + self.mono_embeddings(mono_id)
467
+
468
+ return enhanced
469
+
470
+ @staticmethod
471
+ def get_mono_type_id(mono_name: str) -> int:
472
+ """Convert monosaccharide name to type ID."""
473
+ return MONOSACCHARIDE_VOCAB.get(mono_name, MONOSACCHARIDE_VOCAB['[UNK_MONO]'])
474
+
475
+
476
+ # =============================================================================
477
+ # Improvement #4: Relative Position Encoding for Glycan Trees
478
+ # =============================================================================
479
+
480
+ class RelativePositionBias(nn.Module):
481
+ """
482
+ Compute relative position bias for attention based on residue IDs.
483
+
484
+ Tokens in the same residue get distance 0.
485
+ Tokens in adjacent residues get distance ±1.
486
+ This helps the model understand glycan tree structure.
487
+ """
488
+
489
+ def __init__(self, num_heads: int, max_distance: int = 10):
490
+ super().__init__()
491
+ self.num_heads = num_heads
492
+ self.max_distance = max_distance
493
+
494
+ # Learnable bias for each relative distance (-max to +max)
495
+ num_distances = 2 * max_distance + 1
496
+ self.relative_bias = nn.Embedding(num_distances, num_heads)
497
+
498
+ def forward(self, residue_ids: torch.Tensor) -> torch.Tensor:
499
+ """
500
+ Compute relative position bias.
501
+
502
+ Args:
503
+ residue_ids: (batch, seq_len)
504
+
505
+ Returns:
506
+ Bias to add to attention scores: (batch, num_heads, seq_len, seq_len)
507
+ """
508
+ # Compute pairwise residue distances
509
+ # (batch, seq_len, 1) - (batch, 1, seq_len) = (batch, seq_len, seq_len)
510
+ distance = residue_ids.unsqueeze(2) - residue_ids.unsqueeze(1)
511
+
512
+ # Clamp to max distance range and shift to 0-indexed
513
+ distance_clamped = distance.clamp(-self.max_distance, self.max_distance)
514
+ distance_idx = distance_clamped + self.max_distance # Now 0 to 2*max_distance
515
+
516
+ # Look up bias: (batch, seq_len, seq_len, num_heads)
517
+ bias = self.relative_bias(distance_idx)
518
+
519
+ # Transpose to (batch, num_heads, seq_len, seq_len)
520
+ bias = bias.permute(0, 3, 1, 2)
521
+
522
+ return bias
523
+
524
+
525
+ class CrossAttentionLayer(nn.Module):
526
+ """
527
+ Cross-attention layer for sequence-structure alignment.
528
+
529
+ Allows sequence tokens to attend to structural atoms using attention masks.
530
+ """
531
+
532
+ def __init__(self, config: MultimodalGlycanBERTConfig):
533
+ super().__init__()
534
+ self.num_heads = config.cross_attn_num_heads
535
+ self.hidden_size = config.seq_hidden_size
536
+ self.head_dim = self.hidden_size // self.num_heads
537
+
538
+ assert self.hidden_size % self.num_heads == 0, "hidden_size must be divisible by num_heads"
539
+
540
+ # Query from sequence, Key/Value from structure (VQ-VAE tokens)
541
+ self.query = nn.Linear(config.seq_hidden_size, self.hidden_size)
542
+ self.key = nn.Linear(config.struct_hidden_size, self.hidden_size)
543
+ self.value = nn.Linear(config.struct_hidden_size, self.hidden_size)
544
+
545
+ self.output = nn.Linear(self.hidden_size, config.seq_hidden_size)
546
+ self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
547
+ self.layer_norm = nn.LayerNorm(config.seq_hidden_size, eps=config.layer_norm_eps)
548
+
549
+ def forward(
550
+ self,
551
+ seq_hidden: torch.Tensor, # (batch, seq_len, seq_hidden)
552
+ struct_hidden: torch.Tensor, # (batch, struct_len, struct_hidden)
553
+ attention_mask: Optional[torch.Tensor] = None, # (batch, seq_len, struct_len)
554
+ ) -> torch.Tensor:
555
+ """
556
+ Apply cross-attention from sequence to structure.
557
+
558
+ Args:
559
+ seq_hidden: Sequence hidden states
560
+ struct_hidden: Structure hidden states
561
+ attention_mask: Boolean mask (True = can attend, False = cannot attend)
562
+
563
+ Returns:
564
+ Updated sequence hidden states
565
+ """
566
+ batch_size, seq_len, _ = seq_hidden.shape
567
+ struct_len = struct_hidden.shape[1]
568
+
569
+ # Project to Q, K, V
570
+ Q = self.query(seq_hidden) # (batch, seq_len, hidden)
571
+ K = self.key(struct_hidden) # (batch, struct_len, hidden)
572
+ V = self.value(struct_hidden) # (batch, struct_len, hidden)
573
+
574
+ # Reshape for multi-head attention
575
+ Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, seq_len, head_dim)
576
+ K = K.view(batch_size, struct_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, struct_len, head_dim)
577
+ V = V.view(batch_size, struct_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, struct_len, head_dim)
578
+
579
+ # Compute attention scores
580
+ scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim) # (batch, heads, seq_len, struct_len)
581
+
582
+ # Apply attention mask
583
+ if attention_mask is not None:
584
+ # attention_mask: (batch, seq_len, struct_len) -> (batch, 1, seq_len, struct_len)
585
+ attention_mask = attention_mask.unsqueeze(1)
586
+ # Convert boolean mask to float: True -> 0.0, False -> -10000.0
587
+ attention_mask = (~attention_mask).float() * -10000.0
588
+ scores = scores + attention_mask
589
+
590
+ # Softmax and dropout
591
+ attn_weights = torch.softmax(scores, dim=-1) # (batch, heads, seq_len, struct_len)
592
+ attn_weights = self.dropout(attn_weights)
593
+
594
+ # Apply attention to values
595
+ context = torch.matmul(attn_weights, V) # (batch, heads, seq_len, head_dim)
596
+
597
+ # Reshape back
598
+ context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.hidden_size)
599
+
600
+ # Output projection
601
+ output = self.output(context)
602
+ output = self.dropout(output)
603
+
604
+ # Residual connection + layer norm
605
+ output = self.layer_norm(seq_hidden + output)
606
+
607
+ return output
608
+
609
+
610
+ class DistanceHead(nn.Module):
611
+ """
612
+ Head for predicting structural distance (shortest path) between token pairs.
613
+
614
+ Input: Pair of token embeddings (x_i, x_j)
615
+ Output: Scalar distance prediction
616
+
617
+ Formula: Dist(i, j) = Linear( [x_i; x_j; x_i * x_j; |x_i - x_j|] )
618
+ Using a comprehensive bilinear-like interaction for robust distance learning.
619
+ """
620
+ def __init__(self, config):
621
+ super().__init__()
622
+ self.hidden_size = config.seq_hidden_size
623
+ # Interaction features: concat(u, v, u*v, |u-v|) -> 4 * hidden
624
+ self.dense = nn.Linear(self.hidden_size * 4, self.hidden_size)
625
+ self.activation = nn.GELU()
626
+ self.layer_norm = nn.LayerNorm(self.hidden_size, eps=config.layer_norm_eps)
627
+ self.decoder = nn.Linear(self.hidden_size, 1) # Output scalar distance
628
+
629
+ def forward(self, hidden_states):
630
+ """
631
+ Args:
632
+ hidden_states: (batch, seq_len, hidden)
633
+
634
+ Returns:
635
+ distances: (batch, seq_len, seq_len)
636
+ """
637
+ batch_size, seq_len, hidden = hidden_states.shape
638
+
639
+ # Expand to pairs (batch, seq, seq, hidden)
640
+ u = hidden_states.unsqueeze(2).expand(batch_size, seq_len, seq_len, hidden)
641
+ v = hidden_states.unsqueeze(1).expand(batch_size, seq_len, seq_len, hidden)
642
+
643
+ # Features
644
+ features = torch.cat([
645
+ u,
646
+ v,
647
+ u * v,
648
+ torch.abs(u - v)
649
+ ], dim=-1) # (batch, seq, seq, 4*hidden)
650
+
651
+ x = self.dense(features)
652
+ x = self.activation(x)
653
+ x = self.layer_norm(x)
654
+ x = self.decoder(x) # (batch, seq, seq, 1)
655
+
656
+ return x.squeeze(-1) # (batch, seq, seq)
657
+
658
+ class MultimodalGlycanBERT(nn.Module):
659
+ """
660
+ Multimodal BERT for glycan representation learning (v3).
661
+
662
+ Architecture:
663
+ 1. Separate encoders for each modality (sequence, MS, 3D structure)
664
+ 2. Cross-attention for sequence-structure alignment
665
+ 3. Modality-specific MLM heads
666
+ 4. Fusion layer for combined representation
667
+ """
668
+
669
+ def __init__(self, config: MultimodalGlycanBERTConfig):
670
+ super().__init__()
671
+ self.config = config
672
+
673
+ # ===== Sequence Encoder =====
674
+ seq_config = config.to_seq_config()
675
+ seq_config.cnn_kernel_size = config.cnn_kernel_size
676
+
677
+ if config.use_cnn_frontend:
678
+ print(f"✅ Enabled Convolutional Front-End (Kernel={config.cnn_kernel_size})")
679
+ self.seq_embeddings = ConvGlycanBERTEmbeddings(seq_config)
680
+ else:
681
+ self.seq_embeddings = GlycanBERTEmbeddings(seq_config)
682
+ self.seq_layers = nn.ModuleList([GlycanBERTLayer(seq_config) for _ in range(seq_config.num_hidden_layers)])
683
+ self.seq_mlm_head = nn.Linear(seq_config.hidden_size, seq_config.vocab_size)
684
+ self.distance_head = DistanceHead(config) # New Distance Prediction Head
685
+
686
+
687
+ # ===== MS Encoder =====
688
+ ms_config = config.to_ms_config()
689
+ self.ms_embeddings = GlycanBERTEmbeddings(ms_config)
690
+ self.ms_layers = nn.ModuleList([GlycanBERTLayer(ms_config) for _ in range(ms_config.num_hidden_layers)])
691
+ self.ms_mlm_head = nn.Linear(ms_config.hidden_size, ms_config.vocab_size)
692
+
693
+ # ===== Structure Encoder (VQ-VAE tokens) =====
694
+ if config.use_3d:
695
+ struct_config = config.to_struct_config()
696
+ self.struct_embeddings = GlycanBERTEmbeddings(struct_config)
697
+ self.struct_layers = nn.ModuleList([GlycanBERTLayer(struct_config) for _ in range(struct_config.num_hidden_layers)])
698
+ self.struct_mlm_head = nn.Linear(struct_config.hidden_size, struct_config.vocab_size)
699
+
700
+ # Cross-attention layer (sequence → VQ-VAE structural tokens)
701
+ if config.use_cross_attention:
702
+ self.cross_attention = CrossAttentionLayer(config)
703
+
704
+ # ===== Projection layers (align hidden sizes) =====
705
+ if config.ms_hidden_size != config.seq_hidden_size:
706
+ self.ms_projection = nn.Linear(config.ms_hidden_size, config.seq_hidden_size)
707
+ else:
708
+ self.ms_projection = nn.Identity()
709
+
710
+ if config.use_3d and config.struct_hidden_size != config.seq_hidden_size:
711
+ self.struct_projection = nn.Linear(config.struct_hidden_size, config.seq_hidden_size)
712
+ else:
713
+ self.struct_projection = nn.Identity()
714
+
715
+ # ===== Fusion Layer =====
716
+ # Concatenate seq + ms + struct
717
+ fusion_input_size = config.seq_hidden_size * (3 if config.use_3d else 2)
718
+ self.fusion_layer = nn.Sequential(
719
+ nn.Linear(fusion_input_size, config.fusion_hidden_size),
720
+ nn.LayerNorm(config.fusion_hidden_size, eps=config.layer_norm_eps),
721
+ nn.GELU(),
722
+ nn.Dropout(config.hidden_dropout_prob),
723
+ nn.Linear(config.fusion_hidden_size, config.fusion_hidden_size),
724
+ )
725
+
726
+ # Initialize weights
727
+ self.apply(self._init_weights)
728
+
729
+ def _init_weights(self, module):
730
+ """Initialize weights."""
731
+ if isinstance(module, nn.Linear):
732
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
733
+ if module.bias is not None:
734
+ module.bias.data.zero_()
735
+ elif isinstance(module, nn.Embedding):
736
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
737
+ if module.padding_idx is not None:
738
+ module.weight.data[module.padding_idx].zero_()
739
+ elif isinstance(module, nn.LayerNorm):
740
+ module.bias.data.zero_()
741
+ module.weight.data.fill_(1.0)
742
+
743
+ def forward(
744
+ self,
745
+ seq_token_ids: torch.Tensor,
746
+ seq_attention_mask: torch.Tensor,
747
+ seq_residue_ids: torch.Tensor,
748
+ seq_branch_depths: Optional[torch.Tensor] = None, # NEW: Branch depths
749
+ seq_linkage_types: Optional[torch.Tensor] = None, # NEW: Linkage types
750
+ ms_token_ids: torch.Tensor = None,
751
+ ms_attention_mask: torch.Tensor = None,
752
+ has_ms: torch.Tensor = None,
753
+ struct_token_ids: Optional[torch.Tensor] = None,
754
+ struct_attention_mask: Optional[torch.Tensor] = None,
755
+ struct_residue_ids: Optional[torch.Tensor] = None,
756
+ has_3d: Optional[torch.Tensor] = None,
757
+ seq_labels: Optional[torch.Tensor] = None,
758
+ ms_labels: Optional[torch.Tensor] = None,
759
+ struct_labels: Optional[torch.Tensor] = None,
760
+ dist_labels: Optional[torch.Tensor] = None, # NEW: Topology distance labels
761
+ return_dict: bool = True,
762
+ compute_distance: bool = True, # Skip distance_head if False (saves memory)
763
+ ) -> Dict[str, torch.Tensor]:
764
+ """
765
+ Forward pass for multimodal BERT v3.
766
+
767
+ Args:
768
+ seq_token_ids: (batch_size, seq_len) - Sequence token IDs
769
+ seq_attention_mask: (batch_size, seq_len) - Sequence attention mask
770
+ seq_residue_ids: (batch_size, seq_len) - Sequence token residue IDs
771
+ ms_token_ids: (batch_size, ms_len) - MS token IDs
772
+ ms_attention_mask: (batch_size, ms_len) - MS attention mask
773
+ has_ms: (batch_size,) - Boolean mask for samples with MS data
774
+ struct_token_ids: (batch_size, struct_len) - Structure VQ-VAE token IDs (optional)
775
+ struct_attention_mask: (batch_size, struct_len) - Structure attention mask (optional)
776
+ struct_residue_ids: (batch_size, struct_len) - Structure token residue IDs (optional)
777
+ has_3d: (batch_size,) - Boolean mask for samples with 3D data (optional)
778
+ seq_labels: (batch_size, seq_len) - Masked sequence labels (optional)
779
+ ms_labels: (batch_size, ms_len) - Masked MS labels (optional)
780
+ struct_labels: (batch_size, struct_len) - Masked structure labels (optional)
781
+ return_dict: Whether to return dict or tuple
782
+
783
+ Returns:
784
+ Dictionary containing logits, hidden states, losses, etc.
785
+ """
786
+ batch_size = seq_token_ids.shape[0]
787
+ device = seq_token_ids.device
788
+
789
+ # ===== Sequence Encoder =====
790
+ # Pass branch_depths and linkage_types to embeddings for tree-aware encoding
791
+ seq_hidden = self.seq_embeddings(seq_token_ids, seq_branch_depths, seq_linkage_types)
792
+ for layer in self.seq_layers:
793
+ seq_hidden = layer(seq_hidden, seq_attention_mask)
794
+
795
+ seq_pooled = seq_hidden[:, 0, :] # [CLS] token
796
+ seq_logits = self.seq_mlm_head(seq_hidden)
797
+
798
+ # Calculate structural distances
799
+ # Calculate structural distances only if needed (saves ~48GB memory)
800
+ dist_predictions = self.distance_head(seq_hidden) if compute_distance else None
801
+
802
+
803
+ # ===== MS Encoder =====
804
+ ms_hidden = None
805
+ ms_pooled = None
806
+ ms_logits = None
807
+
808
+ if ms_token_ids is not None:
809
+ ms_hidden = self.ms_embeddings(ms_token_ids)
810
+ for layer in self.ms_layers:
811
+ ms_hidden = layer(ms_hidden, ms_attention_mask)
812
+
813
+ ms_pooled = ms_hidden[:, 0, :] # [CLS] token
814
+ ms_logits = self.ms_mlm_head(ms_hidden)
815
+
816
+ # Zero out MS representations for samples without MS data
817
+ if has_ms is not None:
818
+ has_ms_expanded = has_ms.unsqueeze(1).float() # (batch, 1)
819
+ ms_pooled = ms_pooled * has_ms_expanded
820
+
821
+ # ===== Structure Encoder =====
822
+ struct_pooled = None
823
+ struct_logits = None
824
+ struct_hidden = None
825
+
826
+ if self.config.use_3d and struct_token_ids is not None:
827
+ struct_hidden = self.struct_embeddings(struct_token_ids)
828
+ for layer in self.struct_layers:
829
+ struct_hidden = layer(struct_hidden, struct_attention_mask)
830
+
831
+ struct_pooled = struct_hidden[:, 0, :] # [CLS] token
832
+ struct_logits = self.struct_mlm_head(struct_hidden)
833
+
834
+ # Zero out structure representations for samples without 3D data
835
+ if has_3d is not None:
836
+ has_3d_expanded = has_3d.unsqueeze(1).float() # (batch, 1)
837
+ struct_pooled = struct_pooled * has_3d_expanded
838
+
839
+ # ===== Cross-Attention (Sequence → VQ-VAE Structural Tokens) =====
840
+ # Use residue-level alignment between WURCS tokens and VQ-VAE tokens
841
+ if self.config.use_cross_attention and struct_residue_ids is not None:
842
+ # Create residue-level mask
843
+ # WURCS token with residue_id=0 → VQ-VAE tokens with residue_id=0
844
+ residue_mask = create_residue_level_mask(
845
+ seq_residue_ids=seq_residue_ids,
846
+ struct_residue_ids=struct_residue_ids,
847
+ ) # (batch, N_seq, N_struct)
848
+
849
+ # Apply cross-attention: sequence tokens attend to VQ-VAE tokens
850
+ seq_hidden = self.cross_attention(
851
+ seq_hidden=seq_hidden,
852
+ struct_hidden=struct_hidden, # VQ-VAE token features
853
+ attention_mask=residue_mask, # Residue-based mask
854
+ )
855
+
856
+ # Update seq_pooled after cross-attention
857
+ seq_pooled = seq_hidden[:, 0, :]
858
+
859
+ # ===== Fusion =====
860
+ # Project to common hidden size
861
+ ms_pooled_projected = self.ms_projection(ms_pooled)
862
+
863
+ if self.config.use_3d and struct_pooled is not None:
864
+ struct_pooled_projected = self.struct_projection(struct_pooled)
865
+ combined = torch.cat([seq_pooled, ms_pooled_projected, struct_pooled_projected], dim=-1)
866
+ else:
867
+ combined = torch.cat([seq_pooled, ms_pooled_projected], dim=-1)
868
+
869
+ fused_repr = self.fusion_layer(combined)
870
+
871
+ # ===== Compute Losses =====
872
+ total_loss = None
873
+ seq_loss = None
874
+ ms_loss = None
875
+ struct_loss = None
876
+ dist_loss = None # NEW: Topology loss
877
+
878
+ if seq_labels is not None:
879
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
880
+ seq_loss = loss_fct(
881
+ seq_logits.view(-1, self.config.seq_vocab_size),
882
+ seq_labels.view(-1)
883
+ )
884
+
885
+ if ms_labels is not None:
886
+ ms_labels_masked = ms_labels.clone()
887
+ ms_labels_masked[~has_ms] = -100
888
+ # Only compute loss if there are valid labels (not all -100)
889
+ if (ms_labels_masked != -100).any():
890
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
891
+ ms_loss = loss_fct(
892
+ ms_logits.view(-1, self.config.ms_total_vocab_size),
893
+ ms_labels_masked.view(-1)
894
+ )
895
+ else:
896
+ ms_loss = torch.tensor(0.0, device=seq_token_ids.device)
897
+
898
+ if self.config.use_3d and struct_labels is not None and struct_logits is not None:
899
+ struct_labels_masked = struct_labels.clone()
900
+ if has_3d is not None:
901
+ struct_labels_masked[~has_3d] = -100
902
+ # Only compute loss if there are valid labels (not all -100)
903
+ if (struct_labels_masked != -100).any():
904
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
905
+ struct_loss = loss_fct(
906
+ struct_logits.view(-1, self.config.struct_vocab_size),
907
+ struct_labels_masked.view(-1)
908
+ )
909
+ else:
910
+ struct_loss = torch.tensor(0.0, device=seq_token_ids.device)
911
+
912
+ # ===== Distance Loss (Topology) =====
913
+ if dist_labels is not None:
914
+ # dist_predictions: (Batch, Seq, Seq, 1) -> (Batch, Seq, Seq)
915
+ preds = dist_predictions.squeeze(-1)
916
+
917
+ # Create mask for valid distance pairs (label != -1)
918
+ # Also respect attention mask to avoid padding
919
+ valid_mask = (dist_labels != -1) & (seq_attention_mask.unsqueeze(1) * seq_attention_mask.unsqueeze(2) == 1)
920
+
921
+ if valid_mask.sum() > 0:
922
+ # MSE loss on valid positions only
923
+ loss_fct = nn.MSELoss()
924
+ dist_loss = loss_fct(preds[valid_mask], dist_labels[valid_mask].float())
925
+ else:
926
+ dist_loss = torch.tensor(0.0, device=seq_token_ids.device)
927
+
928
+ # Weighted combination
929
+ losses = []
930
+ if seq_loss is not None:
931
+ losses.append(self.config.seq_loss_weight * seq_loss)
932
+ if ms_loss is not None:
933
+ losses.append(self.config.ms_loss_weight * ms_loss)
934
+ if struct_loss is not None:
935
+ losses.append(self.config.struct_loss_weight * struct_loss)
936
+ if dist_loss is not None:
937
+ losses.append(self.config.dist_loss_weight * dist_loss)
938
+
939
+ if losses:
940
+ total_loss = sum(losses)
941
+
942
+ if return_dict:
943
+ return {
944
+ 'loss': total_loss,
945
+ 'seq_loss': seq_loss,
946
+ 'ms_loss': ms_loss,
947
+ 'struct_loss': struct_loss,
948
+ 'dist_loss': dist_loss,
949
+ 'seq_logits': seq_logits,
950
+ 'ms_logits': ms_logits,
951
+ 'struct_logits': struct_logits,
952
+ 'dist_predictions': dist_predictions, # Return distance predictions
953
+ 'seq_hidden': seq_hidden,
954
+ 'ms_hidden': ms_hidden,
955
+ 'struct_hidden': struct_hidden,
956
+ 'seq_pooled': seq_pooled,
957
+ 'ms_pooled': ms_pooled,
958
+ 'struct_pooled': struct_pooled,
959
+ 'fused_repr': fused_repr,
960
+ }
961
+ else:
962
+ return (total_loss, seq_logits, ms_logits, struct_logits, fused_repr)
963
+
964
+ def get_multimodal_representation(
965
+ self,
966
+ seq_token_ids: torch.Tensor,
967
+ seq_attention_mask: torch.Tensor,
968
+ seq_residue_ids: torch.Tensor,
969
+ ms_token_ids: torch.Tensor,
970
+ ms_attention_mask: torch.Tensor,
971
+ has_ms: torch.Tensor,
972
+ struct_token_ids: Optional[torch.Tensor] = None,
973
+ struct_attention_mask: Optional[torch.Tensor] = None,
974
+ struct_residue_ids: Optional[torch.Tensor] = None,
975
+ has_3d: Optional[torch.Tensor] = None,
976
+ ) -> torch.Tensor:
977
+ """Get fused multimodal representation (for inference)."""
978
+ outputs = self.forward(
979
+ seq_token_ids=seq_token_ids,
980
+ seq_attention_mask=seq_attention_mask,
981
+ seq_residue_ids=seq_residue_ids,
982
+ ms_token_ids=ms_token_ids,
983
+ ms_attention_mask=ms_attention_mask,
984
+ has_ms=has_ms,
985
+ struct_token_ids=struct_token_ids,
986
+ struct_attention_mask=struct_attention_mask,
987
+ struct_residue_ids=struct_residue_ids,
988
+ has_3d=has_3d,
989
+ return_dict=True,
990
+ )
991
+ return outputs['fused_repr']
992
+
993
+
994
+ if __name__ == "__main__":
995
+ # Test the model
996
+ print("="*80)
997
+ print("Testing Multimodal GlycanBERT v3")
998
+ print("="*80)
999
+
1000
+ # Create config
1001
+ config = MultimodalGlycanBERTConfig(
1002
+ seq_vocab_size=166,
1003
+ seq_hidden_size=768,
1004
+ seq_num_layers=12,
1005
+ seq_num_heads=12,
1006
+ ms_vocab_size=242,
1007
+ ms_hidden_size=384,
1008
+ ms_num_layers=6,
1009
+ ms_num_heads=6,
1010
+ struct_vocab_size=1024,
1011
+ struct_hidden_size=512,
1012
+ struct_num_layers=8,
1013
+ struct_num_heads=8,
1014
+ use_3d=True,
1015
+ use_cross_attention=True,
1016
+ seq_loss_weight=0.60,
1017
+ ms_loss_weight=0.15,
1018
+ struct_loss_weight=0.25,
1019
+ )
1020
+
1021
+ print(f"\nConfig:")
1022
+ print(f" Sequence vocab: {config.seq_vocab_size}")
1023
+ print(f" MS vocab: {config.ms_vocab_size}")
1024
+ print(f" Structure vocab: {config.struct_vocab_size}")
1025
+ print(f" Loss weights: seq={config.seq_loss_weight}, ms={config.ms_loss_weight}, struct={config.struct_loss_weight}")
1026
+
1027
+ # Create model
1028
+ model = MultimodalGlycanBERT(config)
1029
+
1030
+ # Count parameters
1031
+ total_params = sum(p.numel() for p in model.parameters())
1032
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
1033
+
1034
+ print(f"\nModel Parameters:")
1035
+ print(f" Total: {total_params:,}")
1036
+ print(f" Trainable: {trainable_params:,}")
1037
+
1038
+ # Test forward pass
1039
+ print(f"\n{'='*80}")
1040
+ print("Testing Forward Pass (with Conv front-end)")
1041
+ print("="*80)
1042
+
1043
+ batch_size = 4
1044
+ seq_len = 128
1045
+ ms_len = 50
1046
+ struct_len = 40
1047
+
1048
+ # Create dummy inputs
1049
+ seq_token_ids = torch.randint(0, config.seq_vocab_size, (batch_size, seq_len))
1050
+ seq_attention_mask = torch.ones(batch_size, seq_len)
1051
+ # Approximate: ~5 tokens per residue
1052
+ seq_residue_ids = torch.div(
1053
+ torch.arange(seq_len), 5, rounding_mode="floor"
1054
+ ).unsqueeze(0).expand(batch_size, -1)
1055
+
1056
+ ms_token_ids = torch.randint(config.ms_vocab_offset, config.ms_total_vocab_size, (batch_size, ms_len))
1057
+ ms_attention_mask = torch.ones(batch_size, ms_len)
1058
+ struct_token_ids = torch.randint(0, config.struct_vocab_size, (batch_size, struct_len))
1059
+ struct_attention_mask = torch.ones(batch_size, struct_len)
1060
+ # Approximate: 4 tokens per residue for VQ-VAE tokens
1061
+ struct_residue_ids = torch.div(
1062
+ torch.arange(struct_len), 4, rounding_mode="floor"
1063
+ ).unsqueeze(0).expand(batch_size, -1)
1064
+
1065
+ has_ms = torch.tensor([True, True, False, True])
1066
+ has_3d = torch.tensor([True, False, True, True])
1067
+
1068
+ # Create labels for MLM
1069
+ seq_labels = seq_token_ids.clone()
1070
+ seq_labels[seq_labels != config.mask_token_id] = -100
1071
+ ms_labels = ms_token_ids.clone()
1072
+ ms_labels[ms_labels != config.mask_token_id] = -100
1073
+ struct_labels = struct_token_ids.clone()
1074
+ struct_labels[struct_labels != config.mask_token_id] = -100
1075
+
1076
+ # Forward pass
1077
+ outputs = model(
1078
+ seq_token_ids=seq_token_ids,
1079
+ seq_attention_mask=seq_attention_mask,
1080
+ seq_residue_ids=seq_residue_ids,
1081
+ ms_token_ids=ms_token_ids,
1082
+ ms_attention_mask=ms_attention_mask,
1083
+ has_ms=has_ms,
1084
+ struct_token_ids=struct_token_ids,
1085
+ struct_attention_mask=struct_attention_mask,
1086
+ struct_residue_ids=struct_residue_ids,
1087
+ has_3d=has_3d,
1088
+ seq_labels=seq_labels,
1089
+ ms_labels=ms_labels,
1090
+ struct_labels=struct_labels,
1091
+ )
1092
+
1093
+ print(f"\nOutput shapes:")
1094
+ print(f" seq_logits: {outputs['seq_logits'].shape}")
1095
+ print(f" ms_logits: {outputs['ms_logits'].shape}")
1096
+ print(f" struct_logits: {outputs['struct_logits'].shape}")
1097
+ print(f" fused_repr: {outputs['fused_repr'].shape}")
1098
+
1099
+ print(f"\nLosses:")
1100
+ print(f" Total loss: {outputs['loss'].item():.4f}")
1101
+ print(f" Sequence loss: {outputs['seq_loss'].item():.4f}")
1102
+ print(f" MS loss: {outputs['ms_loss'].item():.4f}")
1103
+ print(f" Structure loss: {outputs['struct_loss'].item():.4f}")
1104
+
1105
+ print(f"\n{'='*80}")
1106
+ print("Model Test Complete!")
1107
+ print("="*80)
1108
+
releases/2026-10-02/bertose_source/data/bpe_vocabulary.json ADDED
@@ -0,0 +1,2612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "special_tokens": {
3
+ "[PAD]": 0,
4
+ "[UNK]": 1,
5
+ "[START]": 2,
6
+ "[END]": 3,
7
+ "[MASK]": 4,
8
+ "[BRANCH_OPEN]": 5,
9
+ "[BRANCH_CLOSE]": 6,
10
+ "[LINK]": 7,
11
+ "[MOD]": 8,
12
+ "[RESIDUE_ERROR]": 9
13
+ },
14
+ "token_to_id": {
15
+ "[PAD]": 0,
16
+ "[UNK]": 1,
17
+ "[START]": 2,
18
+ "[END]": 3,
19
+ "[MASK]": 4,
20
+ "[BRANCH_OPEN]": 5,
21
+ "[BRANCH_CLOSE]": 6,
22
+ "[LINK]": 7,
23
+ "[MOD]": 8,
24
+ "[RESIDUE_ERROR]": 9,
25
+ "#": 10,
26
+ "$": 11,
27
+ "%": 12,
28
+ "(": 13,
29
+ ")": 14,
30
+ "*": 15,
31
+ "-": 16,
32
+ ".": 17,
33
+ "/": 18,
34
+ "0": 19,
35
+ "1": 20,
36
+ "2": 21,
37
+ "3": 22,
38
+ "4": 23,
39
+ "5": 24,
40
+ "6": 25,
41
+ "7": 26,
42
+ "8": 27,
43
+ "9": 28,
44
+ "<": 29,
45
+ "=": 30,
46
+ ">": 31,
47
+ "?": 32,
48
+ "A": 33,
49
+ "B": 34,
50
+ "C": 35,
51
+ "D": 36,
52
+ "E": 37,
53
+ "F": 38,
54
+ "G": 39,
55
+ "H": 40,
56
+ "I": 41,
57
+ "J": 42,
58
+ "K": 43,
59
+ "L": 44,
60
+ "M": 45,
61
+ "N": 46,
62
+ "O": 47,
63
+ "P": 48,
64
+ "Q": 49,
65
+ "R": 50,
66
+ "S": 51,
67
+ "T": 52,
68
+ "U": 53,
69
+ "V": 54,
70
+ "W": 55,
71
+ "X": 56,
72
+ "Y": 57,
73
+ "Z": 58,
74
+ "^": 59,
75
+ "a": 60,
76
+ "b": 61,
77
+ "c": 62,
78
+ "d": 63,
79
+ "e": 64,
80
+ "f": 65,
81
+ "g": 66,
82
+ "h": 67,
83
+ "i": 68,
84
+ "j": 69,
85
+ "k": 70,
86
+ "l": 71,
87
+ "m": 72,
88
+ "n": 73,
89
+ "o": 74,
90
+ "p": 75,
91
+ "q": 76,
92
+ "r": 77,
93
+ "s": 78,
94
+ "t": 79,
95
+ "u": 80,
96
+ "v": 81,
97
+ "w": 82,
98
+ "x": 83,
99
+ "y": 84,
100
+ "z": 85,
101
+ "{": 86,
102
+ "|": 87,
103
+ "}": 88,
104
+ "~": 89,
105
+ "?|": 90,
106
+ "12": 91,
107
+ "1-": 92,
108
+ "-1": 93,
109
+ "1-5": 94,
110
+ "h-1": 95,
111
+ "122": 96,
112
+ "CC": 97,
113
+ "a2": 98,
114
+ "/3": 99,
115
+ "=O": 100,
116
+ "/3=O": 101,
117
+ "122h-1": 102,
118
+ "*N": 103,
119
+ "CC/3=O": 104,
120
+ "xx": 105,
121
+ "*NCC/3=O": 106,
122
+ "2*NCC/3=O": 107,
123
+ "a?|": 108,
124
+ "a?|b": 109,
125
+ "?|c": 110,
126
+ "a?|b?|c": 111,
127
+ "?|d": 112,
128
+ "a?|b?|c?|d": 113,
129
+ "?|e": 114,
130
+ "a?|b?|c?|d?|e": 115,
131
+ "?|f": 116,
132
+ "a?|b?|c?|d?|e?|f": 117,
133
+ "?-": 118,
134
+ "a2122h-1": 119,
135
+ "?|g": 120,
136
+ "a?|b?|c?|d?|e?|f?|g": 121,
137
+ "?|h": 122,
138
+ "a?|b?|c?|d?|e?|f?|g?|h": 123,
139
+ "?|i": 124,
140
+ "a?|b?|c?|d?|e?|f?|g?|h?|i": 125,
141
+ "?|j": 126,
142
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j": 127,
143
+ "112": 128,
144
+ "?|k": 129,
145
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k": 130,
146
+ "?|l": 131,
147
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l": 132,
148
+ "a2112": 133,
149
+ "?|m": 134,
150
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m": 135,
151
+ "a2112h-1": 136,
152
+ "4-": 137,
153
+ "a1": 138,
154
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|": 139,
155
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n": 140,
156
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|": 141,
157
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o": 142,
158
+ "?}": 143,
159
+ "a1122h-1": 144,
160
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|": 145,
161
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p": 146,
162
+ "?}-": 147,
163
+ "?}-{": 148,
164
+ "2-": 149,
165
+ "122h": 150,
166
+ "a2122h-1b": 151,
167
+ "3-": 152,
168
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|": 153,
169
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q": 154,
170
+ "*O": 155,
171
+ "xxxx": 156,
172
+ "b1": 157,
173
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|": 158,
174
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r": 159,
175
+ "a2122h-1x": 160,
176
+ "c1": 161,
177
+ "d1": 162,
178
+ "a2112h-1b": 163,
179
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|": 164,
180
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s": 165,
181
+ "m-1": 166,
182
+ "21": 167,
183
+ "1221": 168,
184
+ "6-": 169,
185
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|": 170,
186
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t": 171,
187
+ "CCCC": 172,
188
+ "u2": 173,
189
+ "e1": 174,
190
+ "a1122h-1a": 175,
191
+ "-2": 176,
192
+ "uxxxx": 177,
193
+ "a2112h-1x": 178,
194
+ "122h-2": 179,
195
+ "h-1x": 180,
196
+ "Aa": 181,
197
+ "2-6": 182,
198
+ "d21": 183,
199
+ "f1": 184,
200
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|": 185,
201
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u": 186,
202
+ "a1221": 187,
203
+ "O/3=O": 188,
204
+ "a1221m-1": 189,
205
+ "uxxxxh": 190,
206
+ "Aad21": 191,
207
+ "Aad21122h-2": 192,
208
+ "a4-": 193,
209
+ "a1122h-1x": 194,
210
+ "a?-": 195,
211
+ "axxxx": 196,
212
+ "c?-": 197,
213
+ "*OC": 198,
214
+ "5*NCC/3=O": 199,
215
+ "a4-b1": 200,
216
+ "axxxxh-1x": 201,
217
+ "g1": 202,
218
+ "h1": 203,
219
+ "u2122h": 204,
220
+ "b?-": 205,
221
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|": 206,
222
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v": 207,
223
+ "b4-": 208,
224
+ "a?-b1": 209,
225
+ "i1": 210,
226
+ "b4-c1": 211,
227
+ "d?-": 212,
228
+ "a1221m-1a": 213,
229
+ "CCCCCC": 214,
230
+ "*OCC/3=O": 215,
231
+ "b?-c1": 216,
232
+ "c3-": 217,
233
+ "j1": 218,
234
+ "CCCCCC$": 219,
235
+ "(CCCCCC$": 220,
236
+ "Aad21122h-2a": 221,
237
+ "a1122h-1b": 222,
238
+ "a2122h-1a": 223,
239
+ "(CCCCCC$4": 224,
240
+ "*OC(CCCCCC$4": 225,
241
+ "c3-d1": 226,
242
+ "*OC(CCCCCC$4)": 227,
243
+ "c?-d1": 228,
244
+ "SO/3=O": 229,
245
+ "SO/3=O/3=O": 230,
246
+ "a6-": 231,
247
+ "112h": 232,
248
+ "k1": 233,
249
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|": 234,
250
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w": 235,
251
+ "Aad21122h-2x": 236,
252
+ "a3-": 237,
253
+ "A-1": 238,
254
+ "a1221m-1x": 239,
255
+ "d?-e1": 240,
256
+ "*OSO/3=O/3=O": 241,
257
+ "a3-b1": 242,
258
+ "e?-": 243,
259
+ "1-?": 244,
260
+ "c6-": 245,
261
+ "b3-": 246,
262
+ "h-1a": 247,
263
+ "d2-": 248,
264
+ "u2112h": 249,
265
+ "d2-e1": 250,
266
+ "211": 251,
267
+ "l1": 252,
268
+ "2*N": 253,
269
+ "a2112h-1a": 254,
270
+ "e4-": 255,
271
+ "5*N": 256,
272
+ "AU": 257,
273
+ "u1": 258,
274
+ "a11221": 259,
275
+ "c4-": 260,
276
+ "e?-f1": 261,
277
+ "m-1a": 262,
278
+ "b3-c1": 263,
279
+ "CCO/3=O": 264,
280
+ "a2211": 265,
281
+ "5*NCCO/3=O": 266,
282
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|": 267,
283
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x": 268,
284
+ "m1": 269,
285
+ "3|": 270,
286
+ "f?-": 271,
287
+ "*OP": 272,
288
+ "d4-": 273,
289
+ "e4-f1": 274,
290
+ "a212": 275,
291
+ "a2122": 276,
292
+ "h2": 277,
293
+ "n1": 278,
294
+ "m-1x": 279,
295
+ "u1122h": 280,
296
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|": 281,
297
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y": 282,
298
+ "-b1": 283,
299
+ "h-1b": 284,
300
+ "xh": 285,
301
+ "11": 286,
302
+ "a11221h-1a": 287,
303
+ "/3O/3=O": 288,
304
+ "a2211m-1a": 289,
305
+ "c4-d1": 290,
306
+ "g?-": 291,
307
+ "a2122A-1": 292,
308
+ "1-4": 293,
309
+ "*OPO": 294,
310
+ "o1": 295,
311
+ "6*OSO/3=O/3=O": 296,
312
+ "g2": 297,
313
+ "i?-": 298,
314
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?}-{": 299,
315
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t": 300,
316
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?": 301,
317
+ "*OC(CCCCCC$4)/3=O": 302,
318
+ "h?-": 303,
319
+ "22": 304,
320
+ "Aad1": 305,
321
+ "e3-": 306,
322
+ "AUd21": 307,
323
+ "AUd21122h": 308,
324
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?}-{": 309,
325
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u": 310,
326
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?": 311,
327
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?}-{": 312,
328
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r": 313,
329
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?": 314,
330
+ "Aad1122h-2": 315,
331
+ "f?-g1": 316,
332
+ "d3-": 317,
333
+ "b6-": 318,
334
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?}-{": 319,
335
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o": 320,
336
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?": 321,
337
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|": 322,
338
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z": 323,
339
+ "f3-": 324,
340
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?}-{": 325,
341
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s": 326,
342
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?": 327,
343
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?}-{": 328,
344
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p": 329,
345
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?": 330,
346
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?}-{": 331,
347
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q": 332,
348
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?": 333,
349
+ "3*OCC/3=O": 334,
350
+ "uxxxxm": 335,
351
+ "a1-": 336,
352
+ "g?-h1": 337,
353
+ "e3-f1": 338,
354
+ "2*OCC/3=O": 339,
355
+ "p1": 340,
356
+ "j?-": 341,
357
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?}-{": 342,
358
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l": 343,
359
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?": 344,
360
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?}-{": 345,
361
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n": 346,
362
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?": 347,
363
+ "h?-i1": 348,
364
+ "a2-b1": 349,
365
+ "^X": 350,
366
+ "3*OC": 351,
367
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?}-{": 352,
368
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v": 353,
369
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?": 354,
370
+ "k?-": 355,
371
+ "4*OCC/3=O": 356,
372
+ "i?-j1": 357,
373
+ "a11221h-1x": 358,
374
+ "d3-e1": 359,
375
+ "6*OCC/3=O": 360,
376
+ "c2-": 361,
377
+ "c2": 362,
378
+ "u1221": 363,
379
+ "a1122": 364,
380
+ "a6-b1": 365,
381
+ "c2-d1": 366,
382
+ "u1221m": 367,
383
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?}-{": 368,
384
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m": 369,
385
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?": 370,
386
+ "d4-e1": 371,
387
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?}-{": 372,
388
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k": 373,
389
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?": 374,
390
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|": 375,
391
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A": 376,
392
+ "4-i1": 377,
393
+ "b2-": 378,
394
+ "*OPO/3O/3=O": 379,
395
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?}-{": 380,
396
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j": 381,
397
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?": 382,
398
+ "d2": 383,
399
+ "h2-": 384,
400
+ "b2-c1": 385,
401
+ "l?-": 386,
402
+ "^XO": 387,
403
+ "q1": 388,
404
+ "4-h1": 389,
405
+ "dxxxx": 390,
406
+ "f3-g1": 391,
407
+ "A-1a": 392,
408
+ "AUdxxxx": 393,
409
+ "CCN": 394,
410
+ "h4-i1": 395,
411
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|": 396,
412
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B": 397,
413
+ "~n": 398,
414
+ "a2122A-1b": 399,
415
+ "xh-1": 400,
416
+ "A-1x": 401,
417
+ "i4-": 402,
418
+ "j?-k1": 403,
419
+ "3*OC(CCCCCC$4)": 404,
420
+ "a211": 405,
421
+ "2*OSO/3=O/3=O": 406,
422
+ "m?-": 407,
423
+ "g4-h1": 408,
424
+ "r1": 409,
425
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?}-{": 410,
426
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?}-{a?|b?|c?|d?|e?|f?|g?|h?|i": 411,
427
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?": 412,
428
+ "g2-": 413,
429
+ "a5": 414,
430
+ "g3-": 415,
431
+ "34": 416,
432
+ "m-1b": 417,
433
+ "k?-l1": 418,
434
+ "f?-g2": 419,
435
+ "a212h-1b": 420,
436
+ "C/": 421,
437
+ "a?|b?|c?|d?|e?|f?|g?|h?}-{": 422,
438
+ "a?|b?|c?|d?|e?|f?|g?|h?}-{a?|b?|c?|d?|e?|f?|g?|h": 423,
439
+ "a?|b?|c?|d?|e?|f?|g?|h?}-{a?|b?|c?|d?|e?|f?|g?|h?": 424,
440
+ "Aad1122h-2a": 425,
441
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|": 426,
442
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C": 427,
443
+ "i4-j1": 428,
444
+ "Aad1122h-2x": 429,
445
+ "a?|b?|c?|d?|e?|f?|g?}-{": 430,
446
+ "a?|b?|c?|d?|e?|f?|g?}-{a?|b?|c?|d?|e?|f?|g": 431,
447
+ "a?|b?|c?|d?|e?|f?|g?}-{a?|b?|c?|d?|e?|f?|g?": 432,
448
+ "d6-": 433,
449
+ "h2-i1": 434,
450
+ "2*NSO/3=O/3=O": 435,
451
+ "*OP^XO": 436,
452
+ "2*OC(CCCCCC$4)": 437,
453
+ "a2211m-1x": 438,
454
+ "ax": 439,
455
+ "f4-": 440,
456
+ "4|": 441,
457
+ "a?|b?|c?|d?|e?|f?}-{": 442,
458
+ "a?|b?|c?|d?|e?|f?}-{a?|b?|c?|d?|e?|f": 443,
459
+ "a?|b?|c?|d?|e?|f?}-{a?|b?|c?|d?|e?|f?": 444,
460
+ "f4-g1": 445,
461
+ "l?-m1": 446,
462
+ "e2-": 447,
463
+ "*OCC": 448,
464
+ "AUdxxxxxh": 449,
465
+ "h2122h": 450,
466
+ "e2-f1": 451,
467
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?}-{": 452,
468
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w": 453,
469
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?": 454,
470
+ "b2": 455,
471
+ "i2-": 456,
472
+ "4*OC(CCCCCC$4)": 457,
473
+ "g2-h1": 458,
474
+ "a212h-1x": 459,
475
+ "j4-": 460,
476
+ "l4-": 461,
477
+ "g3-h1": 462,
478
+ "6*OC(CCCCCC$4)": 463,
479
+ "a2122A-1x": 464,
480
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|": 465,
481
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D": 466,
482
+ "e2": 467,
483
+ "O/": 468,
484
+ "i2-j1": 469,
485
+ "i3-": 470,
486
+ "a5-b1": 471,
487
+ "e4-g1": 472,
488
+ "a?|b?|c?|d?|e?}-{": 473,
489
+ "a?|b?|c?|d?|e?}-{a?|b?|c?|d?|e": 474,
490
+ "a?|b?|c?|d?|e?}-{a?|b?|c?|d?|e?": 475,
491
+ "f6-": 476,
492
+ "/3C": 477,
493
+ "s1": 478,
494
+ "j4-k1": 479,
495
+ "xh-1u": 480,
496
+ "h2112h": 481,
497
+ "h3-": 482,
498
+ "c6-h1": 483,
499
+ "k2-": 484,
500
+ "3*OSO/3=O/3=O": 485,
501
+ "l4-m1": 486,
502
+ "j2": 487,
503
+ "2-5": 488,
504
+ "a2121": 489,
505
+ "d1122h": 490,
506
+ "a4-c1": 491,
507
+ "AUd1122h": 492,
508
+ "n?-": 493,
509
+ "8-": 494,
510
+ "*OP^XOCCN": 495,
511
+ "2*OC": 496,
512
+ "a?|b?|c?|d?}-{": 497,
513
+ "a?|b?|c?|d?}-{a?|b?|c?|d": 498,
514
+ "a?|b?|c?|d?}-{a?|b?|c?|d?": 499,
515
+ "axx": 500,
516
+ "7-": 501,
517
+ "uxxxxxh": 502,
518
+ "f2-": 503,
519
+ "c6-g1": 504,
520
+ "j2-": 505,
521
+ "2*OC(CCCCCC$4)/3=O": 506,
522
+ "f2": 507,
523
+ "k4-": 508,
524
+ "m?-n1": 509,
525
+ "4*OSO/3=O/3=O": 510,
526
+ "f2-g1": 511,
527
+ "*OS": 512,
528
+ "h3-i1": 513,
529
+ "222": 514,
530
+ "k2-l1": 515,
531
+ "1221h": 516,
532
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|": 517,
533
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E": 518,
534
+ "axxxxm-1x": 519,
535
+ "i2": 520,
536
+ "u11221h": 521,
537
+ "4*OPO/3O/3=O": 522,
538
+ "334": 523,
539
+ "b6-c1": 524,
540
+ "c6-i1": 525,
541
+ "d4-h1": 526,
542
+ "d?-h1": 527,
543
+ "o?-": 528,
544
+ "k2": 529,
545
+ "6*OC": 530,
546
+ "a11222": 531,
547
+ "c?-i1": 532,
548
+ "a122h-1": 533,
549
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|": 534,
550
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F": 535,
551
+ "c?-h1": 536,
552
+ "3*OC(CCCCCC$4)/3=O": 537,
553
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|": 538,
554
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G": 539,
555
+ "i3-j1": 540,
556
+ "4*OC": 541,
557
+ "ha": 542,
558
+ "j2-k1": 543,
559
+ "j3-": 544,
560
+ "a4": 545,
561
+ "6|": 546,
562
+ "*OP^XOCCN/3O/3=O": 547,
563
+ "C/4": 548,
564
+ "a2112A-1a": 549,
565
+ "m4-": 550,
566
+ "n2": 551,
567
+ "c6-k1": 552,
568
+ "k4-l1": 553,
569
+ "ad": 554,
570
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?}-{": 555,
571
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x": 556,
572
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?": 557,
573
+ "t1": 558,
574
+ "a21": 559,
575
+ "4*N": 560,
576
+ "u2211": 561,
577
+ "a?|b?|c?}-{": 562,
578
+ "a?|b?|c?}-{a?|b?|c": 563,
579
+ "a?|b?|c?}-{a?|b?|c?": 564,
580
+ "a12": 565,
581
+ "CCCCCCCC": 566,
582
+ "g6-": 567,
583
+ "f3-g2": 568,
584
+ "u2211m": 569,
585
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|": 570,
586
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|H": 571,
587
+ "5-": 572,
588
+ "ha122h-2": 573,
589
+ "e6-": 574,
590
+ "h6-": 575,
591
+ "3|g": 576,
592
+ "c6-d1": 577,
593
+ "4*OC(CCCCCC$4)/3=O": 578,
594
+ "3*NCC/3=O": 579,
595
+ "2-?": 580,
596
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|H?|": 581,
597
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|H?|I": 582,
598
+ "?}*OC": 583,
599
+ "C/3=O": 584,
600
+ "i6-": 585,
601
+ "c?-k1": 586,
602
+ "c?-g1": 587,
603
+ "l2-": 588,
604
+ "a22": 589
605
+ },
606
+ "merges": [
607
+ [
608
+ "?",
609
+ "|"
610
+ ],
611
+ [
612
+ "1",
613
+ "2"
614
+ ],
615
+ [
616
+ "1",
617
+ "-"
618
+ ],
619
+ [
620
+ "-",
621
+ "1"
622
+ ],
623
+ [
624
+ "1-",
625
+ "5"
626
+ ],
627
+ [
628
+ "h",
629
+ "-1"
630
+ ],
631
+ [
632
+ "12",
633
+ "2"
634
+ ],
635
+ [
636
+ "C",
637
+ "C"
638
+ ],
639
+ [
640
+ "a",
641
+ "2"
642
+ ],
643
+ [
644
+ "/",
645
+ "3"
646
+ ],
647
+ [
648
+ "=",
649
+ "O"
650
+ ],
651
+ [
652
+ "/3",
653
+ "=O"
654
+ ],
655
+ [
656
+ "122",
657
+ "h-1"
658
+ ],
659
+ [
660
+ "*",
661
+ "N"
662
+ ],
663
+ [
664
+ "CC",
665
+ "/3=O"
666
+ ],
667
+ [
668
+ "x",
669
+ "x"
670
+ ],
671
+ [
672
+ "*N",
673
+ "CC/3=O"
674
+ ],
675
+ [
676
+ "2",
677
+ "*NCC/3=O"
678
+ ],
679
+ [
680
+ "a",
681
+ "?|"
682
+ ],
683
+ [
684
+ "a?|",
685
+ "b"
686
+ ],
687
+ [
688
+ "?|",
689
+ "c"
690
+ ],
691
+ [
692
+ "a?|b",
693
+ "?|c"
694
+ ],
695
+ [
696
+ "?|",
697
+ "d"
698
+ ],
699
+ [
700
+ "a?|b?|c",
701
+ "?|d"
702
+ ],
703
+ [
704
+ "?|",
705
+ "e"
706
+ ],
707
+ [
708
+ "a?|b?|c?|d",
709
+ "?|e"
710
+ ],
711
+ [
712
+ "?|",
713
+ "f"
714
+ ],
715
+ [
716
+ "a?|b?|c?|d?|e",
717
+ "?|f"
718
+ ],
719
+ [
720
+ "?",
721
+ "-"
722
+ ],
723
+ [
724
+ "a2",
725
+ "122h-1"
726
+ ],
727
+ [
728
+ "?|",
729
+ "g"
730
+ ],
731
+ [
732
+ "a?|b?|c?|d?|e?|f",
733
+ "?|g"
734
+ ],
735
+ [
736
+ "?|",
737
+ "h"
738
+ ],
739
+ [
740
+ "a?|b?|c?|d?|e?|f?|g",
741
+ "?|h"
742
+ ],
743
+ [
744
+ "?|",
745
+ "i"
746
+ ],
747
+ [
748
+ "a?|b?|c?|d?|e?|f?|g?|h",
749
+ "?|i"
750
+ ],
751
+ [
752
+ "?|",
753
+ "j"
754
+ ],
755
+ [
756
+ "a?|b?|c?|d?|e?|f?|g?|h?|i",
757
+ "?|j"
758
+ ],
759
+ [
760
+ "1",
761
+ "12"
762
+ ],
763
+ [
764
+ "?|",
765
+ "k"
766
+ ],
767
+ [
768
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j",
769
+ "?|k"
770
+ ],
771
+ [
772
+ "?|",
773
+ "l"
774
+ ],
775
+ [
776
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k",
777
+ "?|l"
778
+ ],
779
+ [
780
+ "a2",
781
+ "112"
782
+ ],
783
+ [
784
+ "?|",
785
+ "m"
786
+ ],
787
+ [
788
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l",
789
+ "?|m"
790
+ ],
791
+ [
792
+ "a2112",
793
+ "h-1"
794
+ ],
795
+ [
796
+ "4",
797
+ "-"
798
+ ],
799
+ [
800
+ "a",
801
+ "1"
802
+ ],
803
+ [
804
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m",
805
+ "?|"
806
+ ],
807
+ [
808
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|",
809
+ "n"
810
+ ],
811
+ [
812
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n",
813
+ "?|"
814
+ ],
815
+ [
816
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|",
817
+ "o"
818
+ ],
819
+ [
820
+ "?",
821
+ "}"
822
+ ],
823
+ [
824
+ "a1",
825
+ "122h-1"
826
+ ],
827
+ [
828
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o",
829
+ "?|"
830
+ ],
831
+ [
832
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|",
833
+ "p"
834
+ ],
835
+ [
836
+ "?}",
837
+ "-"
838
+ ],
839
+ [
840
+ "?}-",
841
+ "{"
842
+ ],
843
+ [
844
+ "2",
845
+ "-"
846
+ ],
847
+ [
848
+ "122",
849
+ "h"
850
+ ],
851
+ [
852
+ "a2122h-1",
853
+ "b"
854
+ ],
855
+ [
856
+ "3",
857
+ "-"
858
+ ],
859
+ [
860
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p",
861
+ "?|"
862
+ ],
863
+ [
864
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|",
865
+ "q"
866
+ ],
867
+ [
868
+ "*",
869
+ "O"
870
+ ],
871
+ [
872
+ "xx",
873
+ "xx"
874
+ ],
875
+ [
876
+ "b",
877
+ "1"
878
+ ],
879
+ [
880
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q",
881
+ "?|"
882
+ ],
883
+ [
884
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|",
885
+ "r"
886
+ ],
887
+ [
888
+ "a2122h-1",
889
+ "x"
890
+ ],
891
+ [
892
+ "c",
893
+ "1"
894
+ ],
895
+ [
896
+ "d",
897
+ "1"
898
+ ],
899
+ [
900
+ "a2112h-1",
901
+ "b"
902
+ ],
903
+ [
904
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r",
905
+ "?|"
906
+ ],
907
+ [
908
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|",
909
+ "s"
910
+ ],
911
+ [
912
+ "m",
913
+ "-1"
914
+ ],
915
+ [
916
+ "2",
917
+ "1"
918
+ ],
919
+ [
920
+ "122",
921
+ "1"
922
+ ],
923
+ [
924
+ "6",
925
+ "-"
926
+ ],
927
+ [
928
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s",
929
+ "?|"
930
+ ],
931
+ [
932
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|",
933
+ "t"
934
+ ],
935
+ [
936
+ "CC",
937
+ "CC"
938
+ ],
939
+ [
940
+ "u",
941
+ "2"
942
+ ],
943
+ [
944
+ "e",
945
+ "1"
946
+ ],
947
+ [
948
+ "a1122h-1",
949
+ "a"
950
+ ],
951
+ [
952
+ "-",
953
+ "2"
954
+ ],
955
+ [
956
+ "u",
957
+ "xxxx"
958
+ ],
959
+ [
960
+ "a2112h-1",
961
+ "x"
962
+ ],
963
+ [
964
+ "122h",
965
+ "-2"
966
+ ],
967
+ [
968
+ "h-1",
969
+ "x"
970
+ ],
971
+ [
972
+ "A",
973
+ "a"
974
+ ],
975
+ [
976
+ "2-",
977
+ "6"
978
+ ],
979
+ [
980
+ "d",
981
+ "21"
982
+ ],
983
+ [
984
+ "f",
985
+ "1"
986
+ ],
987
+ [
988
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t",
989
+ "?|"
990
+ ],
991
+ [
992
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|",
993
+ "u"
994
+ ],
995
+ [
996
+ "a",
997
+ "1221"
998
+ ],
999
+ [
1000
+ "O",
1001
+ "/3=O"
1002
+ ],
1003
+ [
1004
+ "a1221",
1005
+ "m-1"
1006
+ ],
1007
+ [
1008
+ "uxxxx",
1009
+ "h"
1010
+ ],
1011
+ [
1012
+ "Aa",
1013
+ "d21"
1014
+ ],
1015
+ [
1016
+ "Aad21",
1017
+ "122h-2"
1018
+ ],
1019
+ [
1020
+ "a",
1021
+ "4-"
1022
+ ],
1023
+ [
1024
+ "a1122h-1",
1025
+ "x"
1026
+ ],
1027
+ [
1028
+ "a",
1029
+ "?-"
1030
+ ],
1031
+ [
1032
+ "a",
1033
+ "xxxx"
1034
+ ],
1035
+ [
1036
+ "c",
1037
+ "?-"
1038
+ ],
1039
+ [
1040
+ "*O",
1041
+ "C"
1042
+ ],
1043
+ [
1044
+ "5",
1045
+ "*NCC/3=O"
1046
+ ],
1047
+ [
1048
+ "a4-",
1049
+ "b1"
1050
+ ],
1051
+ [
1052
+ "axxxx",
1053
+ "h-1x"
1054
+ ],
1055
+ [
1056
+ "g",
1057
+ "1"
1058
+ ],
1059
+ [
1060
+ "h",
1061
+ "1"
1062
+ ],
1063
+ [
1064
+ "u2",
1065
+ "122h"
1066
+ ],
1067
+ [
1068
+ "b",
1069
+ "?-"
1070
+ ],
1071
+ [
1072
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u",
1073
+ "?|"
1074
+ ],
1075
+ [
1076
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|",
1077
+ "v"
1078
+ ],
1079
+ [
1080
+ "b",
1081
+ "4-"
1082
+ ],
1083
+ [
1084
+ "a?-",
1085
+ "b1"
1086
+ ],
1087
+ [
1088
+ "i",
1089
+ "1"
1090
+ ],
1091
+ [
1092
+ "b4-",
1093
+ "c1"
1094
+ ],
1095
+ [
1096
+ "d",
1097
+ "?-"
1098
+ ],
1099
+ [
1100
+ "a1221m-1",
1101
+ "a"
1102
+ ],
1103
+ [
1104
+ "CCCC",
1105
+ "CC"
1106
+ ],
1107
+ [
1108
+ "*O",
1109
+ "CC/3=O"
1110
+ ],
1111
+ [
1112
+ "b?-",
1113
+ "c1"
1114
+ ],
1115
+ [
1116
+ "c",
1117
+ "3-"
1118
+ ],
1119
+ [
1120
+ "j",
1121
+ "1"
1122
+ ],
1123
+ [
1124
+ "CCCCCC",
1125
+ "$"
1126
+ ],
1127
+ [
1128
+ "(",
1129
+ "CCCCCC$"
1130
+ ],
1131
+ [
1132
+ "Aad21122h-2",
1133
+ "a"
1134
+ ],
1135
+ [
1136
+ "a1122h-1",
1137
+ "b"
1138
+ ],
1139
+ [
1140
+ "a2122h-1",
1141
+ "a"
1142
+ ],
1143
+ [
1144
+ "(CCCCCC$",
1145
+ "4"
1146
+ ],
1147
+ [
1148
+ "*OC",
1149
+ "(CCCCCC$4"
1150
+ ],
1151
+ [
1152
+ "c3-",
1153
+ "d1"
1154
+ ],
1155
+ [
1156
+ "*OC(CCCCCC$4",
1157
+ ")"
1158
+ ],
1159
+ [
1160
+ "c?-",
1161
+ "d1"
1162
+ ],
1163
+ [
1164
+ "S",
1165
+ "O/3=O"
1166
+ ],
1167
+ [
1168
+ "SO/3=O",
1169
+ "/3=O"
1170
+ ],
1171
+ [
1172
+ "a",
1173
+ "6-"
1174
+ ],
1175
+ [
1176
+ "112",
1177
+ "h"
1178
+ ],
1179
+ [
1180
+ "k",
1181
+ "1"
1182
+ ],
1183
+ [
1184
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v",
1185
+ "?|"
1186
+ ],
1187
+ [
1188
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|",
1189
+ "w"
1190
+ ],
1191
+ [
1192
+ "Aad21122h-2",
1193
+ "x"
1194
+ ],
1195
+ [
1196
+ "a",
1197
+ "3-"
1198
+ ],
1199
+ [
1200
+ "A",
1201
+ "-1"
1202
+ ],
1203
+ [
1204
+ "a1221m-1",
1205
+ "x"
1206
+ ],
1207
+ [
1208
+ "d?-",
1209
+ "e1"
1210
+ ],
1211
+ [
1212
+ "*O",
1213
+ "SO/3=O/3=O"
1214
+ ],
1215
+ [
1216
+ "a3-",
1217
+ "b1"
1218
+ ],
1219
+ [
1220
+ "e",
1221
+ "?-"
1222
+ ],
1223
+ [
1224
+ "1-",
1225
+ "?"
1226
+ ],
1227
+ [
1228
+ "c",
1229
+ "6-"
1230
+ ],
1231
+ [
1232
+ "b",
1233
+ "3-"
1234
+ ],
1235
+ [
1236
+ "h-1",
1237
+ "a"
1238
+ ],
1239
+ [
1240
+ "d",
1241
+ "2-"
1242
+ ],
1243
+ [
1244
+ "u2",
1245
+ "112h"
1246
+ ],
1247
+ [
1248
+ "d2-",
1249
+ "e1"
1250
+ ],
1251
+ [
1252
+ "21",
1253
+ "1"
1254
+ ],
1255
+ [
1256
+ "l",
1257
+ "1"
1258
+ ],
1259
+ [
1260
+ "2",
1261
+ "*N"
1262
+ ],
1263
+ [
1264
+ "a2112h-1",
1265
+ "a"
1266
+ ],
1267
+ [
1268
+ "e",
1269
+ "4-"
1270
+ ],
1271
+ [
1272
+ "5",
1273
+ "*N"
1274
+ ],
1275
+ [
1276
+ "A",
1277
+ "U"
1278
+ ],
1279
+ [
1280
+ "u",
1281
+ "1"
1282
+ ],
1283
+ [
1284
+ "a1",
1285
+ "1221"
1286
+ ],
1287
+ [
1288
+ "c",
1289
+ "4-"
1290
+ ],
1291
+ [
1292
+ "e?-",
1293
+ "f1"
1294
+ ],
1295
+ [
1296
+ "m-1",
1297
+ "a"
1298
+ ],
1299
+ [
1300
+ "b3-",
1301
+ "c1"
1302
+ ],
1303
+ [
1304
+ "CC",
1305
+ "O/3=O"
1306
+ ],
1307
+ [
1308
+ "a2",
1309
+ "211"
1310
+ ],
1311
+ [
1312
+ "5*N",
1313
+ "CCO/3=O"
1314
+ ],
1315
+ [
1316
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w",
1317
+ "?|"
1318
+ ],
1319
+ [
1320
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|",
1321
+ "x"
1322
+ ],
1323
+ [
1324
+ "m",
1325
+ "1"
1326
+ ],
1327
+ [
1328
+ "3",
1329
+ "|"
1330
+ ],
1331
+ [
1332
+ "f",
1333
+ "?-"
1334
+ ],
1335
+ [
1336
+ "*O",
1337
+ "P"
1338
+ ],
1339
+ [
1340
+ "d",
1341
+ "4-"
1342
+ ],
1343
+ [
1344
+ "e4-",
1345
+ "f1"
1346
+ ],
1347
+ [
1348
+ "a2",
1349
+ "12"
1350
+ ],
1351
+ [
1352
+ "a2",
1353
+ "122"
1354
+ ],
1355
+ [
1356
+ "h",
1357
+ "2"
1358
+ ],
1359
+ [
1360
+ "n",
1361
+ "1"
1362
+ ],
1363
+ [
1364
+ "m-1",
1365
+ "x"
1366
+ ],
1367
+ [
1368
+ "u1",
1369
+ "122h"
1370
+ ],
1371
+ [
1372
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x",
1373
+ "?|"
1374
+ ],
1375
+ [
1376
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|",
1377
+ "y"
1378
+ ],
1379
+ [
1380
+ "-",
1381
+ "b1"
1382
+ ],
1383
+ [
1384
+ "h-1",
1385
+ "b"
1386
+ ],
1387
+ [
1388
+ "x",
1389
+ "h"
1390
+ ],
1391
+ [
1392
+ "1",
1393
+ "1"
1394
+ ],
1395
+ [
1396
+ "a11221",
1397
+ "h-1a"
1398
+ ],
1399
+ [
1400
+ "/3",
1401
+ "O/3=O"
1402
+ ],
1403
+ [
1404
+ "a2211",
1405
+ "m-1a"
1406
+ ],
1407
+ [
1408
+ "c4-",
1409
+ "d1"
1410
+ ],
1411
+ [
1412
+ "g",
1413
+ "?-"
1414
+ ],
1415
+ [
1416
+ "a2122",
1417
+ "A-1"
1418
+ ],
1419
+ [
1420
+ "1-",
1421
+ "4"
1422
+ ],
1423
+ [
1424
+ "*OP",
1425
+ "O"
1426
+ ],
1427
+ [
1428
+ "o",
1429
+ "1"
1430
+ ],
1431
+ [
1432
+ "6",
1433
+ "*OSO/3=O/3=O"
1434
+ ],
1435
+ [
1436
+ "g",
1437
+ "2"
1438
+ ],
1439
+ [
1440
+ "i",
1441
+ "?-"
1442
+ ],
1443
+ [
1444
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t",
1445
+ "?}-{"
1446
+ ],
1447
+ [
1448
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?}-{",
1449
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t"
1450
+ ],
1451
+ [
1452
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t",
1453
+ "?"
1454
+ ],
1455
+ [
1456
+ "*OC(CCCCCC$4)",
1457
+ "/3=O"
1458
+ ],
1459
+ [
1460
+ "h",
1461
+ "?-"
1462
+ ],
1463
+ [
1464
+ "2",
1465
+ "2"
1466
+ ],
1467
+ [
1468
+ "Aa",
1469
+ "d1"
1470
+ ],
1471
+ [
1472
+ "e",
1473
+ "3-"
1474
+ ],
1475
+ [
1476
+ "AU",
1477
+ "d21"
1478
+ ],
1479
+ [
1480
+ "AUd21",
1481
+ "122h"
1482
+ ],
1483
+ [
1484
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u",
1485
+ "?}-{"
1486
+ ],
1487
+ [
1488
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?}-{",
1489
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u"
1490
+ ],
1491
+ [
1492
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u",
1493
+ "?"
1494
+ ],
1495
+ [
1496
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r",
1497
+ "?}-{"
1498
+ ],
1499
+ [
1500
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?}-{",
1501
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r"
1502
+ ],
1503
+ [
1504
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r",
1505
+ "?"
1506
+ ],
1507
+ [
1508
+ "Aad1",
1509
+ "122h-2"
1510
+ ],
1511
+ [
1512
+ "f?-",
1513
+ "g1"
1514
+ ],
1515
+ [
1516
+ "d",
1517
+ "3-"
1518
+ ],
1519
+ [
1520
+ "b",
1521
+ "6-"
1522
+ ],
1523
+ [
1524
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o",
1525
+ "?}-{"
1526
+ ],
1527
+ [
1528
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?}-{",
1529
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o"
1530
+ ],
1531
+ [
1532
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o",
1533
+ "?"
1534
+ ],
1535
+ [
1536
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y",
1537
+ "?|"
1538
+ ],
1539
+ [
1540
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|",
1541
+ "z"
1542
+ ],
1543
+ [
1544
+ "f",
1545
+ "3-"
1546
+ ],
1547
+ [
1548
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s",
1549
+ "?}-{"
1550
+ ],
1551
+ [
1552
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?}-{",
1553
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s"
1554
+ ],
1555
+ [
1556
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s",
1557
+ "?"
1558
+ ],
1559
+ [
1560
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p",
1561
+ "?}-{"
1562
+ ],
1563
+ [
1564
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?}-{",
1565
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p"
1566
+ ],
1567
+ [
1568
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p",
1569
+ "?"
1570
+ ],
1571
+ [
1572
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q",
1573
+ "?}-{"
1574
+ ],
1575
+ [
1576
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?}-{",
1577
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q"
1578
+ ],
1579
+ [
1580
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q",
1581
+ "?"
1582
+ ],
1583
+ [
1584
+ "3",
1585
+ "*OCC/3=O"
1586
+ ],
1587
+ [
1588
+ "uxxxx",
1589
+ "m"
1590
+ ],
1591
+ [
1592
+ "a",
1593
+ "1-"
1594
+ ],
1595
+ [
1596
+ "g?-",
1597
+ "h1"
1598
+ ],
1599
+ [
1600
+ "e3-",
1601
+ "f1"
1602
+ ],
1603
+ [
1604
+ "2",
1605
+ "*OCC/3=O"
1606
+ ],
1607
+ [
1608
+ "p",
1609
+ "1"
1610
+ ],
1611
+ [
1612
+ "j",
1613
+ "?-"
1614
+ ],
1615
+ [
1616
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l",
1617
+ "?}-{"
1618
+ ],
1619
+ [
1620
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?}-{",
1621
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l"
1622
+ ],
1623
+ [
1624
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l",
1625
+ "?"
1626
+ ],
1627
+ [
1628
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n",
1629
+ "?}-{"
1630
+ ],
1631
+ [
1632
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?}-{",
1633
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n"
1634
+ ],
1635
+ [
1636
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n",
1637
+ "?"
1638
+ ],
1639
+ [
1640
+ "h?-",
1641
+ "i1"
1642
+ ],
1643
+ [
1644
+ "a2",
1645
+ "-b1"
1646
+ ],
1647
+ [
1648
+ "^",
1649
+ "X"
1650
+ ],
1651
+ [
1652
+ "3",
1653
+ "*OC"
1654
+ ],
1655
+ [
1656
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v",
1657
+ "?}-{"
1658
+ ],
1659
+ [
1660
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?}-{",
1661
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v"
1662
+ ],
1663
+ [
1664
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v",
1665
+ "?"
1666
+ ],
1667
+ [
1668
+ "k",
1669
+ "?-"
1670
+ ],
1671
+ [
1672
+ "4",
1673
+ "*OCC/3=O"
1674
+ ],
1675
+ [
1676
+ "i?-",
1677
+ "j1"
1678
+ ],
1679
+ [
1680
+ "a11221",
1681
+ "h-1x"
1682
+ ],
1683
+ [
1684
+ "d3-",
1685
+ "e1"
1686
+ ],
1687
+ [
1688
+ "6",
1689
+ "*OCC/3=O"
1690
+ ],
1691
+ [
1692
+ "c",
1693
+ "2-"
1694
+ ],
1695
+ [
1696
+ "c",
1697
+ "2"
1698
+ ],
1699
+ [
1700
+ "u",
1701
+ "1221"
1702
+ ],
1703
+ [
1704
+ "a1",
1705
+ "122"
1706
+ ],
1707
+ [
1708
+ "a6-",
1709
+ "b1"
1710
+ ],
1711
+ [
1712
+ "c2-",
1713
+ "d1"
1714
+ ],
1715
+ [
1716
+ "u1221",
1717
+ "m"
1718
+ ],
1719
+ [
1720
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m",
1721
+ "?}-{"
1722
+ ],
1723
+ [
1724
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?}-{",
1725
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m"
1726
+ ],
1727
+ [
1728
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m",
1729
+ "?"
1730
+ ],
1731
+ [
1732
+ "d4-",
1733
+ "e1"
1734
+ ],
1735
+ [
1736
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k",
1737
+ "?}-{"
1738
+ ],
1739
+ [
1740
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?}-{",
1741
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k"
1742
+ ],
1743
+ [
1744
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k",
1745
+ "?"
1746
+ ],
1747
+ [
1748
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z",
1749
+ "?|"
1750
+ ],
1751
+ [
1752
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|",
1753
+ "A"
1754
+ ],
1755
+ [
1756
+ "4-",
1757
+ "i1"
1758
+ ],
1759
+ [
1760
+ "b",
1761
+ "2-"
1762
+ ],
1763
+ [
1764
+ "*OPO",
1765
+ "/3O/3=O"
1766
+ ],
1767
+ [
1768
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j",
1769
+ "?}-{"
1770
+ ],
1771
+ [
1772
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?}-{",
1773
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j"
1774
+ ],
1775
+ [
1776
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j",
1777
+ "?"
1778
+ ],
1779
+ [
1780
+ "d",
1781
+ "2"
1782
+ ],
1783
+ [
1784
+ "h",
1785
+ "2-"
1786
+ ],
1787
+ [
1788
+ "b2-",
1789
+ "c1"
1790
+ ],
1791
+ [
1792
+ "l",
1793
+ "?-"
1794
+ ],
1795
+ [
1796
+ "^X",
1797
+ "O"
1798
+ ],
1799
+ [
1800
+ "q",
1801
+ "1"
1802
+ ],
1803
+ [
1804
+ "4-",
1805
+ "h1"
1806
+ ],
1807
+ [
1808
+ "d",
1809
+ "xxxx"
1810
+ ],
1811
+ [
1812
+ "f3-",
1813
+ "g1"
1814
+ ],
1815
+ [
1816
+ "A-1",
1817
+ "a"
1818
+ ],
1819
+ [
1820
+ "AU",
1821
+ "dxxxx"
1822
+ ],
1823
+ [
1824
+ "CC",
1825
+ "N"
1826
+ ],
1827
+ [
1828
+ "h",
1829
+ "4-i1"
1830
+ ],
1831
+ [
1832
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A",
1833
+ "?|"
1834
+ ],
1835
+ [
1836
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|",
1837
+ "B"
1838
+ ],
1839
+ [
1840
+ "~",
1841
+ "n"
1842
+ ],
1843
+ [
1844
+ "a2122A-1",
1845
+ "b"
1846
+ ],
1847
+ [
1848
+ "x",
1849
+ "h-1"
1850
+ ],
1851
+ [
1852
+ "A-1",
1853
+ "x"
1854
+ ],
1855
+ [
1856
+ "i",
1857
+ "4-"
1858
+ ],
1859
+ [
1860
+ "j?-",
1861
+ "k1"
1862
+ ],
1863
+ [
1864
+ "3",
1865
+ "*OC(CCCCCC$4)"
1866
+ ],
1867
+ [
1868
+ "a2",
1869
+ "11"
1870
+ ],
1871
+ [
1872
+ "2",
1873
+ "*OSO/3=O/3=O"
1874
+ ],
1875
+ [
1876
+ "m",
1877
+ "?-"
1878
+ ],
1879
+ [
1880
+ "g",
1881
+ "4-h1"
1882
+ ],
1883
+ [
1884
+ "r",
1885
+ "1"
1886
+ ],
1887
+ [
1888
+ "a?|b?|c?|d?|e?|f?|g?|h?|i",
1889
+ "?}-{"
1890
+ ],
1891
+ [
1892
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?}-{",
1893
+ "a?|b?|c?|d?|e?|f?|g?|h?|i"
1894
+ ],
1895
+ [
1896
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?}-{a?|b?|c?|d?|e?|f?|g?|h?|i",
1897
+ "?"
1898
+ ],
1899
+ [
1900
+ "g",
1901
+ "2-"
1902
+ ],
1903
+ [
1904
+ "a",
1905
+ "5"
1906
+ ],
1907
+ [
1908
+ "g",
1909
+ "3-"
1910
+ ],
1911
+ [
1912
+ "3",
1913
+ "4"
1914
+ ],
1915
+ [
1916
+ "m-1",
1917
+ "b"
1918
+ ],
1919
+ [
1920
+ "k?-",
1921
+ "l1"
1922
+ ],
1923
+ [
1924
+ "f?-",
1925
+ "g2"
1926
+ ],
1927
+ [
1928
+ "a212",
1929
+ "h-1b"
1930
+ ],
1931
+ [
1932
+ "C",
1933
+ "/"
1934
+ ],
1935
+ [
1936
+ "a?|b?|c?|d?|e?|f?|g?|h",
1937
+ "?}-{"
1938
+ ],
1939
+ [
1940
+ "a?|b?|c?|d?|e?|f?|g?|h?}-{",
1941
+ "a?|b?|c?|d?|e?|f?|g?|h"
1942
+ ],
1943
+ [
1944
+ "a?|b?|c?|d?|e?|f?|g?|h?}-{a?|b?|c?|d?|e?|f?|g?|h",
1945
+ "?"
1946
+ ],
1947
+ [
1948
+ "Aad1122h-2",
1949
+ "a"
1950
+ ],
1951
+ [
1952
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B",
1953
+ "?|"
1954
+ ],
1955
+ [
1956
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|",
1957
+ "C"
1958
+ ],
1959
+ [
1960
+ "i4-",
1961
+ "j1"
1962
+ ],
1963
+ [
1964
+ "Aad1122h-2",
1965
+ "x"
1966
+ ],
1967
+ [
1968
+ "a?|b?|c?|d?|e?|f?|g",
1969
+ "?}-{"
1970
+ ],
1971
+ [
1972
+ "a?|b?|c?|d?|e?|f?|g?}-{",
1973
+ "a?|b?|c?|d?|e?|f?|g"
1974
+ ],
1975
+ [
1976
+ "a?|b?|c?|d?|e?|f?|g?}-{a?|b?|c?|d?|e?|f?|g",
1977
+ "?"
1978
+ ],
1979
+ [
1980
+ "d",
1981
+ "6-"
1982
+ ],
1983
+ [
1984
+ "h2-",
1985
+ "i1"
1986
+ ],
1987
+ [
1988
+ "2*N",
1989
+ "SO/3=O/3=O"
1990
+ ],
1991
+ [
1992
+ "*OP",
1993
+ "^XO"
1994
+ ],
1995
+ [
1996
+ "2",
1997
+ "*OC(CCCCCC$4)"
1998
+ ],
1999
+ [
2000
+ "a2211",
2001
+ "m-1x"
2002
+ ],
2003
+ [
2004
+ "a",
2005
+ "x"
2006
+ ],
2007
+ [
2008
+ "f",
2009
+ "4-"
2010
+ ],
2011
+ [
2012
+ "4",
2013
+ "|"
2014
+ ],
2015
+ [
2016
+ "a?|b?|c?|d?|e?|f",
2017
+ "?}-{"
2018
+ ],
2019
+ [
2020
+ "a?|b?|c?|d?|e?|f?}-{",
2021
+ "a?|b?|c?|d?|e?|f"
2022
+ ],
2023
+ [
2024
+ "a?|b?|c?|d?|e?|f?}-{a?|b?|c?|d?|e?|f",
2025
+ "?"
2026
+ ],
2027
+ [
2028
+ "f4-",
2029
+ "g1"
2030
+ ],
2031
+ [
2032
+ "l?-",
2033
+ "m1"
2034
+ ],
2035
+ [
2036
+ "e",
2037
+ "2-"
2038
+ ],
2039
+ [
2040
+ "*O",
2041
+ "CC"
2042
+ ],
2043
+ [
2044
+ "AUdxxxx",
2045
+ "xh"
2046
+ ],
2047
+ [
2048
+ "h2",
2049
+ "122h"
2050
+ ],
2051
+ [
2052
+ "e2-",
2053
+ "f1"
2054
+ ],
2055
+ [
2056
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w",
2057
+ "?}-{"
2058
+ ],
2059
+ [
2060
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?}-{",
2061
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w"
2062
+ ],
2063
+ [
2064
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w",
2065
+ "?"
2066
+ ],
2067
+ [
2068
+ "b",
2069
+ "2"
2070
+ ],
2071
+ [
2072
+ "i",
2073
+ "2-"
2074
+ ],
2075
+ [
2076
+ "4",
2077
+ "*OC(CCCCCC$4)"
2078
+ ],
2079
+ [
2080
+ "g2-",
2081
+ "h1"
2082
+ ],
2083
+ [
2084
+ "a212",
2085
+ "h-1x"
2086
+ ],
2087
+ [
2088
+ "j",
2089
+ "4-"
2090
+ ],
2091
+ [
2092
+ "l",
2093
+ "4-"
2094
+ ],
2095
+ [
2096
+ "g3-",
2097
+ "h1"
2098
+ ],
2099
+ [
2100
+ "6",
2101
+ "*OC(CCCCCC$4)"
2102
+ ],
2103
+ [
2104
+ "a2122A-1",
2105
+ "x"
2106
+ ],
2107
+ [
2108
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C",
2109
+ "?|"
2110
+ ],
2111
+ [
2112
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|",
2113
+ "D"
2114
+ ],
2115
+ [
2116
+ "e",
2117
+ "2"
2118
+ ],
2119
+ [
2120
+ "O",
2121
+ "/"
2122
+ ],
2123
+ [
2124
+ "i2-",
2125
+ "j1"
2126
+ ],
2127
+ [
2128
+ "i",
2129
+ "3-"
2130
+ ],
2131
+ [
2132
+ "a5",
2133
+ "-b1"
2134
+ ],
2135
+ [
2136
+ "e4-",
2137
+ "g1"
2138
+ ],
2139
+ [
2140
+ "a?|b?|c?|d?|e",
2141
+ "?}-{"
2142
+ ],
2143
+ [
2144
+ "a?|b?|c?|d?|e?}-{",
2145
+ "a?|b?|c?|d?|e"
2146
+ ],
2147
+ [
2148
+ "a?|b?|c?|d?|e?}-{a?|b?|c?|d?|e",
2149
+ "?"
2150
+ ],
2151
+ [
2152
+ "f",
2153
+ "6-"
2154
+ ],
2155
+ [
2156
+ "/3",
2157
+ "C"
2158
+ ],
2159
+ [
2160
+ "s",
2161
+ "1"
2162
+ ],
2163
+ [
2164
+ "j4-",
2165
+ "k1"
2166
+ ],
2167
+ [
2168
+ "xh-1",
2169
+ "u"
2170
+ ],
2171
+ [
2172
+ "h2",
2173
+ "112h"
2174
+ ],
2175
+ [
2176
+ "h",
2177
+ "3-"
2178
+ ],
2179
+ [
2180
+ "c6-",
2181
+ "h1"
2182
+ ],
2183
+ [
2184
+ "k",
2185
+ "2-"
2186
+ ],
2187
+ [
2188
+ "3",
2189
+ "*OSO/3=O/3=O"
2190
+ ],
2191
+ [
2192
+ "l4-",
2193
+ "m1"
2194
+ ],
2195
+ [
2196
+ "j",
2197
+ "2"
2198
+ ],
2199
+ [
2200
+ "2-",
2201
+ "5"
2202
+ ],
2203
+ [
2204
+ "a212",
2205
+ "1"
2206
+ ],
2207
+ [
2208
+ "d1",
2209
+ "122h"
2210
+ ],
2211
+ [
2212
+ "a4-",
2213
+ "c1"
2214
+ ],
2215
+ [
2216
+ "AU",
2217
+ "d1122h"
2218
+ ],
2219
+ [
2220
+ "n",
2221
+ "?-"
2222
+ ],
2223
+ [
2224
+ "8",
2225
+ "-"
2226
+ ],
2227
+ [
2228
+ "*OP^XO",
2229
+ "CCN"
2230
+ ],
2231
+ [
2232
+ "2",
2233
+ "*OC"
2234
+ ],
2235
+ [
2236
+ "a?|b?|c?|d",
2237
+ "?}-{"
2238
+ ],
2239
+ [
2240
+ "a?|b?|c?|d?}-{",
2241
+ "a?|b?|c?|d"
2242
+ ],
2243
+ [
2244
+ "a?|b?|c?|d?}-{a?|b?|c?|d",
2245
+ "?"
2246
+ ],
2247
+ [
2248
+ "a",
2249
+ "xx"
2250
+ ],
2251
+ [
2252
+ "7",
2253
+ "-"
2254
+ ],
2255
+ [
2256
+ "uxxxx",
2257
+ "xh"
2258
+ ],
2259
+ [
2260
+ "f",
2261
+ "2-"
2262
+ ],
2263
+ [
2264
+ "c6-",
2265
+ "g1"
2266
+ ],
2267
+ [
2268
+ "j",
2269
+ "2-"
2270
+ ],
2271
+ [
2272
+ "2",
2273
+ "*OC(CCCCCC$4)/3=O"
2274
+ ],
2275
+ [
2276
+ "f",
2277
+ "2"
2278
+ ],
2279
+ [
2280
+ "k",
2281
+ "4-"
2282
+ ],
2283
+ [
2284
+ "m?-",
2285
+ "n1"
2286
+ ],
2287
+ [
2288
+ "4",
2289
+ "*OSO/3=O/3=O"
2290
+ ],
2291
+ [
2292
+ "f2-",
2293
+ "g1"
2294
+ ],
2295
+ [
2296
+ "*O",
2297
+ "S"
2298
+ ],
2299
+ [
2300
+ "h3-",
2301
+ "i1"
2302
+ ],
2303
+ [
2304
+ "22",
2305
+ "2"
2306
+ ],
2307
+ [
2308
+ "k2-",
2309
+ "l1"
2310
+ ],
2311
+ [
2312
+ "1221",
2313
+ "h"
2314
+ ],
2315
+ [
2316
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D",
2317
+ "?|"
2318
+ ],
2319
+ [
2320
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|",
2321
+ "E"
2322
+ ],
2323
+ [
2324
+ "axxxx",
2325
+ "m-1x"
2326
+ ],
2327
+ [
2328
+ "i",
2329
+ "2"
2330
+ ],
2331
+ [
2332
+ "u1",
2333
+ "1221h"
2334
+ ],
2335
+ [
2336
+ "4",
2337
+ "*OPO/3O/3=O"
2338
+ ],
2339
+ [
2340
+ "3",
2341
+ "34"
2342
+ ],
2343
+ [
2344
+ "b6-",
2345
+ "c1"
2346
+ ],
2347
+ [
2348
+ "c6-",
2349
+ "i1"
2350
+ ],
2351
+ [
2352
+ "d4-",
2353
+ "h1"
2354
+ ],
2355
+ [
2356
+ "d?-",
2357
+ "h1"
2358
+ ],
2359
+ [
2360
+ "o",
2361
+ "?-"
2362
+ ],
2363
+ [
2364
+ "k",
2365
+ "2"
2366
+ ],
2367
+ [
2368
+ "6",
2369
+ "*OC"
2370
+ ],
2371
+ [
2372
+ "a1122",
2373
+ "2"
2374
+ ],
2375
+ [
2376
+ "c?-",
2377
+ "i1"
2378
+ ],
2379
+ [
2380
+ "a",
2381
+ "122h-1"
2382
+ ],
2383
+ [
2384
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E",
2385
+ "?|"
2386
+ ],
2387
+ [
2388
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|",
2389
+ "F"
2390
+ ],
2391
+ [
2392
+ "c?-",
2393
+ "h1"
2394
+ ],
2395
+ [
2396
+ "3",
2397
+ "*OC(CCCCCC$4)/3=O"
2398
+ ],
2399
+ [
2400
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F",
2401
+ "?|"
2402
+ ],
2403
+ [
2404
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|",
2405
+ "G"
2406
+ ],
2407
+ [
2408
+ "i3-",
2409
+ "j1"
2410
+ ],
2411
+ [
2412
+ "4",
2413
+ "*OC"
2414
+ ],
2415
+ [
2416
+ "h",
2417
+ "a"
2418
+ ],
2419
+ [
2420
+ "j2-",
2421
+ "k1"
2422
+ ],
2423
+ [
2424
+ "j",
2425
+ "3-"
2426
+ ],
2427
+ [
2428
+ "a",
2429
+ "4"
2430
+ ],
2431
+ [
2432
+ "6",
2433
+ "|"
2434
+ ],
2435
+ [
2436
+ "*OP^XOCCN",
2437
+ "/3O/3=O"
2438
+ ],
2439
+ [
2440
+ "C/",
2441
+ "4"
2442
+ ],
2443
+ [
2444
+ "a2112",
2445
+ "A-1a"
2446
+ ],
2447
+ [
2448
+ "m",
2449
+ "4-"
2450
+ ],
2451
+ [
2452
+ "n",
2453
+ "2"
2454
+ ],
2455
+ [
2456
+ "c6-",
2457
+ "k1"
2458
+ ],
2459
+ [
2460
+ "k4-",
2461
+ "l1"
2462
+ ],
2463
+ [
2464
+ "a",
2465
+ "d"
2466
+ ],
2467
+ [
2468
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x",
2469
+ "?}-{"
2470
+ ],
2471
+ [
2472
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?}-{",
2473
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x"
2474
+ ],
2475
+ [
2476
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?}-{a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x",
2477
+ "?"
2478
+ ],
2479
+ [
2480
+ "t",
2481
+ "1"
2482
+ ],
2483
+ [
2484
+ "a2",
2485
+ "1"
2486
+ ],
2487
+ [
2488
+ "4",
2489
+ "*N"
2490
+ ],
2491
+ [
2492
+ "u2",
2493
+ "211"
2494
+ ],
2495
+ [
2496
+ "a?|b?|c",
2497
+ "?}-{"
2498
+ ],
2499
+ [
2500
+ "a?|b?|c?}-{",
2501
+ "a?|b?|c"
2502
+ ],
2503
+ [
2504
+ "a?|b?|c?}-{a?|b?|c",
2505
+ "?"
2506
+ ],
2507
+ [
2508
+ "a",
2509
+ "12"
2510
+ ],
2511
+ [
2512
+ "CCCC",
2513
+ "CCCC"
2514
+ ],
2515
+ [
2516
+ "g",
2517
+ "6-"
2518
+ ],
2519
+ [
2520
+ "f3-",
2521
+ "g2"
2522
+ ],
2523
+ [
2524
+ "u2211",
2525
+ "m"
2526
+ ],
2527
+ [
2528
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G",
2529
+ "?|"
2530
+ ],
2531
+ [
2532
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|",
2533
+ "H"
2534
+ ],
2535
+ [
2536
+ "5",
2537
+ "-"
2538
+ ],
2539
+ [
2540
+ "ha",
2541
+ "122h-2"
2542
+ ],
2543
+ [
2544
+ "e",
2545
+ "6-"
2546
+ ],
2547
+ [
2548
+ "h",
2549
+ "6-"
2550
+ ],
2551
+ [
2552
+ "3|",
2553
+ "g"
2554
+ ],
2555
+ [
2556
+ "c6-",
2557
+ "d1"
2558
+ ],
2559
+ [
2560
+ "4",
2561
+ "*OC(CCCCCC$4)/3=O"
2562
+ ],
2563
+ [
2564
+ "3",
2565
+ "*NCC/3=O"
2566
+ ],
2567
+ [
2568
+ "2-",
2569
+ "?"
2570
+ ],
2571
+ [
2572
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|H",
2573
+ "?|"
2574
+ ],
2575
+ [
2576
+ "a?|b?|c?|d?|e?|f?|g?|h?|i?|j?|k?|l?|m?|n?|o?|p?|q?|r?|s?|t?|u?|v?|w?|x?|y?|z?|A?|B?|C?|D?|E?|F?|G?|H?|",
2577
+ "I"
2578
+ ],
2579
+ [
2580
+ "?}",
2581
+ "*OC"
2582
+ ],
2583
+ [
2584
+ "C",
2585
+ "/3=O"
2586
+ ],
2587
+ [
2588
+ "i",
2589
+ "6-"
2590
+ ],
2591
+ [
2592
+ "c?-",
2593
+ "k1"
2594
+ ],
2595
+ [
2596
+ "c?-",
2597
+ "g1"
2598
+ ],
2599
+ [
2600
+ "l",
2601
+ "2-"
2602
+ ],
2603
+ [
2604
+ "a2",
2605
+ "2"
2606
+ ]
2607
+ ],
2608
+ "metadata": {
2609
+ "vocab_size": 590,
2610
+ "num_merges": 500
2611
+ }
2612
+ }
releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_authority.json ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifacts": {
3
+ "development_expansion_manifest": {
4
+ "bytes": 2467731,
5
+ "name": "review_ms_acquisition_top30_development_expansion.tsv.gz",
6
+ "sha256": "f337cc115011dbdb2040f01142de00f42a28a68218a7441f721cd76b74a18753"
7
+ },
8
+ "parent_manifest": {
9
+ "bytes": 78127,
10
+ "name": "review_ms_acquisition_top30_parents.tsv.gz",
11
+ "sha256": "10dc3fa7622e2a7e448d775f4082fdc84aa0622f9a7a74bcfaeed491c0eb7d39"
12
+ },
13
+ "row_manifest": {
14
+ "bytes": 65148547,
15
+ "name": "review_ms_acquisition_top30_rows.tsv.gz",
16
+ "sha256": "cf1dd59bd0b11525e643b2b44e60ff3bebc374e83d671365f4d5ec1f1e2d3a49"
17
+ },
18
+ "target_ids": {
19
+ "bytes": 26045132,
20
+ "name": "review_ms_acquisition_top30_target_ids_u16le.bin",
21
+ "sha256": "1d545f3d1ed77111f04298c632885b0809879ccc1dc4103c4ffc6f947a4bd472"
22
+ },
23
+ "target_offsets": {
24
+ "bytes": 1800456,
25
+ "name": "review_ms_acquisition_top30_target_offsets_u64le.bin",
26
+ "sha256": "42eb4beae41b9ec821e1fcc0cf18d8f36041d6e2075605012c3f9070fca750cd"
27
+ }
28
+ },
29
+ "counts": {
30
+ "active_acquisition_count": 225056,
31
+ "active_parent_count": 3035,
32
+ "active_target_token_count": 13022566,
33
+ "development_acquisition_count": 49573,
34
+ "development_parent_count": 580,
35
+ "train_acquisition_count": 175483,
36
+ "train_parent_count": 2455
37
+ },
38
+ "development_expansion_contract": {
39
+ "eligible_acquisition_count": 49573,
40
+ "eligible_parent_count": 580,
41
+ "every_eligible_ms_acquisition_required_for_each_output": true,
42
+ "ms_factorized_weight": "1/580 * 1/N_acquisition(parent)",
43
+ "ms_lane_acquisition_expansion_outputs": [
44
+ "sequence",
45
+ "ms"
46
+ ],
47
+ "ms_local_reduction": "family mean within acquisition, acquisition mean within MS parent, then equal over 580 MS parents",
48
+ "replay_sha256": "e50dc962240b622d0cedfc42db0f1653d639bbd4a4f956ed611817222b84d453",
49
+ "schema": "bertose_a5_ms_development_expansion_v2",
50
+ "sequence_global_reduction": "equal parents over full eligible sequence development roster; composite-owned",
51
+ "sequence_ms_parent_reduction": "mean_acquisition within MS parent",
52
+ "sequence_no_ms_parent_view": "one no-MS view",
53
+ "sequence_source": "post-fusion shared_hidden",
54
+ "sequence_within_ms_parent_weight": "1/N_acquisition(parent); global parent denominator is composite-owned",
55
+ "static_development_nesting": "state within parent, then equal parents; separate from acquisition expansion",
56
+ "topology_code_evidence": "review_a5_composite_model_v2_r3.py SHA-256 ee62f065fb347d3967228bf2a17caad9f2e6c0bbb1c387ca2bebefec9713f90b: seq_logits uses shared_hidden at line 2037; topology_v2 receives seq_hidden_base at lines 2041-2046",
57
+ "topology_development_nesting": "pre-fusion seq_hidden_base; once per eligible topology parent on the full topology roster; never acquisition-expanded or restricted to MS parents",
58
+ "topology_roster_ownership": "composite must replay every eligible topology development parent exactly once; MS package supplies no topology roster filter"
59
+ },
60
+ "implementation": {
61
+ "bytes": 43711,
62
+ "name": "review_a5_ms_acquisition_top30_runtime_v3.py",
63
+ "sha256": "68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6"
64
+ },
65
+ "inputs": {
66
+ "count_child_authority": {
67
+ "bytes": 15090,
68
+ "name": "review_a5_ms_top30_count_child_authority.json",
69
+ "sha256": "70a38dffb73aa9a482bc2227dd874862896453b7a45e3441f2aa97ea9ac9fab9"
70
+ },
71
+ "count_child_evidence": {
72
+ "bytes": 820,
73
+ "name": "review_a5_ms_top30_count_delta_evidence.tsv.gz",
74
+ "sha256": "dbdc6bbe0e8c4e17ab30908aaa510a09c522a8c4f8d154efac21b60a1453b2a8"
75
+ },
76
+ "count_child_independent_certificate": {
77
+ "bytes": 2078,
78
+ "name": "review_a5_ms_top30_count_child_authority_independent_audit.json",
79
+ "sha256": "c708f81a39706a9af86bbf5fe2d859a53de19f565b9f4eae2ff12f62a066513e"
80
+ },
81
+ "count_child_receipt": {
82
+ "bytes": 9916,
83
+ "name": "review_a5_ms_top30_count_child_receipt.json",
84
+ "sha256": "1bd22e3d7ac9312db567fdd9357b30e853ada4e7a5e2d23478f864422c3c7b15"
85
+ },
86
+ "design_independent_certificate": {
87
+ "bytes": 5889,
88
+ "name": "review_a5_ms_acquisition_top30_independent_audit_r2.json",
89
+ "sha256": "158450a27ae9bb642c010d5a2275adacef491bcf14a0ba227beb96cc2eaaaa5f"
90
+ },
91
+ "design_manifest": {
92
+ "bytes": 105029603,
93
+ "name": "review_a5_ms_acquisition_top30_manifest.tsv.gz",
94
+ "sha256": "bfe12eb3100552039dd18b217c899e15c75174b314b624deb72e320edc873e97"
95
+ },
96
+ "design_receipt": {
97
+ "bytes": 6517,
98
+ "name": "review_a5_ms_acquisition_top30_receipt.json",
99
+ "sha256": "86913823e6c5c14924735575dffac37ec4264ec130b0eaafda05226f4a87880e"
100
+ },
101
+ "design_summary": {
102
+ "bytes": 21986,
103
+ "name": "review_a5_ms_acquisition_top30_summary.json",
104
+ "sha256": "2fb09f48f204ed8fbf5edb8b109eba880b7de60ff40ce54f3d30cb41294a3211"
105
+ },
106
+ "design_superseded_metadata_certificate": {
107
+ "bytes": 5470,
108
+ "name": "review_a5_ms_acquisition_top30_independent_audit.json",
109
+ "sha256": "dff09234ed4b6550bf5c9f17e32e2e4f356aaa18efb2a7a21e70ac9f568e6338"
110
+ },
111
+ "design_train_only_null": {
112
+ "bytes": 8878,
113
+ "name": "review_a5_ms_acquisition_top30_train_only_null.json",
114
+ "sha256": "2f68b42c9201011a078ea806199fdf151473b4b6ff38a9273f1a53a63fed4493"
115
+ },
116
+ "raw_clean_double_build_certificate": {
117
+ "bytes": 4617,
118
+ "name": "review_a5_ms_raw_to_top30_clean_double_build_r3_certificate.json",
119
+ "sha256": "f92f2c0c56acddbdf818d015cd8f7d9d99c0b98535c97624116fe92f7b9cb28e"
120
+ },
121
+ "raw_end_to_end_receipt": {
122
+ "bytes": 11189,
123
+ "name": "review_a5_ms_raw_to_top30_end_to_end_receipt.json",
124
+ "sha256": "98c4bbf1088e28a0a56593fc66796ffba75235755c72adfe6e02f59c0f4a48e7"
125
+ },
126
+ "raw_publication_certificate": {
127
+ "bytes": 7603,
128
+ "name": "review_a5_ms_raw_to_top30_published_r3_certificate.json",
129
+ "sha256": "d9f5845940f57af88d212d395bf90bcd4e8d5728d88060c460ad0784390a1bd5"
130
+ },
131
+ "raw_transformation_authority": {
132
+ "bytes": 732729924,
133
+ "name": "review_a5_ms_primary_runtime_transformation_authority.tsv.gz",
134
+ "sha256": "d3c16f4bdbe8b35440855d1c83287c5bda12d512ec3b89ba0ca312829dddb7f4"
135
+ },
136
+ "raw_transformation_authority_receipt": {
137
+ "bytes": 1367,
138
+ "name": "review_a5_ms_primary_runtime_transformation_authority_receipt.json",
139
+ "sha256": "e82ceec1a94ec2f250d28e4ed37e818aee234eb132ece9fec03f43fabc9d6378"
140
+ }
141
+ },
142
+ "join_keys": {
143
+ "canonical_parent_identity": "canonical_wurcs",
144
+ "model_parent": "parent_glycorpus_id",
145
+ "physical_acquisition": "acquisition_identity_sha256",
146
+ "provenance_only": [
147
+ "source_record_identity",
148
+ "source_record_identity_sha256",
149
+ "source_raw_numeric_coordinate_sha256",
150
+ "ledger_positive_parser_projection_sha256",
151
+ "raw_coordinate_container_sha256",
152
+ "raw_container_locator",
153
+ "raw_transformation_row_surface_sha256"
154
+ ],
155
+ "source_row": "row_key"
156
+ },
157
+ "nova_jobs_submitted": false,
158
+ "null_ruler": {
159
+ "acquisition_count": 175483,
160
+ "atomic_record": "parent_glycorpus_id + acquisition_identity_sha256 + family + peak-pair label",
161
+ "development_rows_used_for_fit": 0,
162
+ "family_conditional_marginal_weight_formula": "1/N_parent * 1/N_acquisition(parent) * 1/N_peak_pair(acquisition); the common family factor 1/2 cancels after conditioning on family",
163
+ "family_mean_parent_risks": {
164
+ "intensity": 0.9538678814156866,
165
+ "mz": 5.058329202544485
166
+ },
167
+ "family_probabilities": {
168
+ "intensity": [
169
+ 0.5197177353428715,
170
+ 0.36756454029956603,
171
+ 0.1127177243575615
172
+ ],
173
+ "mz": [
174
+ 0.0006405498650642573,
175
+ 0.0006021252988097924,
176
+ 0.0009749945211706317,
177
+ 0.0012252351748472381,
178
+ 0.0014919905269334868,
179
+ 0.0015897223541305183,
180
+ 0.0018750290482665633,
181
+ 0.0022325092284391297,
182
+ 0.0034832951393886616,
183
+ 0.0030005874190584293,
184
+ 0.0031870148335501544,
185
+ 0.002153589976904484,
186
+ 0.006199135195682838,
187
+ 0.0025635386139654104,
188
+ 0.005081629903518679,
189
+ 0.0021085111949207445,
190
+ 0.0049675443759019215,
191
+ 0.0026602408560569084,
192
+ 0.004910147907424021,
193
+ 0.0033596265935932593,
194
+ 0.007460028144741095,
195
+ 0.007342316510259218,
196
+ 0.009383699893628727,
197
+ 0.0035924973862568356,
198
+ 0.007300624112203328,
199
+ 0.003492257441496713,
200
+ 0.007360542338169046,
201
+ 0.007382295095945736,
202
+ 0.014832571097070722,
203
+ 0.004265176358855978,
204
+ 0.010263477266301528,
205
+ 0.0037736525044596487,
206
+ 0.010357919182143932,
207
+ 0.003968945641000579,
208
+ 0.009871247361505384,
209
+ 0.004710725207575534,
210
+ 0.006315269663161841,
211
+ 0.005157638724597456,
212
+ 0.007760572611057847,
213
+ 0.007187969630144797,
214
+ 0.0064124494773172445,
215
+ 0.005628045100877154,
216
+ 0.011602968806865377,
217
+ 0.010493624967610124,
218
+ 0.014841790030570219,
219
+ 0.006582790521429821,
220
+ 0.00750222517517576,
221
+ 0.010939826561705971,
222
+ 0.011289749262542014,
223
+ 0.009076003093031543,
224
+ 0.01059155518012925,
225
+ 0.006798531213847085,
226
+ 0.006580023268606618,
227
+ 0.006625866180657642,
228
+ 0.00809072231972558,
229
+ 0.007049768301782209,
230
+ 0.007619380290109273,
231
+ 0.013172914049614742,
232
+ 0.007973038587594817,
233
+ 0.013669095027737806,
234
+ 0.008854889943844537,
235
+ 0.010764243344026123,
236
+ 0.009261971480032959,
237
+ 0.011407087221082244,
238
+ 0.008275493172872357,
239
+ 0.014411502711840623,
240
+ 0.008746366070571238,
241
+ 0.008573085716761386,
242
+ 0.005291706775934269,
243
+ 0.009011519952519228,
244
+ 0.006988717495289838,
245
+ 0.009253048598594757,
246
+ 0.007821243894415412,
247
+ 0.011086758968541742,
248
+ 0.007673134347475936,
249
+ 0.011097951070032187,
250
+ 0.006218925559457332,
251
+ 0.009936426882326424,
252
+ 0.007959877939685212,
253
+ 0.011968876654308121,
254
+ 0.008339926878809802,
255
+ 0.012695391772888196,
256
+ 0.005731582712877305,
257
+ 0.009291721965872388,
258
+ 0.005613550406088611,
259
+ 0.009175948267808341,
260
+ 0.005978035242077504,
261
+ 0.008944619689293509,
262
+ 0.007367184276934726,
263
+ 0.007941269958145787,
264
+ 0.007299264060980651,
265
+ 0.007239133563442773,
266
+ 0.006612679564797478,
267
+ 0.006283524080156452,
268
+ 0.008149139416552757,
269
+ 0.009507867376475363,
270
+ 0.007738268726780698,
271
+ 0.007931483865568491,
272
+ 0.005231235554487748,
273
+ 0.00665149206592971,
274
+ 0.00650735399622342,
275
+ 0.007954727132383264,
276
+ 0.005030903411493357,
277
+ 0.007363008031486631,
278
+ 0.004293715643194716,
279
+ 0.005116324444397951,
280
+ 0.005079498064988057,
281
+ 0.005328559680790833,
282
+ 0.0047181521200511745,
283
+ 0.00452701797791838,
284
+ 0.005665956328012772,
285
+ 0.0056135415685971725,
286
+ 0.006477197736781981,
287
+ 0.0044094388131974156,
288
+ 0.004562203092508437,
289
+ 0.003927667497232976,
290
+ 0.00595400228271556,
291
+ 0.0044660876335546155,
292
+ 0.0043911564061610954,
293
+ 0.004415206577921035,
294
+ 0.003937588905054545,
295
+ 0.003719985017029448,
296
+ 0.003676435977917263,
297
+ 0.003814600894783378,
298
+ 0.003433020728342275,
299
+ 0.003935725556137375,
300
+ 0.00404266701069346,
301
+ 0.0028970529888585088,
302
+ 0.004150673826274117,
303
+ 0.0032579911376673146,
304
+ 0.004363882486274142,
305
+ 0.004270449886008418,
306
+ 0.0049071061870006915,
307
+ 0.003015248951052818,
308
+ 0.003807070235575648,
309
+ 0.002774851223097479,
310
+ 0.00292725217431102,
311
+ 0.0021288821303970945,
312
+ 0.0032297161628375686,
313
+ 0.0028509226016853015,
314
+ 0.0026799148798400474,
315
+ 0.001825988019983223,
316
+ 0.003292129599799677,
317
+ 0.001677051151594144,
318
+ 0.002439523654513002,
319
+ 0.00198363535229116,
320
+ 0.0037587177141866174,
321
+ 0.003490935731056946,
322
+ 0.002995263611964447,
323
+ 0.0018092325269436526,
324
+ 0.002247483981193846,
325
+ 0.0024445658007253178,
326
+ 0.002339234579099249,
327
+ 0.0017213923060684785,
328
+ 0.0026590153316273036,
329
+ 0.0013800374494157537,
330
+ 0.002284641032769585,
331
+ 0.0011368525865265914,
332
+ 0.0018214148728505491,
333
+ 0.0013022046110269289,
334
+ 0.0013966749830251343,
335
+ 0.0015469207178817875,
336
+ 0.0028377577479343882,
337
+ 0.002359877651502991,
338
+ 0.0020768239242850046,
339
+ 0.0016799064395334328,
340
+ 0.001560964204396998,
341
+ 0.002849456766570763,
342
+ 0.0018607669582667444,
343
+ 0.002124968758476105,
344
+ 0.0018335763283124742,
345
+ 0.001537877313231571,
346
+ 0.0011432679414856727,
347
+ 0.001173615019432312,
348
+ 0.0012124839173755315,
349
+ 0.001279967116160827,
350
+ 0.001498155553665475,
351
+ 0.001122604974469808,
352
+ 0.0014509640741489293,
353
+ 0.0012002230758373785,
354
+ 0.0011659033448112672,
355
+ 0.001313358414307407,
356
+ 0.001296550566618564,
357
+ 0.0020724105583319057,
358
+ 0.0015725440101973384,
359
+ 0.0021442389022742173,
360
+ 0.0014270560400526352,
361
+ 0.0013324118457630541,
362
+ 0.0009997932453970622,
363
+ 0.0011887167011727473,
364
+ 0.0007072343298746987,
365
+ 0.0013301494348818492,
366
+ 0.0006204627186076994,
367
+ 0.00039869722720875154,
368
+ 0.00043077402577967574,
369
+ 0.0004672031444357866,
370
+ 0.00039750241121630064,
371
+ 0.0005384150176558037,
372
+ 0.0008002933341730924,
373
+ 0.0008629090005762214,
374
+ 0.0006959207112273459,
375
+ 0.00040135183128836273,
376
+ 0.0004873041609817168,
377
+ 0.0005243254947311232,
378
+ 0.000664005589046579,
379
+ 0.0004979014294268761,
380
+ 0.0005286575931262811,
381
+ 0.00042244525643066514,
382
+ 0.00040993170463503857,
383
+ 0.00039515285548507293,
384
+ 0.00038376572004818074,
385
+ 0.00039552561334986583,
386
+ 0.00019519812609798947
387
+ ]
388
+ },
389
+ "family_target_counts": {
390
+ "intensity": 4770785,
391
+ "mz": 4770785
392
+ },
393
+ "finite_epoch_exposure_used_for_fit": false,
394
+ "fit_scope": "representation-clean pretraining_train acquisitions only",
395
+ "fixed_before_training": true,
396
+ "frozen_design_oracle_coefficients_preserved": true,
397
+ "independent_packed_recomputed_numeric_surface_sha256": "80cf1bc7092492cf486edbcff422e61c3ad77dde152b9376d79c7457f30697d1",
398
+ "independent_packed_recomputed_scale": 3.006098541980087,
399
+ "masking_estimator": "uniform without-replacement whole-peak-pair sample mean; both MZ and intensity labels are supervised for every selected pair",
400
+ "masking_estimator_unbiased_for_population_loss": true,
401
+ "maximum_absolute_difference_from_frozen_design_oracle": 4.440892098500626e-15,
402
+ "parent_count": 2455,
403
+ "population_atomic_weight_formula": "1/N_parent * 1/N_acquisition(parent) * 1/2 * 1/N_peak_pair(acquisition)",
404
+ "population_loss_formula": "mean_parent(mean_acquisition(0.5*(mean_peak_mz_nll + mean_peak_intensity_nll)))",
405
+ "pseudocount": 0.5,
406
+ "realized_schedule_epochs_used": [],
407
+ "recomputed_from_packed_primary_train_targets": true,
408
+ "scale": 3.006098541980086,
409
+ "support_kind": "all eligible pretraining_train acquisition peak-pair labels as a population expectation, not realized finite-epoch schedule exposure",
410
+ "target_token_count": 10243502,
411
+ "weighting": "equal parents, equal acquisitions within parent, equal family means within acquisition"
412
+ },
413
+ "official_submitted_artifacts_modified": false,
414
+ "raw_to_top30_authority_binding": {
415
+ "all_row_parent_role_source_acquisition_target_joins_exact": true,
416
+ "authority_streamed_no_follow_not_loaded_wholesale": true,
417
+ "packed_runtime_binding_sha256": "5fe30dfcb673f450c600af76c70a10b225d26ae35d5d3a8cc603303dd10aafdd",
418
+ "raw_authority_file_sha256": "d3c16f4bdbe8b35440855d1c83287c5bda12d512ec3b89ba0ca312829dddb7f4",
419
+ "raw_authority_whole_surface_sha256": "83fbdd24bb99332329da7b31c8412e74fef169f3c8faede9e4e09db04f9f1200",
420
+ "raw_publication_certificate_sha256": "d9f5845940f57af88d212d395bf90bcd4e8d5728d88060c460ad0784390a1bd5",
421
+ "row_count": 225056,
422
+ "source_raw_differs_from_positive_parser_projection_count": 351,
423
+ "unique_raw_transformation_row_surface_count": 225056
424
+ },
425
+ "remaining_release_boundary": "This package certifies the MS lane only. Integrated four-objective model, optimizer, static, sequence/topology, checkpoint, and cluster release gates remain external.",
426
+ "schedule": {
427
+ "arm_and_unrelated_modality_invariant": true,
428
+ "development_all_acquisitions": true,
429
+ "development_parent_equal_reduction": true,
430
+ "fixture_epochs": [
431
+ 0,
432
+ 1,
433
+ 7,
434
+ 17
435
+ ],
436
+ "mask_rng_key_includes_acquisition_identity": true,
437
+ "once_per_parent_local_cycle": true,
438
+ "one_acquisition_per_parent_epoch": true,
439
+ "parent_order_domain": "bertose-a5-ms-parent-order-v1",
440
+ "population_binding_sha256": "35a729fc66db5fdb440b70843bccaf557ca2674635523ceff9e440bf8d99ee91",
441
+ "resume_binding": [
442
+ "population_binding_sha256",
443
+ "schedule_schema",
444
+ "seed",
445
+ "epoch",
446
+ "global_parent_cursor",
447
+ "selected_prefix_sha256"
448
+ ],
449
+ "schema": "bertose_a5_ms_parent_cycle_hash_schedule_v1",
450
+ "seed": 43,
451
+ "selection_domain": "bertose-a5-ms-acquisition-view-selection-v1"
452
+ },
453
+ "schema": "bertose_a5_ms_acquisition_top30_runtime_authority_v3",
454
+ "scientific_launch_authorized": false,
455
+ "status": "PASS_RUNTIME_PACKAGE_PREPARATION_ONLY",
456
+ "supersession": {
457
+ "predecessor_preserved_without_overwrite": true,
458
+ "predecessor_release_status": "NO_GO_SUPERSEDED",
459
+ "predecessor_runtime": {
460
+ "bytes": 33000,
461
+ "name": "review_a5_ms_acquisition_top30_runtime_v2.py",
462
+ "sha256": "94359f20bf8040e7758d2b793d5c78a6b3045cd726143b785d7d8cbbeda6aa14"
463
+ },
464
+ "predecessor_schema": "bertose_a5_ms_acquisition_top30_runtime_v2",
465
+ "reason": "The predecessor accepted caller seed values for development corruption. The v3 successor exposes a separate development-mask API fixed to seed 0 and epoch 0.",
466
+ "successor_schema": "bertose_a5_ms_acquisition_top30_runtime_v3"
467
+ },
468
+ "target_contract": {
469
+ "development_mask_api": "mask_development_target(row_index)",
470
+ "development_mask_epoch": 0,
471
+ "development_mask_seed": 0,
472
+ "development_mask_seed_or_epoch_caller_control": false,
473
+ "dtype": "uint16 little-endian",
474
+ "envelope": "[CLS=0,GLYCAN_ID=3,PEAKS=5,(MZ=29..241,INTENSITY=26..28)+,SEP=1]",
475
+ "intensity_levels": {
476
+ "high": 28,
477
+ "low": 26,
478
+ "medium": 27
479
+ },
480
+ "mask_pair_count_formula": "max(1, floor(N_peak_pair * 3 / 20))",
481
+ "mask_pair_probability_denominator": 20,
482
+ "mask_pair_probability_numerator": 3,
483
+ "mask_probability_runtime_configurable": false,
484
+ "mask_selection": "exact rational 3/20 of physical peak pairs via floor with minimum one pair; both the MZ and intensity token of each selected pair are supervised",
485
+ "maximum_length": 64,
486
+ "padding_id_at_collation_only": 2,
487
+ "peak_cap": 30,
488
+ "per_acquisition_equal_family_loss": "0.5*(mean selected MZ token NLL + mean selected intensity token NLL)",
489
+ "physical_condition_used": false,
490
+ "predecessor_vocabulary_metadata_non_authoritative": {
491
+ "parent_top_k": 50,
492
+ "reason": "These retained vocabulary JSON fields describe predecessor construction metadata, not the R3 target grammar. R3 TOP30 selection and target bytes are bound exclusively by the raw-to-TOP30 authority.",
493
+ "spectrum_top_k": 50,
494
+ "token_schema": "parent_consensus_pair_grammar_v1"
495
+ },
496
+ "replacement_routing": {
497
+ "binary_float_thresholds_used": false,
498
+ "half_revealed_selected_pair_possible": false,
499
+ "mask": "exact cumulative interval [0,4/5)",
500
+ "random_same_family_excluding_original": "exact cumulative interval [4/5,9/10)",
501
+ "routing_unit": "one physical m/z+intensity peak pair",
502
+ "shared_route_draw_for_both_pair_tokens": true,
503
+ "unchanged": "exact cumulative interval [9/10,1)"
504
+ },
505
+ "retention_time_used": false,
506
+ "selected_mz_intensity_counts_always_equal": true,
507
+ "training_mask_api": "mask_target(row_index, epoch, seed)",
508
+ "vocabulary_mapping_authority": "fixed IDs only: LOW/MED/HIGH=26/27/28 and MZ_8..MZ_220=29..241"
509
+ },
510
+ "training_authorized": false
511
+ }
releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_independent_certificate.json ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "authority": {
3
+ "bytes": 20541,
4
+ "name": "review_a5_ms_acquisition_top30_runtime_authority.json",
5
+ "sha256": "48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4"
6
+ },
7
+ "certifier_implementation": {
8
+ "bytes": 49683,
9
+ "name": "certify_review_a5_ms_acquisition_top30_runtime_package_r3_20260721.py",
10
+ "sha256": "a1e2989e36876840ee10d555b1ab69e9876fb0483aec839bb5a44187e7a1122a"
11
+ },
12
+ "checks": {
13
+ "all_225056_active_acquisition_targets_reopened_and_rehashed": true,
14
+ "all_225056_rows_rejoined_to_raw_r3_parent_source_acquisition_target_and_provenance": true,
15
+ "all_four_ms_active_factorial_cells_mask_identical": true,
16
+ "all_shortest_acquisitions_pass_pair_mask_contract": true,
17
+ "development_all_acquisitions_parent_equal_reduction": true,
18
+ "development_and_finite_epoch_rows_absent_from_null_fit": true,
19
+ "development_mask_fixed_seed0_epoch0_and_caller_control_absent": true,
20
+ "development_mask_repeat_and_factorial_cell_identity_pass": true,
21
+ "development_mask_seed_epoch_tamper_rejected": true,
22
+ "development_ms_and_sequence_within_parent_weights_and_replay_hash_independently_verified": true,
23
+ "development_sequence_and_ms_expand_every_eligible_ms_acquisition": true,
24
+ "epoch16_and_epoch100_acquisition_and_target_exposure_exact": true,
25
+ "every_valid_target_length_has_nonempty_equal_mz_intensity_pair_supervision": true,
26
+ "exact_13022566_target_tokens": true,
27
+ "exact_3_over_20_pair_count_boundaries_n20_n40_pass": true,
28
+ "exact_80_10_10_replacement_route_boundaries_pass": true,
29
+ "exact_resume_cursor_and_prefix_binding": true,
30
+ "exact_top30_train2455_175483_and_development580_49573_counts": true,
31
+ "factorial_arm_identity_absent_from_schedule_and_mask_apis": true,
32
+ "five_raw_provenance_hash_fields_bound_and_tamper_evident": true,
33
+ "four_epoch_schedules_independently_recomputed": true,
34
+ "mask_rng_includes_physical_acquisition_identity": true,
35
+ "no_selected_peak_pair_half_revealed_by_split_routing": true,
36
+ "null_matches_parent_then_acquisition_expected_sampling_estimand": true,
37
+ "null_support_is_full_train_population_not_finite_epoch_exposure": true,
38
+ "one_shared_replacement_route_per_selected_physical_pair": true,
39
+ "predecessor_runtime_hash_preserved_and_supersession_bound": true,
40
+ "predecessor_top50_vocabulary_metadata_explicitly_non_authoritative": true,
41
+ "random_replacement_same_family_and_excludes_original": true,
42
+ "shortest_acquisition_1024_seed_hash_sweep_passes": true,
43
+ "source_row_and_physical_acquisition_identifiers_unique": true,
44
+ "static_state_then_parent_nesting_kept_separate": true,
45
+ "tampered_resume_cursor_rejected": true,
46
+ "topology_pre_fusion_not_acquisition_expanded_or_ms_restricted": true,
47
+ "train_only_null_independently_recomputed_from_packed_targets": true,
48
+ "training_and_scientific_launch_false": true,
49
+ "whole_pair_mask_estimator_declared_unbiased_for_null_estimand": true,
50
+ "within_parent_sampling_is_source_and_condition_blind": true,
51
+ "world_size_1_2_3_4_8_disjoint_complete": true,
52
+ "zero_peak_pair_probability_rejected_for_active_ms": true,
53
+ "zero_primary_train_development_target_hash_intersection": true
54
+ },
55
+ "counts": {
56
+ "role_parents": {
57
+ "pretraining_development": 580,
58
+ "pretraining_train": 2455
59
+ },
60
+ "role_rows": {
61
+ "pretraining_development": 49573,
62
+ "pretraining_train": 175483
63
+ },
64
+ "target_tokens": 13022566
65
+ },
66
+ "development_expansion": {
67
+ "eligible_acquisition_count": 49573,
68
+ "eligible_parent_count": 580,
69
+ "manifest": {
70
+ "bytes": 2467731,
71
+ "name": "review_ms_acquisition_top30_development_expansion.tsv.gz",
72
+ "sha256": "f337cc115011dbdb2040f01142de00f42a28a68218a7441f721cd76b74a18753"
73
+ },
74
+ "ms_factorized_weight": "1/580 * 1/N_acquisition(parent)",
75
+ "ms_lane_acquisition_expansion_outputs": [
76
+ "sequence",
77
+ "ms"
78
+ ],
79
+ "replay_sha256": "e50dc962240b622d0cedfc42db0f1653d639bbd4a4f956ed611817222b84d453",
80
+ "sequence_within_ms_parent_weight": "1/N_acquisition(parent); global parent denominator is composite-owned",
81
+ "static_development_nesting": "state within parent, then equal parents; separate from acquisition expansion",
82
+ "topology_development_nesting": "pre-fusion seq_hidden_base; once per eligible topology parent on the full topology roster; never acquisition-expanded or restricted to MS parents"
83
+ },
84
+ "development_fixed_mask_sha256": "6e63678cd8ddb4529370398e64e4b8819acefc90481f6a74ffb8e487486869ae",
85
+ "development_parent_equal_fixture": 8.197980484692945,
86
+ "finite_window_exposure": {
87
+ "epoch_windows": {
88
+ "100": {
89
+ "absolute_relative_null_difference": 6.160079253677961e-06,
90
+ "declared_full_train_expected_null_scale": 3.006098541980086,
91
+ "epochs": 100,
92
+ "presentation_condition_signature_present_counts": {
93
+ "absent": 234200,
94
+ "present": 11300
95
+ },
96
+ "presentation_count": 245500,
97
+ "presentation_source_counts": {
98
+ "bounded": 87400,
99
+ "candy_cc2_test": 11300,
100
+ "gap_rescue": 900,
101
+ "legacy": 145900
102
+ },
103
+ "realized_minus_expected_null": -1.8517805262963094e-05,
104
+ "realized_schedule_null_risk": 3.006080024174823,
105
+ "unique_acquisition_condition_signature_present_counts": {
106
+ "absent": 55933,
107
+ "present": 4959
108
+ },
109
+ "unique_acquisition_count": 60892,
110
+ "unique_acquisition_coverage_fraction": 0.34699657516682525,
111
+ "unique_acquisition_source_counts": {
112
+ "bounded": 1225,
113
+ "candy_cc2_test": 4959,
114
+ "gap_rescue": 10,
115
+ "legacy": 54698
116
+ },
117
+ "unique_parent_target_pair_count": 60568,
118
+ "unique_target_hash_count": 60361,
119
+ "unique_target_hash_coverage_fraction": 0.3470538856052069
120
+ },
121
+ "16": {
122
+ "absolute_relative_null_difference": 9.750531018454421e-05,
123
+ "declared_full_train_expected_null_scale": 3.006098541980086,
124
+ "epochs": 16,
125
+ "presentation_condition_signature_present_counts": {
126
+ "absent": 37472,
127
+ "present": 1808
128
+ },
129
+ "presentation_count": 39280,
130
+ "presentation_source_counts": {
131
+ "bounded": 13984,
132
+ "candy_cc2_test": 1808,
133
+ "gap_rescue": 144,
134
+ "legacy": 23344
135
+ },
136
+ "realized_minus_expected_null": 0.0002931105707810744,
137
+ "realized_schedule_null_risk": 3.006391652550867,
138
+ "unique_acquisition_condition_signature_present_counts": {
139
+ "absent": 17362,
140
+ "present": 1286
141
+ },
142
+ "unique_acquisition_count": 18648,
143
+ "unique_acquisition_coverage_fraction": 0.1062667038972436,
144
+ "unique_acquisition_source_counts": {
145
+ "bounded": 1225,
146
+ "candy_cc2_test": 1286,
147
+ "gap_rescue": 10,
148
+ "legacy": 16127
149
+ },
150
+ "unique_parent_target_pair_count": 18598,
151
+ "unique_target_hash_count": 18490,
152
+ "unique_target_hash_coverage_fraction": 0.10631080241944757
153
+ }
154
+ },
155
+ "finite_window_interpretation": "The deterministic seed-43 realization is label-blind within parent. Source-level presentation frequencies reflect equal parent weighting, not the global acquisition histogram.",
156
+ "finite_window_null_alignment_rule": "Absolute realized-minus-population null difference divided by the fixed population null scale must be below 1e-4; exact finite-window equality is not expected.",
157
+ "global_acquisition_proportional_sampling": false,
158
+ "global_acquisition_to_target_redundancy_count": 1559,
159
+ "global_parent_target_pair_count": 174489,
160
+ "global_unique_target_hash_count": 173924,
161
+ "maximum_acquisitions_per_parent": 5116,
162
+ "parents_with_more_than_100_acquisitions": 315,
163
+ "parents_with_multiple_source_families": 0,
164
+ "sampling_estimand": "equal parents; within each parent, acquisition-uniform SHA-256 permutation without replacement per local cycle",
165
+ "source_or_condition_used_by_ranking": false,
166
+ "train_acquisition_count": 175483,
167
+ "within_parent_acquisition_to_target_redundancy_count": 994
168
+ },
169
+ "ms_active_cell_mask_sha256": "2a8f9e16e723407c6762ce2f36aa5bdbe326efa32575268aba2d9b738b623b37",
170
+ "nova_jobs_submitted": false,
171
+ "null_and_reducer_alignment": {
172
+ "aligned": true,
173
+ "finite_window_relative_difference_tolerance_exclusive": 0.0001,
174
+ "independent_packed_train_only_replay": {
175
+ "acquisition_count": 175483,
176
+ "development_rows_used": 0,
177
+ "family_mean_parent_risks": {
178
+ "intensity": 0.9538678814156855,
179
+ "mz": 5.058329202544489
180
+ },
181
+ "family_probabilities": {
182
+ "intensity": [
183
+ 0.5197177353428725,
184
+ 0.36756454029956576,
185
+ 0.11271772435756178
186
+ ],
187
+ "mz": [
188
+ 0.000640549865064257,
189
+ 0.0006021252988097924,
190
+ 0.0009749945211706308,
191
+ 0.001225235174847238,
192
+ 0.001491990526933486,
193
+ 0.0015897223541305192,
194
+ 0.0018750290482665644,
195
+ 0.0022325092284391328,
196
+ 0.003483295139388665,
197
+ 0.003000587419058433,
198
+ 0.0031870148335501557,
199
+ 0.002153589976904486,
200
+ 0.006199135195682839,
201
+ 0.002563538613965407,
202
+ 0.005081629903518681,
203
+ 0.002108511194920743,
204
+ 0.004967544375901917,
205
+ 0.0026602408560569084,
206
+ 0.004910147907424022,
207
+ 0.0033596265935932584,
208
+ 0.007460028144741085,
209
+ 0.0073423165102591975,
210
+ 0.009383699893628675,
211
+ 0.003592497386256839,
212
+ 0.007300624112203314,
213
+ 0.003492257441496715,
214
+ 0.007360542338169028,
215
+ 0.007382295095945714,
216
+ 0.014832571097070651,
217
+ 0.004265176358855972,
218
+ 0.010263477266301436,
219
+ 0.0037736525044596452,
220
+ 0.010357919182143871,
221
+ 0.003968945641000572,
222
+ 0.009871247361505292,
223
+ 0.004710725207575529,
224
+ 0.006315269663161832,
225
+ 0.005157638724597448,
226
+ 0.0077605726110578045,
227
+ 0.007187969630144764,
228
+ 0.006412449477317231,
229
+ 0.005628045100877153,
230
+ 0.011602968806865283,
231
+ 0.010493624967610065,
232
+ 0.01484179003057013,
233
+ 0.0065827905214298045,
234
+ 0.007502225175175732,
235
+ 0.010939826561705892,
236
+ 0.011289749262541926,
237
+ 0.009076003093031464,
238
+ 0.010591555180129155,
239
+ 0.006798531213847064,
240
+ 0.006580023268606611,
241
+ 0.006625866180657621,
242
+ 0.008090722319725546,
243
+ 0.007049768301782186,
244
+ 0.0076193802901092424,
245
+ 0.01317291404961464,
246
+ 0.007973038587594787,
247
+ 0.013669095027737707,
248
+ 0.00885488994384447,
249
+ 0.010764243344026031,
250
+ 0.009261971480032901,
251
+ 0.011407087221082144,
252
+ 0.008275493172872336,
253
+ 0.01441150271184049,
254
+ 0.008746366070571202,
255
+ 0.00857308571676133,
256
+ 0.005291706775934275,
257
+ 0.009011519952519172,
258
+ 0.006988717495289805,
259
+ 0.009253048598594693,
260
+ 0.007821243894415368,
261
+ 0.011086758968541641,
262
+ 0.007673134347475914,
263
+ 0.011097951070032072,
264
+ 0.0062189255594573304,
265
+ 0.009936426882326339,
266
+ 0.007959877939685181,
267
+ 0.011968876654308003,
268
+ 0.008339926878809764,
269
+ 0.01269539177288809,
270
+ 0.005731582712877293,
271
+ 0.00929172196587231,
272
+ 0.005613550406088605,
273
+ 0.0091759482678083,
274
+ 0.0059780352420775095,
275
+ 0.008944619689293477,
276
+ 0.007367184276934712,
277
+ 0.007941269958145774,
278
+ 0.007299264060980633,
279
+ 0.007239133563442758,
280
+ 0.006612679564797477,
281
+ 0.006283524080156457,
282
+ 0.008149139416552728,
283
+ 0.009507867376475309,
284
+ 0.007738268726780678,
285
+ 0.007931483865568464,
286
+ 0.005231235554487746,
287
+ 0.006651492065929697,
288
+ 0.006507353996223413,
289
+ 0.00795472713238325,
290
+ 0.005030903411493361,
291
+ 0.007363008031486606,
292
+ 0.004293715643194716,
293
+ 0.005116324444397954,
294
+ 0.005079498064988051,
295
+ 0.005328559680790829,
296
+ 0.004718152120051172,
297
+ 0.004527017977918373,
298
+ 0.005665956328012781,
299
+ 0.005613541568597172,
300
+ 0.006477197736781978,
301
+ 0.004409438813197414,
302
+ 0.004562203092508441,
303
+ 0.003927667497232979,
304
+ 0.0059540022827155575,
305
+ 0.004466087633554613,
306
+ 0.004391156406161099,
307
+ 0.004415206577921035,
308
+ 0.003937588905054544,
309
+ 0.003719985017029452,
310
+ 0.003676435977917266,
311
+ 0.003814600894783379,
312
+ 0.003433020728342273,
313
+ 0.003935725556137382,
314
+ 0.0040426670106934615,
315
+ 0.0028970529888585096,
316
+ 0.004150673826274122,
317
+ 0.0032579911376673194,
318
+ 0.004363882486274141,
319
+ 0.004270449886008421,
320
+ 0.004907106187000692,
321
+ 0.0030152489510528215,
322
+ 0.0038070702355756493,
323
+ 0.0027748512230974798,
324
+ 0.0029272521743110213,
325
+ 0.0021288821303970962,
326
+ 0.0032297161628375743,
327
+ 0.0028509226016853032,
328
+ 0.002679914879840049,
329
+ 0.0018259880199832242,
330
+ 0.0032921295997996778,
331
+ 0.001677051151594143,
332
+ 0.0024395236545130036,
333
+ 0.0019836353522911625,
334
+ 0.003758717714186613,
335
+ 0.0034909357310569456,
336
+ 0.002995263611964451,
337
+ 0.0018092325269436543,
338
+ 0.0022474839811938456,
339
+ 0.00244456580072532,
340
+ 0.00233923457909925,
341
+ 0.0017213923060684793,
342
+ 0.0026590153316273075,
343
+ 0.0013800374494157535,
344
+ 0.002284641032769587,
345
+ 0.001136852586526592,
346
+ 0.0018214148728505498,
347
+ 0.0013022046110269284,
348
+ 0.001396674983025135,
349
+ 0.0015469207178817877,
350
+ 0.002837757747934389,
351
+ 0.002359877651502992,
352
+ 0.002076823924285004,
353
+ 0.0016799064395334354,
354
+ 0.001560964204396998,
355
+ 0.002849456766570762,
356
+ 0.001860766958266745,
357
+ 0.0021249687584761062,
358
+ 0.0018335763283124761,
359
+ 0.0015378773132315709,
360
+ 0.0011432679414856723,
361
+ 0.0011736150194323123,
362
+ 0.001212483917375532,
363
+ 0.001279967116160827,
364
+ 0.0014981555536654756,
365
+ 0.001122604974469808,
366
+ 0.0014509640741489289,
367
+ 0.0012002230758373783,
368
+ 0.001165903344811268,
369
+ 0.0013133584143074069,
370
+ 0.0012965505666185648,
371
+ 0.0020724105583319074,
372
+ 0.0015725440101973375,
373
+ 0.0021442389022742194,
374
+ 0.0014270560400526345,
375
+ 0.0013324118457630544,
376
+ 0.0009997932453970622,
377
+ 0.0011887167011727473,
378
+ 0.0007072343298746989,
379
+ 0.0013301494348818492,
380
+ 0.0006204627186076996,
381
+ 0.00039869722720875164,
382
+ 0.00043077402577967584,
383
+ 0.00046720314443578676,
384
+ 0.00039750241121630064,
385
+ 0.0005384150176558039,
386
+ 0.0008002933341730924,
387
+ 0.0008629090005762214,
388
+ 0.0006959207112273458,
389
+ 0.00040135183128836295,
390
+ 0.0004873041609817168,
391
+ 0.0005243254947311233,
392
+ 0.000664005589046579,
393
+ 0.0004979014294268762,
394
+ 0.000528657593126281,
395
+ 0.00042244525643066514,
396
+ 0.0004099317046350386,
397
+ 0.00039515285548507293,
398
+ 0.00038376572004818085,
399
+ 0.0003955256133498657,
400
+ 0.00019519812609798947
401
+ ]
402
+ },
403
+ "family_target_counts": {
404
+ "intensity": 4770785,
405
+ "mz": 4770785
406
+ },
407
+ "finite_epoch_schedule_used": false,
408
+ "maximum_absolute_difference_from_declared_runtime_null": 4.440892098500626e-15,
409
+ "parent_count": 2455,
410
+ "scale": 3.006098541980087,
411
+ "target_token_count": 10243502
412
+ },
413
+ "ms_local_development_reducer": "equal MS parents then equal acquisitions within MS parent",
414
+ "null_scale": 3.006098541980086,
415
+ "null_weighting": "equal parents, equal acquisitions within parent, equal family means within acquisition",
416
+ "population_atomic_weight_formula": "1/N_parent * 1/N_acquisition(parent) * 1/2 * 1/N_peak_pair(acquisition)",
417
+ "support_kind": "all eligible pretraining_train acquisition peak-pair labels as a population expectation, not realized finite-epoch schedule exposure",
418
+ "training_schedule_expectation": "equal parents then equal acquisitions within parent"
419
+ },
420
+ "official_submitted_artifacts_modified": false,
421
+ "raw_authority_binding": {
422
+ "all_source_parent_acquisition_target_and_provenance_fields_match": true,
423
+ "raw_authority": {
424
+ "bytes": 732729924,
425
+ "name": "review_a5_ms_primary_runtime_transformation_authority.tsv.gz",
426
+ "sha256": "d3c16f4bdbe8b35440855d1c83287c5bda12d512ec3b89ba0ca312829dddb7f4"
427
+ },
428
+ "row_count": 225056,
429
+ "source_raw_differs_from_positive_projection_count": 351,
430
+ "streamed_no_follow_not_loaded_wholesale": true,
431
+ "unique_row_surface_count": 225056
432
+ },
433
+ "receipt": {
434
+ "bytes": 6260,
435
+ "name": "review_a5_ms_acquisition_top30_runtime_receipt.json",
436
+ "sha256": "f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4"
437
+ },
438
+ "release_boundary": "MS lane is independently promotable into the isolated eight-cell integration bundle; global training/scientific launch remains false until every other objective and execution gate passes.",
439
+ "resume_cursor_fixture": {
440
+ "authority_sha256": "48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4",
441
+ "epoch": 11,
442
+ "global_parent_cursor": 1337,
443
+ "global_parent_order_sha256": "b9efe4846a7ed24e681342982393f387ac37daf95e95e963b7f76e1131d59819",
444
+ "population_binding_sha256": "35a729fc66db5fdb440b70843bccaf557ca2674635523ceff9e440bf8d99ee91",
445
+ "receipt_sha256": "f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4",
446
+ "schedule_schema": "bertose_a5_ms_parent_cycle_hash_schedule_v1",
447
+ "schema": "bertose_a5_ms_acquisition_top30_resume_cursor_v3",
448
+ "seed": 43,
449
+ "selected_prefix_sha256": "40076d2d1390fa7225cfbb9f37e177d16bc32eded92447b567b9265c91ab87f9"
450
+ },
451
+ "runtime_implementation": {
452
+ "bytes": 43711,
453
+ "name": "review_a5_ms_acquisition_top30_runtime_v3.py",
454
+ "sha256": "68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6"
455
+ },
456
+ "schedule_fixture_sha256_by_epoch": {
457
+ "0": "c9f2332c62840aa0c2e4bcbafdfff9abfd7cb2a2c4bfb0847a6b176aa9b9eed1",
458
+ "1": "f209078a76268d07409838423889de542e782f53355747c1940aeac497331772",
459
+ "17": "dc7a6debb69e6d5f995173def4066d139da51b9e7198779556211abbdbf75872",
460
+ "7": "33c8498639d1f0a338e9aeb90708a67f90649ce84f3c5dcd51663ae4eb19315d"
461
+ },
462
+ "schema": "bertose_a5_ms_acquisition_top30_runtime_independent_certificate_v3",
463
+ "scientific_launch_authorized": false,
464
+ "status": "PASS_MS_TOP30_LANE_RUNTIME_CERTIFIED_INTEGRATED_LAUNCH_CLOSED",
465
+ "training_authorized": false
466
+ }
releases/2026-10-02/bertose_source/data/review_ms_acquisition_top30_runtime_20260721_r3/review_a5_ms_acquisition_top30_runtime_receipt.json ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifacts": {
3
+ "development_expansion_manifest": {
4
+ "bytes": 2467731,
5
+ "name": "review_ms_acquisition_top30_development_expansion.tsv.gz",
6
+ "sha256": "f337cc115011dbdb2040f01142de00f42a28a68218a7441f721cd76b74a18753"
7
+ },
8
+ "parent_manifest": {
9
+ "bytes": 78127,
10
+ "name": "review_ms_acquisition_top30_parents.tsv.gz",
11
+ "sha256": "10dc3fa7622e2a7e448d775f4082fdc84aa0622f9a7a74bcfaeed491c0eb7d39"
12
+ },
13
+ "row_manifest": {
14
+ "bytes": 65148547,
15
+ "name": "review_ms_acquisition_top30_rows.tsv.gz",
16
+ "sha256": "cf1dd59bd0b11525e643b2b44e60ff3bebc374e83d671365f4d5ec1f1e2d3a49"
17
+ },
18
+ "target_ids": {
19
+ "bytes": 26045132,
20
+ "name": "review_ms_acquisition_top30_target_ids_u16le.bin",
21
+ "sha256": "1d545f3d1ed77111f04298c632885b0809879ccc1dc4103c4ffc6f947a4bd472"
22
+ },
23
+ "target_offsets": {
24
+ "bytes": 1800456,
25
+ "name": "review_ms_acquisition_top30_target_offsets_u64le.bin",
26
+ "sha256": "42eb4beae41b9ec821e1fcc0cf18d8f36041d6e2075605012c3f9070fca750cd"
27
+ }
28
+ },
29
+ "authority": {
30
+ "bytes": 20541,
31
+ "name": "review_a5_ms_acquisition_top30_runtime_authority.json",
32
+ "sha256": "48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4"
33
+ },
34
+ "implementation": {
35
+ "builder": {
36
+ "bytes": 55528,
37
+ "name": "build_review_a5_ms_acquisition_top30_runtime_package_r3_20260721.py",
38
+ "sha256": "e797638591e884cfda0053e84519e82770155ae339f531689b04e2d7993ef6d5"
39
+ },
40
+ "certifier": {
41
+ "bytes": 49683,
42
+ "name": "certify_review_a5_ms_acquisition_top30_runtime_package_r3_20260721.py",
43
+ "sha256": "a1e2989e36876840ee10d555b1ab69e9876fb0483aec839bb5a44187e7a1122a"
44
+ },
45
+ "runtime": {
46
+ "bytes": 43711,
47
+ "name": "review_a5_ms_acquisition_top30_runtime_v3.py",
48
+ "sha256": "68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6"
49
+ },
50
+ "tests": {
51
+ "bytes": 15700,
52
+ "name": "test_review_a5_ms_acquisition_top30_runtime_v3_20260721.py",
53
+ "sha256": "3ba5679eb77bb1f3ae27b168009b4af60a7d63dca71d108cb179cb017f69b64e"
54
+ }
55
+ },
56
+ "inputs": {
57
+ "count_child_authority": {
58
+ "bytes": 15090,
59
+ "name": "review_a5_ms_top30_count_child_authority.json",
60
+ "sha256": "70a38dffb73aa9a482bc2227dd874862896453b7a45e3441f2aa97ea9ac9fab9"
61
+ },
62
+ "count_child_evidence": {
63
+ "bytes": 820,
64
+ "name": "review_a5_ms_top30_count_delta_evidence.tsv.gz",
65
+ "sha256": "dbdc6bbe0e8c4e17ab30908aaa510a09c522a8c4f8d154efac21b60a1453b2a8"
66
+ },
67
+ "count_child_independent_certificate": {
68
+ "bytes": 2078,
69
+ "name": "review_a5_ms_top30_count_child_authority_independent_audit.json",
70
+ "sha256": "c708f81a39706a9af86bbf5fe2d859a53de19f565b9f4eae2ff12f62a066513e"
71
+ },
72
+ "count_child_receipt": {
73
+ "bytes": 9916,
74
+ "name": "review_a5_ms_top30_count_child_receipt.json",
75
+ "sha256": "1bd22e3d7ac9312db567fdd9357b30e853ada4e7a5e2d23478f864422c3c7b15"
76
+ },
77
+ "design_independent_certificate": {
78
+ "bytes": 5889,
79
+ "name": "review_a5_ms_acquisition_top30_independent_audit_r2.json",
80
+ "sha256": "158450a27ae9bb642c010d5a2275adacef491bcf14a0ba227beb96cc2eaaaa5f"
81
+ },
82
+ "design_manifest": {
83
+ "bytes": 105029603,
84
+ "name": "review_a5_ms_acquisition_top30_manifest.tsv.gz",
85
+ "sha256": "bfe12eb3100552039dd18b217c899e15c75174b314b624deb72e320edc873e97"
86
+ },
87
+ "design_receipt": {
88
+ "bytes": 6517,
89
+ "name": "review_a5_ms_acquisition_top30_receipt.json",
90
+ "sha256": "86913823e6c5c14924735575dffac37ec4264ec130b0eaafda05226f4a87880e"
91
+ },
92
+ "design_summary": {
93
+ "bytes": 21986,
94
+ "name": "review_a5_ms_acquisition_top30_summary.json",
95
+ "sha256": "2fb09f48f204ed8fbf5edb8b109eba880b7de60ff40ce54f3d30cb41294a3211"
96
+ },
97
+ "design_superseded_metadata_certificate": {
98
+ "bytes": 5470,
99
+ "name": "review_a5_ms_acquisition_top30_independent_audit.json",
100
+ "sha256": "dff09234ed4b6550bf5c9f17e32e2e4f356aaa18efb2a7a21e70ac9f568e6338"
101
+ },
102
+ "design_train_only_null": {
103
+ "bytes": 8878,
104
+ "name": "review_a5_ms_acquisition_top30_train_only_null.json",
105
+ "sha256": "2f68b42c9201011a078ea806199fdf151473b4b6ff38a9273f1a53a63fed4493"
106
+ },
107
+ "raw_clean_double_build_certificate": {
108
+ "bytes": 4617,
109
+ "name": "review_a5_ms_raw_to_top30_clean_double_build_r3_certificate.json",
110
+ "sha256": "f92f2c0c56acddbdf818d015cd8f7d9d99c0b98535c97624116fe92f7b9cb28e"
111
+ },
112
+ "raw_end_to_end_receipt": {
113
+ "bytes": 11189,
114
+ "name": "review_a5_ms_raw_to_top30_end_to_end_receipt.json",
115
+ "sha256": "98c4bbf1088e28a0a56593fc66796ffba75235755c72adfe6e02f59c0f4a48e7"
116
+ },
117
+ "raw_publication_certificate": {
118
+ "bytes": 7603,
119
+ "name": "review_a5_ms_raw_to_top30_published_r3_certificate.json",
120
+ "sha256": "d9f5845940f57af88d212d395bf90bcd4e8d5728d88060c460ad0784390a1bd5"
121
+ },
122
+ "raw_transformation_authority": {
123
+ "bytes": 732729924,
124
+ "name": "review_a5_ms_primary_runtime_transformation_authority.tsv.gz",
125
+ "sha256": "d3c16f4bdbe8b35440855d1c83287c5bda12d512ec3b89ba0ca312829dddb7f4"
126
+ },
127
+ "raw_transformation_authority_receipt": {
128
+ "bytes": 1367,
129
+ "name": "review_a5_ms_primary_runtime_transformation_authority_receipt.json",
130
+ "sha256": "e82ceec1a94ec2f250d28e4ed37e818aee234eb132ece9fec03f43fabc9d6378"
131
+ }
132
+ },
133
+ "official_submitted_artifacts_modified": false,
134
+ "schema": "bertose_a5_ms_acquisition_top30_runtime_receipt_v3",
135
+ "scientific_launch_authorized": false,
136
+ "status": "PASS_RUNTIME_BYTES_PREPARATION_ONLY",
137
+ "supersession": {
138
+ "predecessor_preserved_without_overwrite": true,
139
+ "predecessor_release_status": "NO_GO_SUPERSEDED",
140
+ "predecessor_runtime": {
141
+ "bytes": 33000,
142
+ "name": "review_a5_ms_acquisition_top30_runtime_v2.py",
143
+ "sha256": "94359f20bf8040e7758d2b793d5c78a6b3045cd726143b785d7d8cbbeda6aa14"
144
+ },
145
+ "predecessor_schema": "bertose_a5_ms_acquisition_top30_runtime_v2",
146
+ "reason": "The predecessor accepted caller seed values for development corruption. The v3 successor exposes a separate development-mask API fixed to seed 0 and epoch 0.",
147
+ "successor_schema": "bertose_a5_ms_acquisition_top30_runtime_v3"
148
+ },
149
+ "training_authorized": false
150
+ }
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_bpe_vocabulary.json ADDED
The diff for this file is too large to render. See raw diff
 
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_sequence_tree_runtime.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fail-closed WURCS tree channels for the review-only A5 runtime.
2
+
3
+ The fitted BPE vocabulary is deliberately not changed here. This module only
4
+ repairs deterministic transform-time annotations. A tree depth is emitted
5
+ only when every linkage is a full, unambiguous residue-pair token, exactly one
6
+ endpoint of every bond is the anomeric donor, and the resulting directed graph
7
+ is one rooted tree. Otherwise residue tokens receive an explicit UNKNOWN
8
+ depth while the original WURCS units remain available to BPE.
9
+
10
+ This file is an isolated review runtime. It must not be copied over the shared
11
+ ``training/a5_wurcs_bpe.py`` module.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections import defaultdict, deque
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ from training.a5_topology import node_index, node_label
22
+ from training.a5_wurcs_bpe import A5WURCSBPETokenizer, A5WURCSPreTokenizer
23
+
24
+
25
+ BRANCH_DEPTH_UNKNOWN = 202
26
+ BRANCH_DEPTH_VOCAB_SIZE = 203
27
+
28
+ LINKAGE_NONE = 0
29
+ LINKAGE_1_2 = 1
30
+ LINKAGE_1_3 = 2
31
+ LINKAGE_1_4 = 3
32
+ LINKAGE_1_6 = 4
33
+ LINKAGE_2_3 = 5
34
+ LINKAGE_2_6 = 6
35
+ LINKAGE_3_6 = 7
36
+ LINKAGE_OTHER_EXACT = 8
37
+ LINKAGE_UNKNOWN = 9
38
+ LINKAGE_VOCAB_SIZE = 10
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class TreeChannelAnalysis:
43
+ residue_count: int
44
+ exact_undirected_tree: bool
45
+ exact_rooted_tree: bool
46
+ failure_reason: str
47
+ depths: dict[int, int]
48
+ branch_open_counts: dict[int, int]
49
+ distance_matrix: list[list[int]]
50
+
51
+
52
+ def _distance_matrix(adjacency: dict[int, set[int]], count: int) -> list[list[int]]:
53
+ matrix: list[list[int]] = []
54
+ for source in range(count):
55
+ distances = {source: 0}
56
+ queue: deque[int] = deque([source])
57
+ while queue:
58
+ current = queue.popleft()
59
+ for neighbor in sorted(adjacency[current]):
60
+ if neighbor not in distances:
61
+ distances[neighbor] = distances[current] + 1
62
+ queue.append(neighbor)
63
+ if len(distances) != count:
64
+ return []
65
+ matrix.append([distances[target] for target in range(count)])
66
+ return matrix
67
+
68
+
69
+ class ReviewA5WURCSPreTokenizerV3(A5WURCSPreTokenizer):
70
+ """A5 semantic units with fail-closed branch and linkage channels."""
71
+
72
+ def _sections(
73
+ self, wurcs: str
74
+ ) -> tuple[list[str], list[str], list[str], list[int | None]]:
75
+ parts = self._split_wurcs_sections(wurcs)
76
+ if len(parts) < 4:
77
+ return [], [], [], []
78
+ residue_definitions = self.RESIDUE_PATTERN.findall(parts[2])
79
+ index_list = parts[3].split("-") if parts[3] else []
80
+ linkages = parts[4].split("_") if len(parts) > 4 and parts[4] else []
81
+ anomeric = self._instance_anomeric_positions(residue_definitions, index_list)
82
+ return residue_definitions, index_list, linkages, anomeric
83
+
84
+ def analyze_tree(self, wurcs: str) -> TreeChannelAnalysis:
85
+ _, index_list, linkages, anomeric = self._sections(wurcs)
86
+ count = len(index_list)
87
+ if count == 0:
88
+ return TreeChannelAnalysis(0, False, False, "missing_residue_instances", {}, {}, [])
89
+ if count == 1:
90
+ if linkages:
91
+ return TreeChannelAnalysis(
92
+ 1, False, False, "single_residue_has_linkage_tokens", {}, {}, []
93
+ )
94
+ return TreeChannelAnalysis(1, True, True, "", {0: 0}, {}, [[0]])
95
+
96
+ adjacency: dict[int, set[int]] = {index: set() for index in range(count)}
97
+ directed_children: dict[int, list[int]] = defaultdict(list)
98
+ child_parent: dict[int, int] = {}
99
+ seen_edges: set[tuple[int, int]] = set()
100
+ orientation_failure = ""
101
+
102
+ for linkage in linkages:
103
+ match = self.A5_LINKAGE_PATTERN.fullmatch(linkage)
104
+ if match is None:
105
+ return TreeChannelAnalysis(
106
+ count, False, False, "non_exact_linkage_token", {}, {}, []
107
+ )
108
+ left_label, left_position, right_label, right_position = match.groups()
109
+ try:
110
+ left = node_index(left_label) - 1
111
+ right = node_index(right_label) - 1
112
+ except ValueError:
113
+ return TreeChannelAnalysis(
114
+ count, False, False, "invalid_residue_label", {}, {}, []
115
+ )
116
+ if not (0 <= left < count and 0 <= right < count) or left == right:
117
+ return TreeChannelAnalysis(
118
+ count, False, False, "invalid_residue_edge", {}, {}, []
119
+ )
120
+ edge = tuple(sorted((left, right)))
121
+ if edge in seen_edges:
122
+ return TreeChannelAnalysis(
123
+ count, False, False, "duplicate_residue_edge", {}, {}, []
124
+ )
125
+ seen_edges.add(edge)
126
+ adjacency[left].add(right)
127
+ adjacency[right].add(left)
128
+
129
+ left_is_donor = (
130
+ left_position != "?"
131
+ and anomeric[left] is not None
132
+ and int(left_position) == anomeric[left]
133
+ )
134
+ right_is_donor = (
135
+ right_position != "?"
136
+ and anomeric[right] is not None
137
+ and int(right_position) == anomeric[right]
138
+ )
139
+ if left_is_donor == right_is_donor:
140
+ orientation_failure = "bond_donor_not_unique"
141
+ continue
142
+ child = left if left_is_donor else right
143
+ parent = right if left_is_donor else left
144
+ if child in child_parent and child_parent[child] != parent:
145
+ orientation_failure = "residue_has_multiple_parents"
146
+ continue
147
+ child_parent[child] = parent
148
+ directed_children[parent].append(child)
149
+
150
+ if len(seen_edges) != count - 1:
151
+ return TreeChannelAnalysis(
152
+ count, False, False, "edge_count_not_residue_count_minus_one", {}, {}, []
153
+ )
154
+ distances = _distance_matrix(adjacency, count)
155
+ if not distances:
156
+ return TreeChannelAnalysis(
157
+ count, False, False, "residue_graph_disconnected", {}, {}, []
158
+ )
159
+ if orientation_failure:
160
+ return TreeChannelAnalysis(
161
+ count, True, False, orientation_failure, {}, {}, distances
162
+ )
163
+
164
+ roots = sorted(set(range(count)) - set(child_parent))
165
+ if len(roots) != 1:
166
+ return TreeChannelAnalysis(
167
+ count, True, False, "root_not_unique", {}, {}, distances
168
+ )
169
+ depths = {roots[0]: 0}
170
+ queue: deque[int] = deque([roots[0]])
171
+ while queue:
172
+ parent = queue.popleft()
173
+ for child in sorted(directed_children.get(parent, [])):
174
+ if child in depths:
175
+ return TreeChannelAnalysis(
176
+ count, True, False, "directed_cycle", {}, {}, distances
177
+ )
178
+ depths[child] = depths[parent] + 1
179
+ queue.append(child)
180
+ if len(depths) != count:
181
+ return TreeChannelAnalysis(
182
+ count, True, False, "directed_tree_disconnected", {}, {}, distances
183
+ )
184
+ if max(depths.values()) >= BRANCH_DEPTH_UNKNOWN:
185
+ return TreeChannelAnalysis(
186
+ count, True, False, "exact_depth_collides_with_unknown_id", {}, {}, distances
187
+ )
188
+ branch_counts = {
189
+ parent: len(children) - 1
190
+ for parent, children in directed_children.items()
191
+ if len(children) > 1
192
+ }
193
+ return TreeChannelAnalysis(
194
+ count, True, True, "", depths, branch_counts, distances
195
+ )
196
+
197
+ @staticmethod
198
+ def exact_linkage_type(linkage: str) -> int:
199
+ match = ReviewA5WURCSPreTokenizerV3.A5_LINKAGE_PATTERN.fullmatch(linkage)
200
+ if match is None:
201
+ return LINKAGE_UNKNOWN
202
+ _, left_position, _, right_position = match.groups()
203
+ if left_position == "?" or right_position == "?":
204
+ return LINKAGE_UNKNOWN
205
+ positions = tuple(sorted((int(left_position), int(right_position))))
206
+ mapping = {
207
+ (1, 2): LINKAGE_1_2,
208
+ (1, 3): LINKAGE_1_3,
209
+ (1, 4): LINKAGE_1_4,
210
+ (1, 6): LINKAGE_1_6,
211
+ (2, 3): LINKAGE_2_3,
212
+ (2, 6): LINKAGE_2_6,
213
+ (3, 6): LINKAGE_3_6,
214
+ }
215
+ return mapping.get(positions, LINKAGE_OTHER_EXACT)
216
+
217
+ def pre_tokenize(self, wurcs: str) -> list[dict]:
218
+ # Reuse only the already-audited semantic WURCS unit extraction. All
219
+ # v2 branch markers, depths, linkage classes, and distance matrices are
220
+ # discarded and rebuilt below.
221
+ inherited = super().pre_tokenize(wurcs)
222
+ units = [dict(unit) for unit in inherited if unit.get("text") != "[BRANCH_OPEN]"]
223
+ analysis = self.analyze_tree(wurcs)
224
+ first_unit_for_residue: set[int] = set()
225
+ repaired: list[dict] = []
226
+ for unit in units:
227
+ unit.pop("distance_matrix", None)
228
+ residue_id = int(unit.get("residue_id", -1))
229
+ unit["linkage_type"] = LINKAGE_NONE
230
+ if residue_id >= 0:
231
+ depth = (
232
+ analysis.depths[residue_id]
233
+ if analysis.exact_rooted_tree
234
+ else BRANCH_DEPTH_UNKNOWN
235
+ )
236
+ unit["branch_depth"] = depth
237
+ if residue_id not in first_unit_for_residue:
238
+ for _ in range(analysis.branch_open_counts.get(residue_id, 0)):
239
+ repaired.append(
240
+ {
241
+ "text": "[BRANCH_OPEN]",
242
+ "type": "special",
243
+ "residue_id": -1,
244
+ "branch_depth": depth,
245
+ "linkage_type": LINKAGE_NONE,
246
+ }
247
+ )
248
+ first_unit_for_residue.add(residue_id)
249
+ else:
250
+ unit["branch_depth"] = 0
251
+ if unit.get("type") == "linkage":
252
+ unit["linkage_type"] = self.exact_linkage_type(str(unit["text"]))
253
+ repaired.append(unit)
254
+
255
+ if repaired:
256
+ repaired[0]["distance_matrix"] = analysis.distance_matrix
257
+ repaired[0]["tree_channels_valid"] = analysis.exact_rooted_tree
258
+ repaired[0]["tree_channel_failure_reason"] = analysis.failure_reason
259
+ return repaired
260
+
261
+
262
+ class ReviewA5WURCSBPETokenizerV3(A5WURCSBPETokenizer):
263
+ """Existing train-only BPE vocabulary with repaired transform channels."""
264
+
265
+ def __init__(self, vocab_path: Optional[str | Path] = None) -> None:
266
+ super().__init__(vocab_path)
267
+ self.pre_tokenizer = ReviewA5WURCSPreTokenizerV3()
268
+
269
+ def tokenize_sequence(self, wurcs: str, max_length: int = 256) -> dict:
270
+ encoded = super().tokenize_sequence(wurcs, max_length=max_length)
271
+ analysis = self.pre_tokenizer.analyze_tree(wurcs)
272
+ encoded["tree_channels_valid"] = analysis.exact_rooted_tree
273
+ encoded["tree_channel_failure_reason"] = analysis.failure_reason
274
+ return encoded
275
+
276
+
277
+ __all__ = [
278
+ "BRANCH_DEPTH_UNKNOWN",
279
+ "BRANCH_DEPTH_VOCAB_SIZE",
280
+ "LINKAGE_NONE",
281
+ "LINKAGE_OTHER_EXACT",
282
+ "LINKAGE_UNKNOWN",
283
+ "LINKAGE_VOCAB_SIZE",
284
+ "ReviewA5WURCSBPETokenizerV3",
285
+ "ReviewA5WURCSPreTokenizerV3",
286
+ "TreeChannelAnalysis",
287
+ ]
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_topology.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Residue-level shortest-path targets used by the A5 topology objective."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from collections import deque
9
+
10
+
11
+ EDGE_RE = re.compile(
12
+ r"(?P<n1>[A-Za-z]+)(?P<p1>[0-9?]+)-(?P<n2>[A-Za-z]+)(?P<p2>[0-9?]+)"
13
+ )
14
+ RESDEF_RE = re.compile(r"\[([^\]]*)\]")
15
+ WURCS_RES_INDEX_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
16
+
17
+
18
+ def node_label(index: int) -> str:
19
+ if index < 1:
20
+ raise ValueError("WURCS residue indices are one-based")
21
+ letters = WURCS_RES_INDEX_ALPHABET
22
+ base = len(letters)
23
+ index -= 1
24
+ label = ""
25
+ while True:
26
+ label = letters[index % base] + label
27
+ index = index // base - 1
28
+ if index < 0:
29
+ return label
30
+
31
+
32
+ def node_index(label: str) -> int:
33
+ if not label:
34
+ raise ValueError("WURCS residue label cannot be empty")
35
+ base = len(WURCS_RES_INDEX_ALPHABET)
36
+ value = 0
37
+ for character in label:
38
+ try:
39
+ digit = WURCS_RES_INDEX_ALPHABET.index(character) + 1
40
+ except ValueError as error:
41
+ raise ValueError(f"Invalid WURCS residue label: {label}") from error
42
+ value = value * base + digit
43
+ return value
44
+
45
+
46
+ def split_wurcs_sections(body: str) -> list[str]:
47
+ sections: list[str] = []
48
+ current: list[str] = []
49
+ bracket_depth = 0
50
+ for character in body:
51
+ if character == "[":
52
+ bracket_depth += 1
53
+ elif character == "]" and bracket_depth:
54
+ bracket_depth -= 1
55
+ if character == "/" and bracket_depth == 0:
56
+ sections.append("".join(current))
57
+ current = []
58
+ else:
59
+ current.append(character)
60
+ sections.append("".join(current))
61
+ return sections
62
+
63
+
64
+ def split_wurcs(wurcs: str) -> tuple[list[str], dict[str, str], str]:
65
+ if not wurcs.startswith("WURCS=2.0/"):
66
+ return [], {}, ""
67
+ parts = split_wurcs_sections(wurcs[len("WURCS=2.0/") :])
68
+ if len(parts) < 4:
69
+ return [], {}, ""
70
+ residue_definitions = RESDEF_RE.findall(parts[1])
71
+ sequence = parts[2].split("-") if parts[2] else []
72
+ node_to_residue: dict[str, str] = {}
73
+ for index, value in enumerate(sequence, start=1):
74
+ try:
75
+ residue = residue_definitions[int(value) - 1]
76
+ except (ValueError, IndexError):
77
+ residue = ""
78
+ node_to_residue[node_label(index)] = residue
79
+ return residue_definitions, node_to_residue, "/".join(parts[3:])
80
+
81
+
82
+ def _hash_fields(*values: object) -> str:
83
+ payload = "\x1f".join(str(value) for value in values).encode("utf-8")
84
+ return hashlib.sha256(payload).hexdigest()
85
+
86
+
87
+ def build_residue_shortest_path(wurcs: str) -> dict[str, object]:
88
+ """Build the same immutable residue target frozen by the P0/P1 audit."""
89
+ _, node_to_residue, linkage_section = split_wurcs(wurcs)
90
+ node_ids = list(node_to_residue)
91
+ if not node_ids or any(not node_to_residue[node] for node in node_ids):
92
+ return _failure("missing_or_unmapped_residue_sequence", len(node_ids), 0)
93
+ if len(node_ids) < 2:
94
+ return _failure(
95
+ "fewer_than_two_residues_no_off_diagonal_target", len(node_ids), 0
96
+ )
97
+
98
+ adjacency: dict[str, set[str]] = {node: set() for node in node_ids}
99
+ parsed_edges: list[tuple[str, str]] = []
100
+ unparsed_tokens: list[str] = []
101
+ for token in linkage_section.split("_") if linkage_section else []:
102
+ match = EDGE_RE.fullmatch(token)
103
+ if not match:
104
+ if token:
105
+ unparsed_tokens.append(token)
106
+ continue
107
+ left, right = match.group("n1"), match.group("n2")
108
+ if left not in adjacency or right not in adjacency or left == right:
109
+ unparsed_tokens.append(token)
110
+ continue
111
+ parsed_edges.append(tuple(sorted((left, right))))
112
+ adjacency[left].add(right)
113
+ adjacency[right].add(left)
114
+
115
+ reason = ""
116
+ if unparsed_tokens:
117
+ reason = "one_or_more_edge_tokens_not_unambiguous_residue_pairs"
118
+ elif len(set(parsed_edges)) != len(parsed_edges):
119
+ reason = "duplicate_residue_edge"
120
+ elif len(parsed_edges) != len(node_ids) - 1:
121
+ reason = "edge_count_not_residue_count_minus_one"
122
+
123
+ seen = {node_ids[0]}
124
+ queue = deque([node_ids[0]])
125
+ while queue:
126
+ node = queue.popleft()
127
+ for neighbor in sorted(adjacency[node]):
128
+ if neighbor not in seen:
129
+ seen.add(neighbor)
130
+ queue.append(neighbor)
131
+ if not reason and len(seen) != len(node_ids):
132
+ reason = "residue_graph_disconnected"
133
+ if reason:
134
+ return _failure(reason, len(node_ids), len(parsed_edges))
135
+
136
+ matrix: list[list[int]] = []
137
+ for source in node_ids:
138
+ distance = {source: 0}
139
+ queue = deque([source])
140
+ while queue:
141
+ node = queue.popleft()
142
+ for neighbor in sorted(adjacency[node]):
143
+ if neighbor not in distance:
144
+ distance[neighbor] = distance[node] + 1
145
+ queue.append(neighbor)
146
+ matrix.append([distance[target] for target in node_ids])
147
+
148
+ matrix_payload = json.dumps(matrix, separators=(",", ":"))
149
+ matrix_sha256 = hashlib.sha256(matrix_payload.encode("utf-8")).hexdigest()
150
+ return {
151
+ "target_valid": True,
152
+ "failure_reason": "",
153
+ "residue_count": len(node_ids),
154
+ "parsed_edge_count": len(parsed_edges),
155
+ "valid_unordered_pair_count": len(node_ids) * (len(node_ids) - 1) // 2,
156
+ "matrix": matrix,
157
+ "matrix_sha256": matrix_sha256,
158
+ "target_identity_sha256": _hash_fields(
159
+ "residue_shortest_path_v2",
160
+ wurcs,
161
+ matrix_sha256,
162
+ len(node_ids),
163
+ len(parsed_edges),
164
+ ),
165
+ "schema": "residue_shortest_path_v2",
166
+ }
167
+
168
+
169
+ def _failure(reason: str, residues: int, edges: int) -> dict[str, object]:
170
+ return {
171
+ "target_valid": False,
172
+ "failure_reason": reason,
173
+ "residue_count": residues,
174
+ "parsed_edge_count": edges,
175
+ "valid_unordered_pair_count": 0,
176
+ "matrix": [],
177
+ "matrix_sha256": "",
178
+ "target_identity_sha256": "",
179
+ "schema": "residue_shortest_path_v2",
180
+ }
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_frozen_training_a5_wurcs_bpe.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic train-only WURCS BPE utilities for A5."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import Counter, defaultdict
7
+ from pathlib import Path
8
+ from typing import Iterable, Optional
9
+
10
+ from .a5_topology import node_index, node_label
11
+
12
+ try:
13
+ from bert_training_v4.downstream_tasks.utils.wurcs_bpe_tokenizer import (
14
+ WURCSBPETokenizer,
15
+ WURCSPreTokenizer,
16
+ )
17
+ except ImportError:
18
+ from ..bert_training_v4.downstream_tasks.utils.wurcs_bpe_tokenizer import (
19
+ WURCSBPETokenizer,
20
+ WURCSPreTokenizer,
21
+ )
22
+
23
+
24
+ SUBMITTED_FIT_ALPHABET = tuple(
25
+ "#$%()*-./0123456789<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ^abcdefghijklmnopqrstuvwxyz{|}~"
26
+ )
27
+
28
+
29
+ class _TopLevelBracketPattern:
30
+ """Expose regex-like findall while respecting nested SMILES brackets."""
31
+
32
+ @staticmethod
33
+ def findall(text: str) -> list[str]:
34
+ residues: list[str] = []
35
+ depth = 0
36
+ start = -1
37
+ for index, character in enumerate(text):
38
+ if character == "[":
39
+ if depth == 0:
40
+ start = index + 1
41
+ depth += 1
42
+ elif character == "]" and depth:
43
+ depth -= 1
44
+ if depth == 0 and start >= 0:
45
+ residues.append(text[start:index])
46
+ start = -1
47
+ if depth:
48
+ return []
49
+ return residues
50
+
51
+
52
+ class A5WURCSPreTokenizer(WURCSPreTokenizer):
53
+ """Submitted pre-tokenizer with nested bracket-safe residue extraction."""
54
+
55
+ A5_LINKAGE_PATTERN = re.compile(
56
+ r"([A-Za-z]+)(\d+|\?)-([A-Za-z]+)(\d+|\?)"
57
+ )
58
+ A5_ANOMERIC_PATTERN = re.compile(r"-([0-9]+)([abuxo])(?:_|$)")
59
+
60
+ def __init__(self) -> None:
61
+ super().__init__()
62
+ self.RESIDUE_PATTERN = _TopLevelBracketPattern()
63
+
64
+ def _split_wurcs_sections(self, wurcs: str) -> list[str]:
65
+ sections = super()._split_wurcs_sections(wurcs)
66
+ if len(sections) <= 5:
67
+ return sections
68
+ # WURCS LIN modifications may contain unbracketed slashes. The format
69
+ # has exactly five top-level sections, so every trailing fragment
70
+ # belongs to the linkage section rather than a new section.
71
+ return sections[:4] + ["/".join(sections[4:])]
72
+
73
+ def _instance_anomeric_positions(
74
+ self, residue_definitions: list[str], index_list: list[str]
75
+ ) -> list[int | None]:
76
+ positions: list[int | None] = []
77
+ for definition_index in index_list:
78
+ try:
79
+ residue_definition = residue_definitions[int(definition_index) - 1]
80
+ except (IndexError, ValueError):
81
+ positions.append(None)
82
+ continue
83
+ match = self.A5_ANOMERIC_PATTERN.search(residue_definition)
84
+ positions.append(int(match.group(1)) if match is not None else None)
85
+ return positions
86
+
87
+ def _analyze_a5_tree(
88
+ self,
89
+ linkages: list[str],
90
+ residue_count: int,
91
+ anomeric_positions: list[int | None] | None = None,
92
+ ) -> tuple[dict[str, int], dict[str, int], dict[int, list[int]]]:
93
+ branch_points: dict[str, int] = defaultdict(int)
94
+ children: dict[str, list[str]] = defaultdict(list)
95
+ all_residues: set[str] = set()
96
+ adjacency: dict[int, list[int]] = defaultdict(list)
97
+ for linkage in linkages:
98
+ match = self.A5_LINKAGE_PATTERN.match(linkage)
99
+ if not match:
100
+ continue
101
+ left, left_position, right, right_position = match.groups()
102
+ try:
103
+ left_index = node_index(left) - 1
104
+ right_index = node_index(right) - 1
105
+ except ValueError:
106
+ continue
107
+ if not (
108
+ 0 <= left_index < residue_count
109
+ and 0 <= right_index < residue_count
110
+ and left_index != right_index
111
+ ):
112
+ continue
113
+ parent, child = left, right
114
+ if anomeric_positions is not None:
115
+ left_anomeric = anomeric_positions[left_index]
116
+ right_anomeric = anomeric_positions[right_index]
117
+ left_is_donor = (
118
+ left_position != "?"
119
+ and left_anomeric is not None
120
+ and int(left_position) == left_anomeric
121
+ )
122
+ right_is_donor = (
123
+ right_position != "?"
124
+ and right_anomeric is not None
125
+ and int(right_position) == right_anomeric
126
+ )
127
+ if left_is_donor and not right_is_donor:
128
+ parent, child = right, left
129
+ elif right_is_donor and not left_is_donor:
130
+ parent, child = left, right
131
+ children[parent].append(child)
132
+ all_residues.update((parent, child))
133
+ adjacency[left_index].append(right_index)
134
+ adjacency[right_index].append(left_index)
135
+
136
+ for residue, child_labels in children.items():
137
+ if len(child_labels) > 1:
138
+ branch_points[residue] = len(child_labels) - 1
139
+ child_set = {child for values in children.values() for child in values}
140
+ roots = all_residues - child_set
141
+ root = min(roots, key=node_index) if roots else node_label(1)
142
+ depths = {root: 0}
143
+ queue = [root]
144
+ while queue:
145
+ current = queue.pop(0)
146
+ for child in children.get(current, []):
147
+ if child not in depths:
148
+ depths[child] = depths[current] + 1
149
+ queue.append(child)
150
+ return dict(branch_points), depths, adjacency
151
+
152
+ def _a5_linkage_type(self, linkage: str) -> int:
153
+ match = self.A5_LINKAGE_PATTERN.match(linkage)
154
+ if not match:
155
+ return 8
156
+ _, from_position, _, to_position = match.groups()
157
+ try:
158
+ positions = (int(from_position), int(to_position))
159
+ except ValueError:
160
+ return 8
161
+ linkage_types = {
162
+ (1, 2): 0,
163
+ (2, 1): 0,
164
+ (1, 3): 1,
165
+ (3, 1): 1,
166
+ (1, 4): 2,
167
+ (4, 1): 2,
168
+ (1, 6): 3,
169
+ (6, 1): 3,
170
+ (2, 3): 4,
171
+ (3, 2): 4,
172
+ (2, 6): 5,
173
+ (6, 2): 5,
174
+ (3, 6): 6,
175
+ (6, 3): 6,
176
+ }
177
+ return linkage_types.get(positions, 7)
178
+
179
+ def pre_tokenize(self, wurcs: str) -> list[dict]:
180
+ # Reuse the submitted semantic split, then correct only its tree
181
+ # annotations. This keeps the BPE unit text and special-token behavior.
182
+ inherited_units = super().pre_tokenize(wurcs)
183
+ try:
184
+ parts = self._split_wurcs_sections(wurcs)
185
+ index_list = parts[3].split("-") if len(parts) > 3 and parts[3] else []
186
+ linkages = parts[4].split("_") if len(parts) > 4 and parts[4] else []
187
+ residue_definitions = self.RESIDUE_PATTERN.findall(parts[2])
188
+ anomeric_positions = self._instance_anomeric_positions(
189
+ residue_definitions, index_list
190
+ )
191
+ branch_points, depths, adjacency = self._analyze_a5_tree(
192
+ linkages, len(index_list), anomeric_positions
193
+ )
194
+ except (IndexError, ValueError):
195
+ return inherited_units
196
+
197
+ units: list[dict] = []
198
+ opened_residues: set[int] = set()
199
+ for inherited in inherited_units:
200
+ if inherited.get("text") == "[BRANCH_OPEN]":
201
+ continue
202
+ unit = dict(inherited)
203
+ residue_id = int(unit.get("residue_id", -1))
204
+ if residue_id >= 0:
205
+ label = node_label(residue_id + 1)
206
+ if residue_id not in opened_residues:
207
+ for _ in range(branch_points.get(label, 0)):
208
+ units.append(
209
+ {
210
+ "text": "[BRANCH_OPEN]",
211
+ "type": "special",
212
+ "residue_id": -1,
213
+ "branch_depth": depths.get(label, 0),
214
+ "linkage_type": 0,
215
+ }
216
+ )
217
+ opened_residues.add(residue_id)
218
+ unit["branch_depth"] = depths.get(label, 0)
219
+ elif unit.get("type") == "linkage":
220
+ unit["linkage_type"] = self._a5_linkage_type(str(unit["text"]))
221
+ units.append(unit)
222
+
223
+ if units:
224
+ units[0]["distance_matrix"] = self._compute_distance_matrix(
225
+ adjacency, len(index_list)
226
+ )
227
+ return units
228
+
229
+
230
+ class A5WURCSBPETokenizer(WURCSBPETokenizer):
231
+ """Train-only BPE with a fit-derived alphabet and deterministic merge ties."""
232
+
233
+ def __init__(self, vocab_path: Optional[str | Path] = None) -> None:
234
+ super().__init__(str(vocab_path) if vocab_path is not None else None)
235
+ self.pre_tokenizer = A5WURCSPreTokenizer()
236
+
237
+ @classmethod
238
+ def train_from_fit_corpus(
239
+ cls,
240
+ wurcs_strings: Iterable[str],
241
+ num_merges: int = 2000,
242
+ output_path: Optional[str | Path] = None,
243
+ min_frequency: int = 2,
244
+ max_token_length: Optional[int] = None,
245
+ ) -> "A5WURCSBPETokenizer":
246
+ tokenizer = cls()
247
+ pre_tokenizer = tokenizer.pre_tokenizer
248
+ unit_counts: Counter[str] = Counter()
249
+ for wurcs in wurcs_strings:
250
+ for unit in pre_tokenizer.pre_tokenize(wurcs):
251
+ if unit["type"] != "special":
252
+ unit_counts[unit["text"]] += 1
253
+
254
+ # The submitted tokenizer learned the base character inventory from its
255
+ # corpus. Preserve that behavior, but fit it from training parents only.
256
+ tokenizer.fit_alphabet = tuple(
257
+ sorted({char for unit in unit_counts for char in unit})
258
+ )
259
+
260
+ next_id = len(tokenizer.special_tokens)
261
+ for char in tokenizer.fit_alphabet:
262
+ tokenizer.token_to_id[char] = next_id
263
+ tokenizer.id_to_token[next_id] = char
264
+ next_id += 1
265
+
266
+ word_freqs: dict[tuple[str, ...], int] = {
267
+ tuple(unit): count
268
+ for unit, count in sorted(unit_counts.items())
269
+ if count >= min_frequency
270
+ }
271
+ merges: list[tuple[str, str]] = []
272
+ for _ in range(num_merges):
273
+ pair_counts: Counter[tuple[str, str]] = Counter()
274
+ for word, frequency in word_freqs.items():
275
+ for index in range(len(word) - 1):
276
+ pair_counts[(word[index], word[index + 1])] += frequency
277
+ candidates = [
278
+ (pair, count)
279
+ for pair, count in pair_counts.items()
280
+ if count >= min_frequency
281
+ and (
282
+ max_token_length is None
283
+ or len(pair[0]) + len(pair[1]) <= max_token_length
284
+ )
285
+ ]
286
+ if not candidates:
287
+ break
288
+ best_pair, _ = min(
289
+ candidates,
290
+ key=lambda item: (-item[1], item[0][0], item[0][1]),
291
+ )
292
+ new_token = best_pair[0] + best_pair[1]
293
+ merges.append(best_pair)
294
+ tokenizer.token_to_id[new_token] = next_id
295
+ tokenizer.id_to_token[next_id] = new_token
296
+ next_id += 1
297
+
298
+ merged_freqs: Counter[tuple[str, ...]] = Counter()
299
+ for word, frequency in word_freqs.items():
300
+ merged_word: list[str] = []
301
+ index = 0
302
+ while index < len(word):
303
+ if (
304
+ index < len(word) - 1
305
+ and word[index] == best_pair[0]
306
+ and word[index + 1] == best_pair[1]
307
+ ):
308
+ merged_word.append(new_token)
309
+ index += 2
310
+ else:
311
+ merged_word.append(word[index])
312
+ index += 1
313
+ merged_freqs[tuple(merged_word)] += frequency
314
+ word_freqs = dict(merged_freqs)
315
+
316
+ tokenizer.merges = merges
317
+ if output_path is not None:
318
+ tokenizer.save_vocab(str(output_path))
319
+ return tokenizer
320
+
321
+ def tokenize_sequence(self, wurcs: str, max_length: int = 256) -> dict:
322
+ units = self.pre_tokenizer.pre_tokenize(wurcs)
323
+ tokens: list[str] = []
324
+ token_ids: list[int] = []
325
+ residue_ids: list[int] = []
326
+ branch_depths: list[int] = []
327
+ linkage_types: list[int] = []
328
+
329
+ for unit in units:
330
+ unit_tokens = (
331
+ [unit["text"]]
332
+ if unit["type"] == "special"
333
+ else self.apply_bpe(unit["text"])
334
+ )
335
+ for token in unit_tokens:
336
+ tokens.append(token)
337
+ token_ids.append(
338
+ self.token_to_id.get(token, self.token_to_id["[UNK]"])
339
+ )
340
+ residue_ids.append(int(unit["residue_id"]))
341
+ branch_depths.append(int(unit["branch_depth"]))
342
+ linkage_types.append(int(unit["linkage_type"]))
343
+
344
+ if len(tokens) > max_length:
345
+ tokens = tokens[: max_length - 1] + ["[END]"]
346
+ token_ids = token_ids[: max_length - 1] + [self.token_to_id["[END]"]]
347
+ residue_ids = residue_ids[: max_length - 1] + [-1]
348
+ branch_depths = branch_depths[: max_length - 1] + [0]
349
+ linkage_types = linkage_types[: max_length - 1] + [0]
350
+
351
+ length = len(tokens)
352
+ attention_mask = [1] * length
353
+ padding = max_length - length
354
+ tokens.extend(["[PAD]"] * padding)
355
+ token_ids.extend([self.token_to_id["[PAD]"]] * padding)
356
+ residue_ids.extend([-1] * padding)
357
+ branch_depths.extend([0] * padding)
358
+ linkage_types.extend([0] * padding)
359
+ attention_mask.extend([0] * padding)
360
+ return {
361
+ "tokens": tokens,
362
+ "token_ids": token_ids,
363
+ "residue_ids": residue_ids,
364
+ "branch_depths": branch_depths,
365
+ "linkage_types": linkage_types,
366
+ "attention_mask": attention_mask,
367
+ "length": length,
368
+ }
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_receipt.json ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint_resume_authorized": false,
3
+ "downstream_evaluation_authorized": false,
4
+ "gpu_launch_authorized": false,
5
+ "nova_submission_authorized": false,
6
+ "official_submitted_or_predecessor_artifacts_modified": false,
7
+ "payloads": {
8
+ "review_attention_mask.npy": {
9
+ "bytes": 71989376,
10
+ "name": "review_attention_mask.npy",
11
+ "sha256": "01ed165720b9414e614dd7e7d75284a8886165f11138e16117ffcbe4e4bbf95c"
12
+ },
13
+ "review_bpe_ambiguity_tokens.json": {
14
+ "bytes": 19693,
15
+ "name": "review_bpe_ambiguity_tokens.json",
16
+ "sha256": "22d2aad45b978988c9e26bc9400016014ee825cc6207377385a8cd74f3c88f2b"
17
+ },
18
+ "review_bpe_fit_contributors.tsv.gz": {
19
+ "bytes": 9934117,
20
+ "name": "review_bpe_fit_contributors.tsv.gz",
21
+ "sha256": "4ec8dfaeacef355092313507ab71c628f48ddf1a4fe39e7663748060a07af297"
22
+ },
23
+ "review_bpe_vocabulary.json": {
24
+ "bytes": 138150,
25
+ "name": "review_bpe_vocabulary.json",
26
+ "sha256": "918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b"
27
+ },
28
+ "review_branch_depths.npy": {
29
+ "bytes": 71989376,
30
+ "name": "review_branch_depths.npy",
31
+ "sha256": "f39f76aa56a9bb782b2349f3ad1ca4ad16351ee1f06a8b9ae4b06bff9b1ebece"
32
+ },
33
+ "review_build_official_sequence_topology_successor.py": {
34
+ "bytes": 16295,
35
+ "name": "review_build_official_sequence_topology_successor.py",
36
+ "sha256": "2e84b03ab2189f009654e97c427930cc511c50d079db06f425035fcfb2ae1efb"
37
+ },
38
+ "review_frozen_sequence_tree_runtime.py": {
39
+ "bytes": 11405,
40
+ "name": "review_frozen_sequence_tree_runtime.py",
41
+ "sha256": "10ca4b121d2f22d7a64208e4f05172e7a756b653d0828c9dcc49dd8ea1a5ad04"
42
+ },
43
+ "review_frozen_training_a5_topology.py": {
44
+ "bytes": 6186,
45
+ "name": "review_frozen_training_a5_topology.py",
46
+ "sha256": "f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7"
47
+ },
48
+ "review_frozen_training_a5_wurcs_bpe.py": {
49
+ "bytes": 14161,
50
+ "name": "review_frozen_training_a5_wurcs_bpe.py",
51
+ "sha256": "6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4"
52
+ },
53
+ "review_full_untruncated_lengths.npy": {
54
+ "bytes": 562544,
55
+ "name": "review_full_untruncated_lengths.npy",
56
+ "sha256": "6d83b42630bd8c5d355378b423b3a0021938741b5faa711ecc129a6789e307c2"
57
+ },
58
+ "review_lengths.npy": {
59
+ "bytes": 562544,
60
+ "name": "review_lengths.npy",
61
+ "sha256": "ade919d1e054c0ba852aa047bf873ac675f81a6c1f8a05cc73a0a2258131f021"
62
+ },
63
+ "review_linkage_types.npy": {
64
+ "bytes": 71989376,
65
+ "name": "review_linkage_types.npy",
66
+ "sha256": "89edb1940765928ced6f54e0d394f95c573eb9d96310b14dbd0adf8e20ac07c4"
67
+ },
68
+ "review_owner_clean_build_report.json": {
69
+ "bytes": 3169,
70
+ "name": "review_owner_clean_build_report.json",
71
+ "sha256": "2c24a655f009c6994756d8199b26c3e6489222c4b26d90efd0c43aacc17b9e4c"
72
+ },
73
+ "review_residue_ids.npy": {
74
+ "bytes": 143978624,
75
+ "name": "review_residue_ids.npy",
76
+ "sha256": "36c96dcd3e03ba30e02e16f98d17605742627a37b37cd7824f2a745dd09dc044"
77
+ },
78
+ "review_sequence_objective_eligible.npy": {
79
+ "bytes": 281336,
80
+ "name": "review_sequence_objective_eligible.npy",
81
+ "sha256": "62502d0d1b44a10989350a78f1e033d271fe8417715e10d42eb47c6720ceb3bf"
82
+ },
83
+ "review_sequence_package_summary.json": {
84
+ "bytes": 7777,
85
+ "name": "review_sequence_package_summary.json",
86
+ "sha256": "93ed219d807d4a7735be96bf7cf6a59a0bd407619925ee194aed6d473048cdf0"
87
+ },
88
+ "review_sequence_parent_index.tsv.gz": {
89
+ "bytes": 14697953,
90
+ "name": "review_sequence_parent_index.tsv.gz",
91
+ "sha256": "2c39e9b26bb41a1b9317c59372414f69812ead8676b0c33b23d2676e444a48b0"
92
+ },
93
+ "review_token_categories.npy": {
94
+ "bytes": 71989376,
95
+ "name": "review_token_categories.npy",
96
+ "sha256": "2d51df879382deb4927a446937983fea5a07fbbc783181f7903546222b856205"
97
+ },
98
+ "review_token_ids.npy": {
99
+ "bytes": 143978624,
100
+ "name": "review_token_ids.npy",
101
+ "sha256": "ec2d52dced65d22f0f218ff617d351fbd2930661bc8681e939990149d493537d"
102
+ },
103
+ "review_topology_lengths.npy": {
104
+ "bytes": 93175,
105
+ "name": "review_topology_lengths.npy",
106
+ "sha256": "b52f6ddcf327b8cfc5159659bada97357216f9526b42c230a35192932f43d2f3"
107
+ },
108
+ "review_topology_offsets.npy": {
109
+ "bytes": 744512,
110
+ "name": "review_topology_offsets.npy",
111
+ "sha256": "e06d61b8e47c2a2a3e0c8aeec7e015e673f64f0f483a483abe40ccda2d08b9e0"
112
+ },
113
+ "review_topology_roster.tsv": {
114
+ "bytes": 3890681,
115
+ "name": "review_topology_roster.tsv",
116
+ "sha256": "a216ce485ea699fb1b66ed6915175a49cde8c7d14279dbaf835f4675bc7c7fa1"
117
+ },
118
+ "review_topology_row.npy": {
119
+ "bytes": 1124960,
120
+ "name": "review_topology_row.npy",
121
+ "sha256": "a0ae28b0476322e3dbeda42a891f4a42c0973002a5d8507bf387b12dcbff4e95"
122
+ },
123
+ "review_topology_values.npy": {
124
+ "bytes": 5027730,
125
+ "name": "review_topology_values.npy",
126
+ "sha256": "2d4ba8395f78864b7eed9ed250bb9fd50bd45ef84a10ed497ffb89c684276e4e"
127
+ },
128
+ "review_tree_channels_valid.npy": {
129
+ "bytes": 281336,
130
+ "name": "review_tree_channels_valid.npy",
131
+ "sha256": "d15b160afe92a91a4a3d1b85b4a0353672b217b1d5c54513f3c80eab29c6b7c5"
132
+ }
133
+ },
134
+ "publication": {
135
+ "atomic_rename": true,
136
+ "no_overwrite": true,
137
+ "nofollow_inputs": true,
138
+ "pre_post_source_hash_stable": true
139
+ },
140
+ "schema": "bertose_a5_official_array_sequence_topology_successor_receipt_v1",
141
+ "scientific_launch_authorized": false,
142
+ "status": "PASS_RECEIPT_LAST",
143
+ "summary": {
144
+ "bytes": 7777,
145
+ "name": "review_sequence_package_summary.json",
146
+ "sha256": "93ed219d807d4a7735be96bf7cf6a59a0bd407619925ee194aed6d473048cdf0"
147
+ },
148
+ "training_authorized": false
149
+ }
releases/2026-10-02/bertose_source/data/review_sequences_a5_non_iar_later_role_official_array_v4_20260721_r1/review_sequence_package_summary.json ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bpe": {
3
+ "fit_contributor_sha256": "4ec8dfaeacef355092313507ab71c628f48ddf1a4fe39e7663748060a07af297",
4
+ "fit_parent_count": 221899,
5
+ "fit_roles": [
6
+ "pretraining_train"
7
+ ],
8
+ "vocabulary_sha256": "918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b",
9
+ "vocabulary_size": 2114
10
+ },
11
+ "checkpoint_resume_authorized": false,
12
+ "downstream_evaluation_authorized": false,
13
+ "gate_audit": {
14
+ "canonical_wurcs_mismatch": 0,
15
+ "duplicate_ids": 0,
16
+ "gate_rows": 285050,
17
+ "legitimate_extra_gate_ids": 3842,
18
+ "missing_sequence_parents": 0,
19
+ "sequence_parents": 281208
20
+ },
21
+ "gpu_launch_authorized": false,
22
+ "nova_submission_authorized": false,
23
+ "official_submitted_or_predecessor_artifacts_modified": false,
24
+ "output_payloads": {
25
+ "review_attention_mask.npy": {
26
+ "bytes": 71989376,
27
+ "name": "review_attention_mask.npy",
28
+ "sha256": "01ed165720b9414e614dd7e7d75284a8886165f11138e16117ffcbe4e4bbf95c"
29
+ },
30
+ "review_bpe_ambiguity_tokens.json": {
31
+ "bytes": 19693,
32
+ "name": "review_bpe_ambiguity_tokens.json",
33
+ "sha256": "22d2aad45b978988c9e26bc9400016014ee825cc6207377385a8cd74f3c88f2b"
34
+ },
35
+ "review_bpe_fit_contributors.tsv.gz": {
36
+ "bytes": 9934117,
37
+ "name": "review_bpe_fit_contributors.tsv.gz",
38
+ "sha256": "4ec8dfaeacef355092313507ab71c628f48ddf1a4fe39e7663748060a07af297"
39
+ },
40
+ "review_bpe_vocabulary.json": {
41
+ "bytes": 138150,
42
+ "name": "review_bpe_vocabulary.json",
43
+ "sha256": "918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b"
44
+ },
45
+ "review_branch_depths.npy": {
46
+ "bytes": 71989376,
47
+ "name": "review_branch_depths.npy",
48
+ "sha256": "f39f76aa56a9bb782b2349f3ad1ca4ad16351ee1f06a8b9ae4b06bff9b1ebece"
49
+ },
50
+ "review_build_official_sequence_topology_successor.py": {
51
+ "bytes": 16295,
52
+ "name": "review_build_official_sequence_topology_successor.py",
53
+ "sha256": "2e84b03ab2189f009654e97c427930cc511c50d079db06f425035fcfb2ae1efb"
54
+ },
55
+ "review_frozen_sequence_tree_runtime.py": {
56
+ "bytes": 11405,
57
+ "name": "review_frozen_sequence_tree_runtime.py",
58
+ "sha256": "10ca4b121d2f22d7a64208e4f05172e7a756b653d0828c9dcc49dd8ea1a5ad04"
59
+ },
60
+ "review_frozen_training_a5_topology.py": {
61
+ "bytes": 6186,
62
+ "name": "review_frozen_training_a5_topology.py",
63
+ "sha256": "f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7"
64
+ },
65
+ "review_frozen_training_a5_wurcs_bpe.py": {
66
+ "bytes": 14161,
67
+ "name": "review_frozen_training_a5_wurcs_bpe.py",
68
+ "sha256": "6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4"
69
+ },
70
+ "review_full_untruncated_lengths.npy": {
71
+ "bytes": 562544,
72
+ "name": "review_full_untruncated_lengths.npy",
73
+ "sha256": "6d83b42630bd8c5d355378b423b3a0021938741b5faa711ecc129a6789e307c2"
74
+ },
75
+ "review_lengths.npy": {
76
+ "bytes": 562544,
77
+ "name": "review_lengths.npy",
78
+ "sha256": "ade919d1e054c0ba852aa047bf873ac675f81a6c1f8a05cc73a0a2258131f021"
79
+ },
80
+ "review_linkage_types.npy": {
81
+ "bytes": 71989376,
82
+ "name": "review_linkage_types.npy",
83
+ "sha256": "89edb1940765928ced6f54e0d394f95c573eb9d96310b14dbd0adf8e20ac07c4"
84
+ },
85
+ "review_owner_clean_build_report.json": {
86
+ "bytes": 3169,
87
+ "name": "review_owner_clean_build_report.json",
88
+ "sha256": "2c24a655f009c6994756d8199b26c3e6489222c4b26d90efd0c43aacc17b9e4c"
89
+ },
90
+ "review_residue_ids.npy": {
91
+ "bytes": 143978624,
92
+ "name": "review_residue_ids.npy",
93
+ "sha256": "36c96dcd3e03ba30e02e16f98d17605742627a37b37cd7824f2a745dd09dc044"
94
+ },
95
+ "review_sequence_objective_eligible.npy": {
96
+ "bytes": 281336,
97
+ "name": "review_sequence_objective_eligible.npy",
98
+ "sha256": "62502d0d1b44a10989350a78f1e033d271fe8417715e10d42eb47c6720ceb3bf"
99
+ },
100
+ "review_sequence_parent_index.tsv.gz": {
101
+ "bytes": 14697953,
102
+ "name": "review_sequence_parent_index.tsv.gz",
103
+ "sha256": "2c39e9b26bb41a1b9317c59372414f69812ead8676b0c33b23d2676e444a48b0"
104
+ },
105
+ "review_token_categories.npy": {
106
+ "bytes": 71989376,
107
+ "name": "review_token_categories.npy",
108
+ "sha256": "2d51df879382deb4927a446937983fea5a07fbbc783181f7903546222b856205"
109
+ },
110
+ "review_token_ids.npy": {
111
+ "bytes": 143978624,
112
+ "name": "review_token_ids.npy",
113
+ "sha256": "ec2d52dced65d22f0f218ff617d351fbd2930661bc8681e939990149d493537d"
114
+ },
115
+ "review_topology_lengths.npy": {
116
+ "bytes": 93175,
117
+ "name": "review_topology_lengths.npy",
118
+ "sha256": "b52f6ddcf327b8cfc5159659bada97357216f9526b42c230a35192932f43d2f3"
119
+ },
120
+ "review_topology_offsets.npy": {
121
+ "bytes": 744512,
122
+ "name": "review_topology_offsets.npy",
123
+ "sha256": "e06d61b8e47c2a2a3e0c8aeec7e015e673f64f0f483a483abe40ccda2d08b9e0"
124
+ },
125
+ "review_topology_roster.tsv": {
126
+ "bytes": 3890681,
127
+ "name": "review_topology_roster.tsv",
128
+ "sha256": "a216ce485ea699fb1b66ed6915175a49cde8c7d14279dbaf835f4675bc7c7fa1"
129
+ },
130
+ "review_topology_row.npy": {
131
+ "bytes": 1124960,
132
+ "name": "review_topology_row.npy",
133
+ "sha256": "a0ae28b0476322e3dbeda42a891f4a42c0973002a5d8507bf387b12dcbff4e95"
134
+ },
135
+ "review_topology_values.npy": {
136
+ "bytes": 5027730,
137
+ "name": "review_topology_values.npy",
138
+ "sha256": "2d4ba8395f78864b7eed9ed250bb9fd50bd45ef84a10ed497ffb89c684276e4e"
139
+ },
140
+ "review_tree_channels_valid.npy": {
141
+ "bytes": 281336,
142
+ "name": "review_tree_channels_valid.npy",
143
+ "sha256": "d15b160afe92a91a4a3d1b85b4a0353672b217b1d5c54513f3c80eab29c6b7c5"
144
+ }
145
+ },
146
+ "parent_count": 281208,
147
+ "rooted_valid_count": 157652,
148
+ "schema": "bertose_a5_official_array_sequence_topology_successor_v1",
149
+ "scientific_launch_authorized": false,
150
+ "sequence_objective_roles": {
151
+ "pretraining_development": 55440,
152
+ "pretraining_train": 221827
153
+ },
154
+ "source_bindings": {
155
+ "ambiguity": "22d2aad45b978988c9e26bc9400016014ee825cc6207377385a8cd74f3c88f2b",
156
+ "builder": "2e84b03ab2189f009654e97c427930cc511c50d079db06f425035fcfb2ae1efb",
157
+ "fit": "4ec8dfaeacef355092313507ab71c628f48ddf1a4fe39e7663748060a07af297",
158
+ "gate": "5fb976a4b3440eb60b52bce9e1932d197f90bf3e074c6e8438bfe92b93c0c7da",
159
+ "helper": "a791b8f4bbda75317f41afb34d62fe518448f4d5d90db0b852e2b0d0af5e83a6",
160
+ "index": "3b9e9f6b4671448cea70a3c16717dc6538c01c59ebfce478ce274e133e563ac3",
161
+ "input_all": "27da2fa1513e77e7f1ced2b5461299d9a997138a643db99f5b2de704bec7a746",
162
+ "molwurcs": "bc38e44e66daed7ec04f7c7907a13509aac125a8a09c97d9972725ab4278edaf",
163
+ "owner_report": "2c24a655f009c6994756d8199b26c3e6489222c4b26d90efd0c43aacc17b9e4c",
164
+ "partition": "86a8c6742156683d502c93fc71b31b9a93450037d647e5a90db437a015bb3bbc",
165
+ "pretokenizer": "6591b854962c278dc797981c942e114c18a26e97af43029c1d539bae3badb2f4",
166
+ "raw": "d69583e2499f775ded3f18baaee843de4dd7c424e2ce5780ad89acab517d85fc",
167
+ "topology_source": "f48ba266abf30a7a1298dea747676da64d53acef4ef02cba7ac8f16509460fc7",
168
+ "tree_runtime": "10ca4b121d2f22d7a64208e4f05172e7a756b653d0828c9dcc49dd8ea1a5ad04",
169
+ "vocabulary": "918c73a55adeb603614112d75bb02033e5823fd9c8a70d43f6c0a4a9c78e360b",
170
+ "wurcsframework": "5449f9276a7b952adf087468c874bb13c7295062cce7d1afb13fdea78ace7f37"
171
+ },
172
+ "status": "PASS_PREPARATION_ONLY_OWNER_CLEAN_BUILD_BYTE_MATCHED",
173
+ "topology_roles_including_excluded_audit": {
174
+ "excluded_exact_or_verified_graph_identity": 3483,
175
+ "pretraining_development": 17650,
176
+ "pretraining_train": 71914
177
+ },
178
+ "topology_storage": {
179
+ "legacy_dense_width26_rejected": true,
180
+ "max_residue_count": 48,
181
+ "row_count": 93047,
182
+ "schema": "ragged_original_residue_axis_bfs_int8_offsets_uint64",
183
+ "value_count": 5027602
184
+ },
185
+ "training_authorized": false
186
+ }
releases/2026-10-02/bertose_source/model/glycan_bert.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Glycan BERT Model
3
+
4
+ Transformer-based masked language model for glycan structures.
5
+ Based on BERT/ESM2 architecture adapted for atomic glycan tokenization.
6
+ """
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import math
11
+
12
+
13
+ class GlycanBERTConfig:
14
+ """Configuration for GlycanBERT."""
15
+
16
+ def __init__(
17
+ self,
18
+ vocab_size: int = 102,
19
+ hidden_size: int = 384,
20
+ num_hidden_layers: int = 6,
21
+ num_attention_heads: int = 6,
22
+ intermediate_size: int = 1536,
23
+ hidden_dropout_prob: float = 0.1,
24
+ attention_probs_dropout_prob: float = 0.1,
25
+ max_position_embeddings: int = 512,
26
+ layer_norm_eps: float = 1e-12,
27
+ pad_token_id: int = 0,
28
+ mask_token_id: int = 4,
29
+ initializer_range: float = 0.02
30
+ ):
31
+ self.vocab_size = vocab_size
32
+ self.hidden_size = hidden_size
33
+ self.num_hidden_layers = num_hidden_layers
34
+ self.num_attention_heads = num_attention_heads
35
+ self.intermediate_size = intermediate_size
36
+ self.hidden_dropout_prob = hidden_dropout_prob
37
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
38
+ self.max_position_embeddings = max_position_embeddings
39
+ self.layer_norm_eps = layer_norm_eps
40
+ self.pad_token_id = pad_token_id
41
+ self.mask_token_id = mask_token_id
42
+ self.initializer_range = initializer_range
43
+
44
+
45
+ class GlycanBERTEmbeddings(nn.Module):
46
+ """
47
+ Embeddings for glycan tokens including token and positional embeddings.
48
+ """
49
+
50
+ def __init__(self, config: GlycanBERTConfig):
51
+ super().__init__()
52
+ self.token_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
53
+ self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
54
+
55
+ self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
56
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
57
+
58
+ # position_ids (1, max_seq_len) is contiguous in memory and exported when serialized
59
+ self.register_buffer("position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)))
60
+
61
+ def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
62
+ """
63
+ Args:
64
+ input_ids: Tensor of shape (batch_size, seq_len)
65
+
66
+ Returns:
67
+ Embeddings of shape (batch_size, seq_len, hidden_size)
68
+ """
69
+ batch_size, seq_len = input_ids.shape
70
+
71
+ # Token embeddings
72
+ token_embeds = self.token_embeddings(input_ids)
73
+
74
+ # Position embeddings
75
+ position_ids = self.position_ids[:, :seq_len]
76
+ position_embeds = self.position_embeddings(position_ids)
77
+
78
+ # Combine
79
+ embeddings = token_embeds + position_embeds
80
+ embeddings = self.LayerNorm(embeddings)
81
+ embeddings = self.dropout(embeddings)
82
+
83
+ return embeddings
84
+
85
+
86
+ class GlycanBERTAttention(nn.Module):
87
+ """Multi-head self-attention."""
88
+
89
+ def __init__(self, config: GlycanBERTConfig):
90
+ super().__init__()
91
+ assert config.hidden_size % config.num_attention_heads == 0
92
+
93
+ self.num_attention_heads = config.num_attention_heads
94
+ self.attention_head_size = config.hidden_size // config.num_attention_heads
95
+ self.all_head_size = self.num_attention_heads * self.attention_head_size
96
+
97
+ self.query = nn.Linear(config.hidden_size, self.all_head_size)
98
+ self.key = nn.Linear(config.hidden_size, self.all_head_size)
99
+ self.value = nn.Linear(config.hidden_size, self.all_head_size)
100
+
101
+ self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
102
+
103
+ def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor:
104
+ """Reshape for multi-head attention."""
105
+ new_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
106
+ x = x.view(*new_shape)
107
+ return x.permute(0, 2, 1, 3) # (batch, heads, seq_len, head_size)
108
+
109
+ def forward(
110
+ self,
111
+ hidden_states: torch.Tensor,
112
+ attention_mask: torch.Tensor = None
113
+ ) -> torch.Tensor:
114
+ """
115
+ Args:
116
+ hidden_states: (batch_size, seq_len, hidden_size)
117
+ attention_mask: (batch_size, seq_len) - 1 for valid, 0 for padding
118
+
119
+ Returns:
120
+ Attention output: (batch_size, seq_len, hidden_size)
121
+ """
122
+ batch_size, seq_len, _ = hidden_states.shape
123
+
124
+ # Linear projections
125
+ query_layer = self.transpose_for_scores(self.query(hidden_states))
126
+ key_layer = self.transpose_for_scores(self.key(hidden_states))
127
+ value_layer = self.transpose_for_scores(self.value(hidden_states))
128
+
129
+ # Attention scores
130
+ attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
131
+ attention_scores = attention_scores / math.sqrt(self.attention_head_size)
132
+
133
+ # Apply attention mask
134
+ if attention_mask is not None:
135
+ # Convert mask to additive mask
136
+ attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) # (batch, 1, 1, seq_len)
137
+ attention_mask = (1.0 - attention_mask) * -10000.0
138
+ attention_scores = attention_scores + attention_mask
139
+
140
+ # Attention probabilities
141
+ attention_probs = nn.functional.softmax(attention_scores, dim=-1)
142
+ attention_probs = self.dropout(attention_probs)
143
+
144
+ # Apply attention to values
145
+ context_layer = torch.matmul(attention_probs, value_layer)
146
+
147
+ # Reshape back
148
+ context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
149
+ new_shape = context_layer.size()[:-2] + (self.all_head_size,)
150
+ context_layer = context_layer.view(*new_shape)
151
+
152
+ return context_layer
153
+
154
+
155
+ class GlycanBERTLayer(nn.Module):
156
+ """Single transformer layer."""
157
+
158
+ def __init__(self, config: GlycanBERTConfig):
159
+ super().__init__()
160
+ self.attention = GlycanBERTAttention(config)
161
+ self.attention_output = nn.Linear(config.hidden_size, config.hidden_size)
162
+ self.attention_layer_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
163
+
164
+ self.intermediate = nn.Linear(config.hidden_size, config.intermediate_size)
165
+ self.output = nn.Linear(config.intermediate_size, config.hidden_size)
166
+ self.output_layer_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
167
+
168
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
169
+
170
+ def forward(
171
+ self,
172
+ hidden_states: torch.Tensor,
173
+ attention_mask: torch.Tensor = None
174
+ ) -> torch.Tensor:
175
+ """
176
+ Args:
177
+ hidden_states: (batch_size, seq_len, hidden_size)
178
+ attention_mask: (batch_size, seq_len)
179
+
180
+ Returns:
181
+ Output: (batch_size, seq_len, hidden_size)
182
+ """
183
+ # Self-attention
184
+ attention_output = self.attention(hidden_states, attention_mask)
185
+ attention_output = self.attention_output(attention_output)
186
+ attention_output = self.dropout(attention_output)
187
+
188
+ # Add & Norm
189
+ hidden_states = self.attention_layer_norm(hidden_states + attention_output)
190
+
191
+ # Feed-forward
192
+ intermediate_output = self.intermediate(hidden_states)
193
+ intermediate_output = nn.functional.gelu(intermediate_output)
194
+
195
+ layer_output = self.output(intermediate_output)
196
+ layer_output = self.dropout(layer_output)
197
+
198
+ # Add & Norm
199
+ layer_output = self.output_layer_norm(hidden_states + layer_output)
200
+
201
+ return layer_output
202
+
203
+
204
+ class GlycanBERT(nn.Module):
205
+ """
206
+ Glycan BERT model for masked language modeling.
207
+ """
208
+
209
+ def __init__(self, config: GlycanBERTConfig):
210
+ super().__init__()
211
+ self.config = config
212
+
213
+ # Embeddings
214
+ self.embeddings = GlycanBERTEmbeddings(config)
215
+
216
+ # Transformer layers
217
+ self.layers = nn.ModuleList([GlycanBERTLayer(config) for _ in range(config.num_hidden_layers)])
218
+
219
+ # MLM head
220
+ self.mlm_head = nn.Linear(config.hidden_size, config.vocab_size)
221
+
222
+ # Initialize weights
223
+ self.apply(self._init_weights)
224
+
225
+ def _init_weights(self, module):
226
+ """Initialize weights."""
227
+ if isinstance(module, nn.Linear):
228
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
229
+ if module.bias is not None:
230
+ module.bias.data.zero_()
231
+ elif isinstance(module, nn.Embedding):
232
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
233
+ if module.padding_idx is not None:
234
+ module.weight.data[module.padding_idx].zero_()
235
+ elif isinstance(module, nn.LayerNorm):
236
+ module.bias.data.zero_()
237
+ module.weight.data.fill_(1.0)
238
+
239
+ def forward(
240
+ self,
241
+ input_ids: torch.Tensor,
242
+ attention_mask: torch.Tensor = None,
243
+ labels: torch.Tensor = None
244
+ ):
245
+ """
246
+ Args:
247
+ input_ids: (batch_size, seq_len)
248
+ attention_mask: (batch_size, seq_len) - 1 for valid, 0 for padding
249
+ labels: (batch_size, seq_len) - token IDs to predict, -100 for positions to ignore
250
+
251
+ Returns:
252
+ If labels provided: (loss, logits)
253
+ Else: logits
254
+ """
255
+ # Create attention mask if not provided
256
+ if attention_mask is None:
257
+ attention_mask = (input_ids != self.config.pad_token_id).float()
258
+
259
+ # Embeddings
260
+ hidden_states = self.embeddings(input_ids)
261
+
262
+ # Transformer layers
263
+ for layer in self.layers:
264
+ hidden_states = layer(hidden_states, attention_mask)
265
+
266
+ # MLM prediction
267
+ logits = self.mlm_head(hidden_states)
268
+
269
+ # Calculate loss if labels provided
270
+ loss = None
271
+ if labels is not None:
272
+ loss_fct = nn.CrossEntropyLoss() # -100 is ignored
273
+ loss = loss_fct(logits.view(-1, self.config.vocab_size), labels.view(-1))
274
+
275
+ if loss is not None:
276
+ return loss, logits
277
+ return logits
278
+
279
+ def get_embeddings(
280
+ self,
281
+ input_ids: torch.Tensor,
282
+ attention_mask: torch.Tensor = None
283
+ ) -> torch.Tensor:
284
+ """
285
+ Get contextualized embeddings (for downstream tasks).
286
+
287
+ Args:
288
+ input_ids: (batch_size, seq_len)
289
+ attention_mask: (batch_size, seq_len)
290
+
291
+ Returns:
292
+ Embeddings: (batch_size, seq_len, hidden_size)
293
+ """
294
+ if attention_mask is None:
295
+ attention_mask = (input_ids != self.config.pad_token_id).float()
296
+
297
+ hidden_states = self.embeddings(input_ids)
298
+
299
+ for layer in self.layers:
300
+ hidden_states = layer(hidden_states, attention_mask)
301
+
302
+ return hidden_states
303
+
releases/2026-10-02/bertose_source/model/multimodal_glycan_bert_v3.py ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multimodal Glycan BERT Model v3
3
+
4
+ Extends GlycanBERT to handle three modalities:
5
+ - Sequence (WURCS atomic tokenization)
6
+ - MS (mass spectrometry peaks, RT, intensity)
7
+ - 3D structure (VQ-VAE discrete tokens, 4 per residue)
8
+
9
+ Each modality has its own encoder, with cross-attention for sequence-structure alignment.
10
+ """
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ from typing import Dict, Optional, Tuple
15
+ import math
16
+
17
+ try:
18
+ from .glycan_bert import GlycanBERTConfig, GlycanBERTEmbeddings, GlycanBERTLayer
19
+ except ImportError:
20
+ from glycan_bert import GlycanBERTConfig, GlycanBERTEmbeddings, GlycanBERTLayer
21
+
22
+
23
+ class ConvGlycanBERTEmbeddings(nn.Module):
24
+ """
25
+ Improved Convolutional front-end that mixes local WURCS context before the Transformer.
26
+
27
+ Key improvements over original:
28
+ 1. Position embeddings added BEFORE convolution (provides spatial context to conv)
29
+ 2. Residual connection (conv enriches embeddings rather than replacing them)
30
+ 3. Multi-scale convolutions (kernel sizes 3, 5, 7) for better receptive field
31
+ 4. Proper layer normalization on the residual path
32
+ """
33
+
34
+ def __init__(self, config):
35
+ super().__init__()
36
+ self.token_embeddings = nn.Embedding(
37
+ config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id
38
+ )
39
+ self.position_embeddings = nn.Embedding(
40
+ config.max_position_embeddings, config.hidden_size
41
+ )
42
+
43
+ # NEW: Branch depth embeddings - encodes depth in glycan tree (0=root, 1=child, etc.)
44
+ max_branch_depth = getattr(config, "max_branch_depth", 8)
45
+ self.branch_embeddings = nn.Embedding(max_branch_depth, config.hidden_size)
46
+
47
+ # NEW: Linkage type embeddings - encodes chemistry of glycosidic bond
48
+ # 0=none, 1=1-3, 2=1-4, 3=1-6, etc.
49
+ num_linkage_types = getattr(config, "num_linkage_types", 9)
50
+ self.linkage_embeddings = nn.Embedding(num_linkage_types, config.hidden_size)
51
+
52
+ # Multi-scale convolutions for different receptive fields
53
+ kernel_size = getattr(config, "cnn_kernel_size", 3)
54
+ # Split channels evenly: 256 + 256 + 256 = 768 for hidden_size=768
55
+ channels_per_scale = config.hidden_size // 3
56
+ self.conv_layers = nn.ModuleList([
57
+ nn.Conv1d(
58
+ in_channels=config.hidden_size,
59
+ out_channels=channels_per_scale,
60
+ kernel_size=kernel_size + 2 * i, # Kernels: 3, 5, 7
61
+ padding=(kernel_size + 2 * i) // 2, # Same padding
62
+ )
63
+ for i in range(3)
64
+ ])
65
+ self.conv_activation = nn.GELU()
66
+ self.conv_proj = nn.Linear(channels_per_scale * 3, config.hidden_size) # Project concatenated back
67
+
68
+ self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
69
+ self.conv_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
70
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
71
+ self.register_buffer(
72
+ "position_ids",
73
+ torch.arange(config.max_position_embeddings).expand((1, -1)),
74
+ )
75
+
76
+ self.hidden_size = config.hidden_size
77
+
78
+ def forward(self, input_ids, branch_depths=None, linkage_types=None):
79
+ seq_len = input_ids.shape[1]
80
+
81
+ # Step 1: Token + Position embeddings FIRST (provides spatial context to conv)
82
+ x = self.token_embeddings(input_ids) # (batch, seq, hidden)
83
+ position_ids = self.position_ids[:, :seq_len]
84
+ x = x + self.position_embeddings(position_ids)
85
+
86
+ # NEW: Add branch depth embeddings (encodes tree structure)
87
+ if branch_depths is not None:
88
+ # Clamp to valid range
89
+ branch_depths = branch_depths.clamp(0, self.branch_embeddings.num_embeddings - 1)
90
+ x = x + self.branch_embeddings(branch_depths)
91
+
92
+ # NEW: Add linkage type embeddings (encodes bond chemistry)
93
+ if linkage_types is not None:
94
+ linkage_types = linkage_types.clamp(0, self.linkage_embeddings.num_embeddings - 1)
95
+ x = x + self.linkage_embeddings(linkage_types)
96
+
97
+ x = self.LayerNorm(x)
98
+
99
+ # Step 2: Multi-scale convolution with RESIDUAL connection
100
+ # Convolution expects (batch, hidden, seq)
101
+ conv_in = x.permute(0, 2, 1)
102
+
103
+ # Apply multi-scale convolutions and concatenate
104
+ conv_outputs = []
105
+ for conv in self.conv_layers:
106
+ conv_out = self.conv_activation(conv(conv_in))
107
+ conv_outputs.append(conv_out)
108
+
109
+ # Concatenate multi-scale features and project back
110
+ conv_out = torch.cat(conv_outputs, dim=1) # (batch, hidden, seq)
111
+ conv_out = conv_out.permute(0, 2, 1) # (batch, seq, hidden)
112
+ conv_out = self.conv_proj(conv_out) # Project to correct size
113
+
114
+ # Step 3: Residual connection - conv ENRICHES rather than replaces
115
+ x = self.conv_norm(x + self.dropout(conv_out))
116
+
117
+ return x
118
+
119
+
120
+ def create_residue_level_mask(
121
+ seq_residue_ids: torch.Tensor, # (batch, N_seq)
122
+ struct_residue_ids: torch.Tensor # (batch, N_struct)
123
+ ) -> torch.Tensor:
124
+ """
125
+ Create residue-level attention mask for cross-attention.
126
+
127
+ Maps WURCS tokens to VQ-VAE structural tokens based on residue IDs.
128
+ A WURCS token with residue_id=0 can only attend to VQ-VAE tokens with residue_id=0.
129
+
130
+ Args:
131
+ seq_residue_ids: Residue IDs for sequence tokens (batch, N_seq)
132
+ struct_residue_ids: Residue IDs for structural tokens (batch, N_struct)
133
+
134
+ Returns:
135
+ Boolean mask (batch, N_seq, N_struct) where True = can attend
136
+ """
137
+ # Expand dimensions for broadcasting
138
+ # seq: (batch, N_seq, 1)
139
+ # struct: (batch, 1, N_struct)
140
+ mask = seq_residue_ids.unsqueeze(2) == struct_residue_ids.unsqueeze(1)
141
+ # Shape: (batch, N_seq, N_struct)
142
+
143
+ # Mask out structural tokens (residue_id = -1) and MS tokens (residue_id = -2)
144
+ # Only tokens with residue_id >= 0 can attend
145
+ mask &= (seq_residue_ids.unsqueeze(2) >= 0)
146
+
147
+ return mask # True = can attend, False = cannot attend
148
+
149
+
150
+ class MultimodalGlycanBERTConfig:
151
+ """Configuration for Multimodal GlycanBERT v3."""
152
+
153
+ def __init__(
154
+ self,
155
+ # Sequence modality
156
+ seq_vocab_size: int = 166,
157
+ seq_hidden_size: int = 768,
158
+ seq_num_layers: int = 12,
159
+ seq_num_heads: int = 12,
160
+ seq_max_length: int = 512,
161
+
162
+ # MS modality
163
+ ms_vocab_size: int = 242,
164
+ ms_hidden_size: int = 384,
165
+ ms_num_layers: int = 6,
166
+ ms_num_heads: int = 6,
167
+ ms_max_length: int = 150,
168
+
169
+ # 3D structure modality
170
+ struct_vocab_size: int = 1024, # VQ-VAE codebook size
171
+ struct_hidden_size: int = 512,
172
+ struct_num_layers: int = 8,
173
+ struct_num_heads: int = 8,
174
+ struct_max_length: int = 200,
175
+ use_3d: bool = True,
176
+
177
+ # Cross-attention
178
+ use_cross_attention: bool = True,
179
+ cross_attn_num_heads: int = 8,
180
+
181
+ # Fusion
182
+ fusion_hidden_size: int = 768,
183
+ fusion_num_layers: int = 2,
184
+
185
+ # Training
186
+ hidden_dropout_prob: float = 0.1,
187
+ attention_probs_dropout_prob: float = 0.1,
188
+ layer_norm_eps: float = 1e-12,
189
+ initializer_range: float = 0.02,
190
+
191
+ # Conv front-end
192
+ use_cnn_frontend: bool = True,
193
+ cnn_kernel_size: int = 3,
194
+
195
+ # Loss weights
196
+ seq_loss_weight: float = 0.60,
197
+ ms_loss_weight: float = 0.15,
198
+ struct_loss_weight: float = 0.25,
199
+ dist_loss_weight: float = 0.25,
200
+
201
+ # Token IDs
202
+ pad_token_id: int = 0,
203
+ mask_token_id: int = 1,
204
+ ):
205
+ # Sequence config
206
+ self.seq_vocab_size = seq_vocab_size
207
+ self.seq_hidden_size = seq_hidden_size
208
+ self.seq_num_layers = seq_num_layers
209
+ self.seq_num_heads = seq_num_heads
210
+ self.seq_max_length = seq_max_length
211
+
212
+ # MS config
213
+ self.ms_vocab_size = ms_vocab_size
214
+ self.ms_vocab_offset = seq_vocab_size # MS tokens start at 166
215
+ self.ms_total_vocab_size = seq_vocab_size + ms_vocab_size # 408 total
216
+ self.ms_hidden_size = ms_hidden_size
217
+ self.ms_num_layers = ms_num_layers
218
+ self.ms_num_heads = ms_num_heads
219
+ self.ms_max_length = ms_max_length
220
+
221
+ # Structure config
222
+ self.struct_vocab_size = struct_vocab_size
223
+ self.struct_hidden_size = struct_hidden_size
224
+ self.struct_num_layers = struct_num_layers
225
+ self.struct_num_heads = struct_num_heads
226
+ self.struct_max_length = struct_max_length
227
+ self.use_3d = use_3d
228
+
229
+ # Cross-attention config
230
+ self.use_cross_attention = use_cross_attention
231
+ self.cross_attn_num_heads = cross_attn_num_heads
232
+
233
+ # Fusion config
234
+ self.fusion_hidden_size = fusion_hidden_size
235
+ self.fusion_num_layers = fusion_num_layers
236
+
237
+ # Training config
238
+ self.hidden_dropout_prob = hidden_dropout_prob
239
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
240
+ self.layer_norm_eps = layer_norm_eps
241
+ self.initializer_range = initializer_range
242
+
243
+ # Conv front-end
244
+ self.use_cnn_frontend = use_cnn_frontend
245
+ self.cnn_kernel_size = cnn_kernel_size
246
+
247
+ # Loss weights
248
+ self.seq_loss_weight = seq_loss_weight
249
+ self.ms_loss_weight = ms_loss_weight
250
+ self.struct_loss_weight = struct_loss_weight
251
+ self.dist_loss_weight = dist_loss_weight
252
+
253
+ # Token IDs
254
+ self.pad_token_id = pad_token_id
255
+ self.mask_token_id = mask_token_id
256
+
257
+ def to_seq_config(self) -> GlycanBERTConfig:
258
+ """Convert to sequence-only config."""
259
+ return GlycanBERTConfig(
260
+ vocab_size=self.seq_vocab_size,
261
+ hidden_size=self.seq_hidden_size,
262
+ num_hidden_layers=self.seq_num_layers,
263
+ num_attention_heads=self.seq_num_heads,
264
+ intermediate_size=self.seq_hidden_size * 4,
265
+ hidden_dropout_prob=self.hidden_dropout_prob,
266
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
267
+ max_position_embeddings=self.seq_max_length,
268
+ layer_norm_eps=self.layer_norm_eps,
269
+ pad_token_id=self.pad_token_id,
270
+ mask_token_id=self.mask_token_id,
271
+ initializer_range=self.initializer_range,
272
+ )
273
+
274
+ def to_ms_config(self) -> GlycanBERTConfig:
275
+ """Convert to MS-only config."""
276
+ return GlycanBERTConfig(
277
+ vocab_size=self.ms_total_vocab_size,
278
+ hidden_size=self.ms_hidden_size,
279
+ num_hidden_layers=self.ms_num_layers,
280
+ num_attention_heads=self.ms_num_heads,
281
+ intermediate_size=self.ms_hidden_size * 4,
282
+ hidden_dropout_prob=self.hidden_dropout_prob,
283
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
284
+ max_position_embeddings=self.ms_max_length,
285
+ layer_norm_eps=self.layer_norm_eps,
286
+ pad_token_id=self.pad_token_id,
287
+ mask_token_id=self.mask_token_id,
288
+ initializer_range=self.initializer_range,
289
+ )
290
+
291
+ def to_struct_config(self) -> GlycanBERTConfig:
292
+ """Convert to structure-only config."""
293
+ return GlycanBERTConfig(
294
+ vocab_size=self.struct_vocab_size,
295
+ hidden_size=self.struct_hidden_size,
296
+ num_hidden_layers=self.struct_num_layers,
297
+ num_attention_heads=self.struct_num_heads,
298
+ intermediate_size=self.struct_hidden_size * 4,
299
+ hidden_dropout_prob=self.hidden_dropout_prob,
300
+ attention_probs_dropout_prob=self.attention_probs_dropout_prob,
301
+ max_position_embeddings=self.struct_max_length,
302
+ layer_norm_eps=self.layer_norm_eps,
303
+ pad_token_id=self.pad_token_id,
304
+ mask_token_id=self.mask_token_id,
305
+ initializer_range=self.initializer_range,
306
+ )
307
+
308
+
309
+ # =============================================================================
310
+ # Improvement #1: Monosaccharide-Level Pooling
311
+ # =============================================================================
312
+
313
+ class MonosaccharidePooling(nn.Module):
314
+ """
315
+ Pool token representations to monosaccharide level, then aggregate.
316
+
317
+ This bridges the gap between token-level BERT and monosaccharide-level CNNs/GNNs.
318
+ Uses monosaccharide_indices from the data to know where each residue starts.
319
+ """
320
+
321
+ def __init__(self, hidden_size: int, num_attention_heads: int = 8, dropout: float = 0.1):
322
+ super().__init__()
323
+ self.hidden_size = hidden_size
324
+
325
+ # Attention pooling over monosaccharide representations
326
+ self.mono_attention = nn.MultiheadAttention(
327
+ embed_dim=hidden_size,
328
+ num_heads=num_attention_heads,
329
+ dropout=dropout,
330
+ batch_first=True
331
+ )
332
+ self.mono_norm = nn.LayerNorm(hidden_size)
333
+
334
+ # Final aggregation to single glycan representation
335
+ self.glycan_query = nn.Parameter(torch.randn(1, 1, hidden_size) * 0.02)
336
+ self.glycan_attention = nn.MultiheadAttention(
337
+ embed_dim=hidden_size,
338
+ num_heads=num_attention_heads,
339
+ dropout=dropout,
340
+ batch_first=True
341
+ )
342
+ self.glycan_norm = nn.LayerNorm(hidden_size)
343
+
344
+ def forward(
345
+ self,
346
+ hidden_states: torch.Tensor, # (batch, seq_len, hidden)
347
+ residue_ids: torch.Tensor, # (batch, seq_len) - which residue each token belongs to
348
+ attention_mask: torch.Tensor = None, # (batch, seq_len)
349
+ ) -> torch.Tensor:
350
+ """
351
+ Pool tokens to monosaccharide level, then to glycan level.
352
+
353
+ Returns:
354
+ Glycan representation: (batch, hidden_size)
355
+ """
356
+ batch_size = hidden_states.size(0)
357
+ device = hidden_states.device
358
+
359
+ # Get unique residue IDs per sample (excluding -1 padding)
360
+ max_residues = 50 # Reasonable max for glycans
361
+
362
+ # Pool tokens within each residue using mean pooling
363
+ mono_reps = torch.zeros(batch_size, max_residues, self.hidden_size, device=device)
364
+ mono_mask = torch.zeros(batch_size, max_residues, dtype=torch.bool, device=device)
365
+
366
+ for b in range(batch_size):
367
+ unique_residues = torch.unique(residue_ids[b][residue_ids[b] >= 0])
368
+ for i, rid in enumerate(unique_residues):
369
+ if i >= max_residues:
370
+ break
371
+ token_mask = residue_ids[b] == rid
372
+ if attention_mask is not None:
373
+ token_mask = token_mask & (attention_mask[b] > 0)
374
+ if token_mask.sum() > 0:
375
+ mono_reps[b, i] = hidden_states[b][token_mask].mean(dim=0)
376
+ mono_mask[b, i] = True
377
+
378
+ # Apply attention over monosaccharide representations
379
+ # Convert mask for attention: True = valid, need to invert for PyTorch
380
+ key_padding_mask = ~mono_mask # True = ignore
381
+
382
+ mono_out, _ = self.mono_attention(
383
+ mono_reps, mono_reps, mono_reps,
384
+ key_padding_mask=key_padding_mask
385
+ )
386
+ mono_out = self.mono_norm(mono_reps + mono_out)
387
+
388
+ # Aggregate to single glycan representation using learned query
389
+ glycan_query = self.glycan_query.expand(batch_size, -1, -1)
390
+ glycan_out, _ = self.glycan_attention(
391
+ glycan_query, mono_out, mono_out,
392
+ key_padding_mask=key_padding_mask
393
+ )
394
+ glycan_out = self.glycan_norm(glycan_query + glycan_out)
395
+
396
+ return glycan_out.squeeze(1) # (batch, hidden)
397
+
398
+
399
+ # =============================================================================
400
+ # Improvement #2: Residue Type Embeddings
401
+ # =============================================================================
402
+
403
+ # Common monosaccharide types vocabulary
404
+ MONOSACCHARIDE_VOCAB = {
405
+ '[PAD_MONO]': 0, '[UNK_MONO]': 1,
406
+ 'Glc': 2, 'GlcNAc': 3, 'GlcA': 4, 'GlcN': 5,
407
+ 'Gal': 6, 'GalNAc': 7, 'GalA': 8, 'GalN': 9,
408
+ 'Man': 10, 'ManNAc': 11, 'ManA': 12, 'ManN': 13,
409
+ 'Fuc': 14, 'Rha': 15, 'Xyl': 16, 'Ara': 17,
410
+ 'Neu5Ac': 18, 'Neu5Gc': 19, 'Kdn': 20, 'Sia': 21,
411
+ 'GalNAcA': 22, 'GlcNAcA': 23, 'IdoA': 24, 'GulA': 25,
412
+ 'Rib': 26, 'Lyx': 27, 'All': 28, 'Alt': 29,
413
+ 'Tal': 30, 'Ido': 31, 'Qui': 32, 'Oli': 33,
414
+ 'Tyv': 34, 'Abe': 35, 'Par': 36, 'Dig': 37,
415
+ 'Col': 38, 'Dha': 39, 'Kdo': 40, 'Hep': 41,
416
+ 'NeuroGc': 42, 'Muramic': 43, 'LDManHep': 44, 'DDManHep': 45,
417
+ 'Bac': 46, 'Pse': 47, 'Leg': 48, 'Aci': 49,
418
+ '6dTal': 50, 'Fru': 51, 'Tag': 52, 'Sor': 53,
419
+ 'Psi': 54, 'Sed': 55, 'MurNAc': 56, 'MurNGc': 57,
420
+ 'Api': 58, 'Erwiniose': 59, 'Yer': 60, 'Thre': 61,
421
+ # Add more as needed, up to ~70
422
+ }
423
+
424
+
425
+ class ResidueTypeEmbeddings(nn.Module):
426
+ """
427
+ Learnable embeddings for monosaccharide types.
428
+
429
+ Instead of the model having to learn that 'a1221m' = Fucose from character patterns,
430
+ we explicitly add a Fucose embedding to all tokens belonging to that residue.
431
+ """
432
+
433
+ def __init__(self, hidden_size: int, num_mono_types: int = 70):
434
+ super().__init__()
435
+ self.mono_embeddings = nn.Embedding(num_mono_types, hidden_size)
436
+ self.mono_vocab = MONOSACCHARIDE_VOCAB
437
+ self.hidden_size = hidden_size
438
+
439
+ def forward(
440
+ self,
441
+ token_embeddings: torch.Tensor, # (batch, seq_len, hidden)
442
+ residue_ids: torch.Tensor, # (batch, seq_len)
443
+ mono_type_ids: torch.Tensor = None, # (batch, max_residues) - monosaccharide type per residue
444
+ ) -> torch.Tensor:
445
+ """
446
+ Add residue type embeddings to token embeddings.
447
+
448
+ Args:
449
+ token_embeddings: Base token embeddings
450
+ residue_ids: Which residue each token belongs to (-1 for special tokens)
451
+ mono_type_ids: Monosaccharide type ID for each residue position
452
+
453
+ Returns:
454
+ Enhanced embeddings with residue type information
455
+ """
456
+ if mono_type_ids is None:
457
+ return token_embeddings
458
+
459
+ batch_size, seq_len, _ = token_embeddings.shape
460
+ enhanced = token_embeddings.clone()
461
+
462
+ # Add mono type embedding to each token based on its residue
463
+ for b in range(batch_size):
464
+ for pos in range(seq_len):
465
+ rid = residue_ids[b, pos].item()
466
+ if rid >= 0 and rid < mono_type_ids.size(1):
467
+ mono_id = mono_type_ids[b, rid]
468
+ enhanced[b, pos] = enhanced[b, pos] + self.mono_embeddings(mono_id)
469
+
470
+ return enhanced
471
+
472
+ @staticmethod
473
+ def get_mono_type_id(mono_name: str) -> int:
474
+ """Convert monosaccharide name to type ID."""
475
+ return MONOSACCHARIDE_VOCAB.get(mono_name, MONOSACCHARIDE_VOCAB['[UNK_MONO]'])
476
+
477
+
478
+ # =============================================================================
479
+ # Improvement #4: Relative Position Encoding for Glycan Trees
480
+ # =============================================================================
481
+
482
+ class RelativePositionBias(nn.Module):
483
+ """
484
+ Compute relative position bias for attention based on residue IDs.
485
+
486
+ Tokens in the same residue get distance 0.
487
+ Tokens in adjacent residues get distance ±1.
488
+ This helps the model understand glycan tree structure.
489
+ """
490
+
491
+ def __init__(self, num_heads: int, max_distance: int = 10):
492
+ super().__init__()
493
+ self.num_heads = num_heads
494
+ self.max_distance = max_distance
495
+
496
+ # Learnable bias for each relative distance (-max to +max)
497
+ num_distances = 2 * max_distance + 1
498
+ self.relative_bias = nn.Embedding(num_distances, num_heads)
499
+
500
+ def forward(self, residue_ids: torch.Tensor) -> torch.Tensor:
501
+ """
502
+ Compute relative position bias.
503
+
504
+ Args:
505
+ residue_ids: (batch, seq_len)
506
+
507
+ Returns:
508
+ Bias to add to attention scores: (batch, num_heads, seq_len, seq_len)
509
+ """
510
+ # Compute pairwise residue distances
511
+ # (batch, seq_len, 1) - (batch, 1, seq_len) = (batch, seq_len, seq_len)
512
+ distance = residue_ids.unsqueeze(2) - residue_ids.unsqueeze(1)
513
+
514
+ # Clamp to max distance range and shift to 0-indexed
515
+ distance_clamped = distance.clamp(-self.max_distance, self.max_distance)
516
+ distance_idx = distance_clamped + self.max_distance # Now 0 to 2*max_distance
517
+
518
+ # Look up bias: (batch, seq_len, seq_len, num_heads)
519
+ bias = self.relative_bias(distance_idx)
520
+
521
+ # Transpose to (batch, num_heads, seq_len, seq_len)
522
+ bias = bias.permute(0, 3, 1, 2)
523
+
524
+ return bias
525
+
526
+
527
+ class CrossAttentionLayer(nn.Module):
528
+ """
529
+ Cross-attention layer for sequence-structure alignment.
530
+
531
+ Allows sequence tokens to attend to structural atoms using attention masks.
532
+ """
533
+
534
+ def __init__(self, config: MultimodalGlycanBERTConfig):
535
+ super().__init__()
536
+ self.num_heads = config.cross_attn_num_heads
537
+ self.hidden_size = config.seq_hidden_size
538
+ self.head_dim = self.hidden_size // self.num_heads
539
+
540
+ assert self.hidden_size % self.num_heads == 0, "hidden_size must be divisible by num_heads"
541
+
542
+ # Query from sequence, Key/Value from structure (VQ-VAE tokens)
543
+ self.query = nn.Linear(config.seq_hidden_size, self.hidden_size)
544
+ self.key = nn.Linear(config.struct_hidden_size, self.hidden_size)
545
+ self.value = nn.Linear(config.struct_hidden_size, self.hidden_size)
546
+
547
+ self.output = nn.Linear(self.hidden_size, config.seq_hidden_size)
548
+ self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
549
+ self.layer_norm = nn.LayerNorm(config.seq_hidden_size, eps=config.layer_norm_eps)
550
+
551
+ def forward(
552
+ self,
553
+ seq_hidden: torch.Tensor, # (batch, seq_len, seq_hidden)
554
+ struct_hidden: torch.Tensor, # (batch, struct_len, struct_hidden)
555
+ attention_mask: Optional[torch.Tensor] = None, # (batch, seq_len, struct_len)
556
+ ) -> torch.Tensor:
557
+ """
558
+ Apply cross-attention from sequence to structure.
559
+
560
+ Args:
561
+ seq_hidden: Sequence hidden states
562
+ struct_hidden: Structure hidden states
563
+ attention_mask: Boolean mask (True = can attend, False = cannot attend)
564
+
565
+ Returns:
566
+ Updated sequence hidden states
567
+ """
568
+ batch_size, seq_len, _ = seq_hidden.shape
569
+ struct_len = struct_hidden.shape[1]
570
+
571
+ # Project to Q, K, V
572
+ Q = self.query(seq_hidden) # (batch, seq_len, hidden)
573
+ K = self.key(struct_hidden) # (batch, struct_len, hidden)
574
+ V = self.value(struct_hidden) # (batch, struct_len, hidden)
575
+
576
+ # Reshape for multi-head attention
577
+ Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, seq_len, head_dim)
578
+ K = K.view(batch_size, struct_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, struct_len, head_dim)
579
+ V = V.view(batch_size, struct_len, self.num_heads, self.head_dim).transpose(1, 2) # (batch, heads, struct_len, head_dim)
580
+
581
+ # Compute attention scores
582
+ scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim) # (batch, heads, seq_len, struct_len)
583
+
584
+ # Apply attention mask
585
+ if attention_mask is not None:
586
+ # attention_mask: (batch, seq_len, struct_len) -> (batch, 1, seq_len, struct_len)
587
+ attention_mask = attention_mask.unsqueeze(1)
588
+ # Convert boolean mask to float: True -> 0.0, False -> -10000.0
589
+ attention_mask = (~attention_mask).float() * -10000.0
590
+ scores = scores + attention_mask
591
+
592
+ # Softmax and dropout
593
+ attn_weights = torch.softmax(scores, dim=-1) # (batch, heads, seq_len, struct_len)
594
+ attn_weights = self.dropout(attn_weights)
595
+
596
+ # Apply attention to values
597
+ context = torch.matmul(attn_weights, V) # (batch, heads, seq_len, head_dim)
598
+
599
+ # Reshape back
600
+ context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.hidden_size)
601
+
602
+ # Output projection
603
+ output = self.output(context)
604
+ output = self.dropout(output)
605
+
606
+ # Residual connection + layer norm
607
+ output = self.layer_norm(seq_hidden + output)
608
+
609
+ return output
610
+
611
+
612
+ class MultimodalGlycanBERT(nn.Module):
613
+ """
614
+ Multimodal BERT for glycan representation learning (v3).
615
+
616
+ Architecture:
617
+ 1. Separate encoders for each modality (sequence, MS, 3D structure)
618
+ 2. Cross-attention for sequence-structure alignment
619
+ 3. Modality-specific MLM heads
620
+ 4. Fusion layer for combined representation
621
+ """
622
+
623
+ def __init__(self, config: MultimodalGlycanBERTConfig):
624
+ super().__init__()
625
+ self.config = config
626
+
627
+ # ===== Sequence Encoder =====
628
+ seq_config = config.to_seq_config()
629
+ seq_config.cnn_kernel_size = config.cnn_kernel_size
630
+
631
+ if config.use_cnn_frontend:
632
+ print(f"✅ Enabled Convolutional Front-End (Kernel={config.cnn_kernel_size})")
633
+ self.seq_embeddings = ConvGlycanBERTEmbeddings(seq_config)
634
+ else:
635
+ self.seq_embeddings = GlycanBERTEmbeddings(seq_config)
636
+ self.seq_layers = nn.ModuleList([GlycanBERTLayer(seq_config) for _ in range(seq_config.num_hidden_layers)])
637
+ self.seq_mlm_head = nn.Linear(seq_config.hidden_size, seq_config.vocab_size)
638
+
639
+ # ===== MS Encoder =====
640
+ ms_config = config.to_ms_config()
641
+ self.ms_embeddings = GlycanBERTEmbeddings(ms_config)
642
+ self.ms_layers = nn.ModuleList([GlycanBERTLayer(ms_config) for _ in range(ms_config.num_hidden_layers)])
643
+ self.ms_mlm_head = nn.Linear(ms_config.hidden_size, ms_config.vocab_size)
644
+
645
+ # ===== Structure Encoder (VQ-VAE tokens) =====
646
+ if config.use_3d:
647
+ struct_config = config.to_struct_config()
648
+ self.struct_embeddings = GlycanBERTEmbeddings(struct_config)
649
+ self.struct_layers = nn.ModuleList([GlycanBERTLayer(struct_config) for _ in range(struct_config.num_hidden_layers)])
650
+ self.struct_mlm_head = nn.Linear(struct_config.hidden_size, struct_config.vocab_size)
651
+
652
+ # Cross-attention layer (sequence → VQ-VAE structural tokens)
653
+ if config.use_cross_attention:
654
+ self.cross_attention = CrossAttentionLayer(config)
655
+
656
+ # ===== Projection layers (align hidden sizes) =====
657
+ if config.ms_hidden_size != config.seq_hidden_size:
658
+ self.ms_projection = nn.Linear(config.ms_hidden_size, config.seq_hidden_size)
659
+ else:
660
+ self.ms_projection = nn.Identity()
661
+
662
+ if config.use_3d and config.struct_hidden_size != config.seq_hidden_size:
663
+ self.struct_projection = nn.Linear(config.struct_hidden_size, config.seq_hidden_size)
664
+ else:
665
+ self.struct_projection = nn.Identity()
666
+
667
+ # ===== Fusion Layer =====
668
+ # Concatenate seq + ms + struct
669
+ fusion_input_size = config.seq_hidden_size * (3 if config.use_3d else 2)
670
+ self.fusion_layer = nn.Sequential(
671
+ nn.Linear(fusion_input_size, config.fusion_hidden_size),
672
+ nn.LayerNorm(config.fusion_hidden_size, eps=config.layer_norm_eps),
673
+ nn.GELU(),
674
+ nn.Dropout(config.hidden_dropout_prob),
675
+ nn.Linear(config.fusion_hidden_size, config.fusion_hidden_size),
676
+ )
677
+
678
+ # ===== Distance Prediction Head (Topology) =====
679
+ # OPTIMIZED: Project down to 128 dim first to save GPU memory
680
+ # (Batch, 256, 256, 768) -> (Batch, 256, 256, 128) reduces memory by 6x
681
+ self.dist_proj = nn.Linear(config.seq_hidden_size, 128)
682
+ self.distance_head = nn.Sequential(
683
+ nn.Linear(128, 64),
684
+ nn.ReLU(),
685
+ nn.Linear(64, 1)
686
+ )
687
+
688
+ # Initialize weights
689
+ self.apply(self._init_weights)
690
+
691
+ def _init_weights(self, module):
692
+ """Initialize weights."""
693
+ if isinstance(module, nn.Linear):
694
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
695
+ if module.bias is not None:
696
+ module.bias.data.zero_()
697
+ elif isinstance(module, nn.Embedding):
698
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
699
+ if module.padding_idx is not None:
700
+ module.weight.data[module.padding_idx].zero_()
701
+ elif isinstance(module, nn.LayerNorm):
702
+ module.bias.data.zero_()
703
+ module.weight.data.fill_(1.0)
704
+
705
+ def forward(
706
+ self,
707
+ seq_token_ids: torch.Tensor,
708
+ seq_attention_mask: torch.Tensor,
709
+ seq_residue_ids: torch.Tensor,
710
+ seq_branch_depths: Optional[torch.Tensor] = None, # NEW: Branch depths
711
+ seq_linkage_types: Optional[torch.Tensor] = None, # NEW: Linkage types
712
+ ms_token_ids: torch.Tensor = None,
713
+ ms_attention_mask: torch.Tensor = None,
714
+ has_ms: torch.Tensor = None,
715
+ struct_token_ids: Optional[torch.Tensor] = None,
716
+ struct_attention_mask: Optional[torch.Tensor] = None,
717
+ struct_residue_ids: Optional[torch.Tensor] = None,
718
+ has_3d: Optional[torch.Tensor] = None,
719
+ seq_labels: Optional[torch.Tensor] = None,
720
+ ms_labels: Optional[torch.Tensor] = None,
721
+ struct_labels: Optional[torch.Tensor] = None,
722
+ dist_labels: Optional[torch.Tensor] = None, # NEW: Topology distance labels
723
+ return_dict: bool = True,
724
+ ) -> Dict[str, torch.Tensor]:
725
+ """
726
+ Forward pass for multimodal BERT v3.
727
+
728
+ Args:
729
+ seq_token_ids: (batch_size, seq_len) - Sequence token IDs
730
+ seq_attention_mask: (batch_size, seq_len) - Sequence attention mask
731
+ seq_residue_ids: (batch_size, seq_len) - Sequence token residue IDs
732
+ ms_token_ids: (batch_size, ms_len) - MS token IDs
733
+ ms_attention_mask: (batch_size, ms_len) - MS attention mask
734
+ has_ms: (batch_size,) - Boolean mask for samples with MS data
735
+ struct_token_ids: (batch_size, struct_len) - Structure VQ-VAE token IDs (optional)
736
+ struct_attention_mask: (batch_size, struct_len) - Structure attention mask (optional)
737
+ struct_residue_ids: (batch_size, struct_len) - Structure token residue IDs (optional)
738
+ has_3d: (batch_size,) - Boolean mask for samples with 3D data (optional)
739
+ seq_labels: (batch_size, seq_len) - Masked sequence labels (optional)
740
+ ms_labels: (batch_size, ms_len) - Masked MS labels (optional)
741
+ struct_labels: (batch_size, struct_len) - Masked structure labels (optional)
742
+ return_dict: Whether to return dict or tuple
743
+
744
+ Returns:
745
+ Dictionary containing logits, hidden states, losses, etc.
746
+ """
747
+ batch_size = seq_token_ids.shape[0]
748
+ device = seq_token_ids.device
749
+
750
+ # ===== Sequence Encoder =====
751
+ # Pass branch_depths and linkage_types to embeddings for tree-aware encoding
752
+ seq_hidden = self.seq_embeddings(seq_token_ids, seq_branch_depths, seq_linkage_types)
753
+ for layer in self.seq_layers:
754
+ seq_hidden = layer(seq_hidden, seq_attention_mask)
755
+
756
+ seq_pooled = seq_hidden[:, 0, :] # [CLS] token
757
+ seq_logits = self.seq_mlm_head(seq_hidden)
758
+
759
+ # ===== Distance Predictions (Topology) =====
760
+ # Compute pairwise distance predictions
761
+ # MEMORY OPTIMIZATION: Project to 128-dim first
762
+ seq_hidden_small = self.dist_proj(seq_hidden) # (batch, seq_len, 128)
763
+
764
+ # Expand for pairwise: (batch, seq_len, 1, 128) - (batch, 1, seq_len, 128)
765
+ h_i = seq_hidden_small.unsqueeze(2)
766
+ h_j = seq_hidden_small.unsqueeze(1)
767
+ h_diff = torch.abs(h_i - h_j) # (batch, seq_len, seq_len, 128) - Much smaller!
768
+ dist_predictions = self.distance_head(h_diff) # (batch, seq_len, seq_len, 1)
769
+
770
+ # ===== MS Encoder =====
771
+ ms_hidden = None
772
+ ms_pooled = None
773
+ ms_logits = None
774
+
775
+ if ms_token_ids is not None:
776
+ ms_hidden = self.ms_embeddings(ms_token_ids)
777
+ for layer in self.ms_layers:
778
+ ms_hidden = layer(ms_hidden, ms_attention_mask)
779
+
780
+ ms_pooled = ms_hidden[:, 0, :] # [CLS] token
781
+ ms_logits = self.ms_mlm_head(ms_hidden)
782
+
783
+ # Zero out MS representations for samples without MS data
784
+ if has_ms is not None:
785
+ has_ms_expanded = has_ms.unsqueeze(1).float() # (batch, 1)
786
+ ms_pooled = ms_pooled * has_ms_expanded
787
+
788
+ # ===== Structure Encoder =====
789
+ struct_pooled = None
790
+ struct_logits = None
791
+ struct_hidden = None
792
+
793
+ if self.config.use_3d and struct_token_ids is not None:
794
+ struct_hidden = self.struct_embeddings(struct_token_ids)
795
+ for layer in self.struct_layers:
796
+ struct_hidden = layer(struct_hidden, struct_attention_mask)
797
+
798
+ struct_pooled = struct_hidden[:, 0, :] # [CLS] token
799
+ struct_logits = self.struct_mlm_head(struct_hidden)
800
+
801
+ # Zero out structure representations for samples without 3D data
802
+ if has_3d is not None:
803
+ has_3d_expanded = has_3d.unsqueeze(1).float() # (batch, 1)
804
+ struct_pooled = struct_pooled * has_3d_expanded
805
+
806
+ # ===== Cross-Attention (Sequence → VQ-VAE Structural Tokens) =====
807
+ # Use residue-level alignment between WURCS tokens and VQ-VAE tokens
808
+ if self.config.use_cross_attention and struct_residue_ids is not None:
809
+ # Create residue-level mask
810
+ # WURCS token with residue_id=0 → VQ-VAE tokens with residue_id=0
811
+ residue_mask = create_residue_level_mask(
812
+ seq_residue_ids=seq_residue_ids,
813
+ struct_residue_ids=struct_residue_ids,
814
+ ) # (batch, N_seq, N_struct)
815
+
816
+ # Apply cross-attention: sequence tokens attend to VQ-VAE tokens
817
+ seq_hidden = self.cross_attention(
818
+ seq_hidden=seq_hidden,
819
+ struct_hidden=struct_hidden, # VQ-VAE token features
820
+ attention_mask=residue_mask, # Residue-based mask
821
+ )
822
+
823
+ # Update seq_pooled after cross-attention
824
+ seq_pooled = seq_hidden[:, 0, :]
825
+
826
+ # ===== Fusion =====
827
+ # Project to common hidden size
828
+ ms_pooled_projected = self.ms_projection(ms_pooled)
829
+
830
+ if self.config.use_3d and struct_pooled is not None:
831
+ struct_pooled_projected = self.struct_projection(struct_pooled)
832
+ combined = torch.cat([seq_pooled, ms_pooled_projected, struct_pooled_projected], dim=-1)
833
+ else:
834
+ combined = torch.cat([seq_pooled, ms_pooled_projected], dim=-1)
835
+
836
+ fused_repr = self.fusion_layer(combined)
837
+
838
+ # ===== Compute Losses =====
839
+ total_loss = None
840
+ seq_loss = None
841
+ ms_loss = None
842
+ struct_loss = None
843
+ dist_loss = None # NEW: Topology distance loss
844
+
845
+ if seq_labels is not None:
846
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
847
+ seq_loss = loss_fct(
848
+ seq_logits.view(-1, self.config.seq_vocab_size),
849
+ seq_labels.view(-1)
850
+ )
851
+
852
+ if ms_labels is not None:
853
+ ms_labels_masked = ms_labels.clone()
854
+ ms_labels_masked[~has_ms] = -100
855
+ # Only compute loss if there are valid labels (not all -100)
856
+ if (ms_labels_masked != -100).any():
857
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
858
+ ms_loss = loss_fct(
859
+ ms_logits.view(-1, self.config.ms_total_vocab_size),
860
+ ms_labels_masked.view(-1)
861
+ )
862
+ else:
863
+ ms_loss = torch.tensor(0.0, device=seq_token_ids.device)
864
+
865
+ if self.config.use_3d and struct_labels is not None and struct_logits is not None:
866
+ struct_labels_masked = struct_labels.clone()
867
+ if has_3d is not None:
868
+ struct_labels_masked[~has_3d] = -100
869
+ # Only compute loss if there are valid labels (not all -100)
870
+ if (struct_labels_masked != -100).any():
871
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
872
+ struct_loss = loss_fct(
873
+ struct_logits.view(-1, self.config.struct_vocab_size),
874
+ struct_labels_masked.view(-1)
875
+ )
876
+ else:
877
+ struct_loss = torch.tensor(0.0, device=seq_token_ids.device)
878
+
879
+ # ===== Distance Loss (Topology) =====
880
+ if dist_labels is not None:
881
+ # dist_predictions: (Batch, Seq, Seq, 1) -> (Batch, Seq, Seq)
882
+ preds = dist_predictions.squeeze(-1)
883
+
884
+ # Create mask for valid distance pairs (label != -1)
885
+ # Also respect attention mask to avoid padding
886
+ valid_mask = (dist_labels != -1) & (seq_attention_mask.unsqueeze(1) * seq_attention_mask.unsqueeze(2) == 1)
887
+
888
+ # DEBUG: Print once
889
+ if not hasattr(self, '_dist_debug_printed'):
890
+ print(f"[DIST DEBUG] dist_labels shape: {dist_labels.shape}, valid_mask.sum: {valid_mask.sum().item()}")
891
+ self._dist_debug_printed = True
892
+
893
+ if valid_mask.sum() > 0:
894
+ # MSE loss on valid positions only
895
+ loss_fct = nn.MSELoss()
896
+ dist_loss = loss_fct(preds[valid_mask], dist_labels[valid_mask].float())
897
+ else:
898
+ dist_loss = torch.tensor(0.0, device=seq_token_ids.device)
899
+ else:
900
+ # DEBUG: dist_labels is None
901
+ if not hasattr(self, '_dist_none_printed'):
902
+ print("[DIST DEBUG] dist_labels is None!")
903
+ self._dist_none_printed = True
904
+
905
+ # Weighted combination
906
+ losses = []
907
+ if seq_loss is not None:
908
+ losses.append(self.config.seq_loss_weight * seq_loss)
909
+ if ms_loss is not None:
910
+ losses.append(self.config.ms_loss_weight * ms_loss)
911
+ if struct_loss is not None:
912
+ losses.append(self.config.struct_loss_weight * struct_loss)
913
+ if dist_loss is not None:
914
+ losses.append(self.config.dist_loss_weight * dist_loss)
915
+
916
+ if losses:
917
+ total_loss = sum(losses)
918
+
919
+ if return_dict:
920
+ return {
921
+ 'loss': total_loss,
922
+ 'seq_loss': seq_loss,
923
+ 'ms_loss': ms_loss,
924
+ 'struct_loss': struct_loss,
925
+ 'dist_loss': dist_loss, # NEW: Topology loss
926
+ 'seq_logits': seq_logits,
927
+ 'ms_logits': ms_logits,
928
+ 'struct_logits': struct_logits,
929
+ 'dist_predictions': dist_predictions, # NEW: Distance predictions
930
+ 'seq_hidden': seq_hidden,
931
+ 'ms_hidden': ms_hidden,
932
+ 'struct_hidden': struct_hidden,
933
+ 'seq_pooled': seq_pooled,
934
+ 'ms_pooled': ms_pooled,
935
+ 'struct_pooled': struct_pooled,
936
+ 'fused_repr': fused_repr,
937
+ }
938
+ else:
939
+ return (total_loss, seq_logits, ms_logits, struct_logits, fused_repr)
940
+
941
+ def get_multimodal_representation(
942
+ self,
943
+ seq_token_ids: torch.Tensor,
944
+ seq_attention_mask: torch.Tensor,
945
+ seq_residue_ids: torch.Tensor,
946
+ ms_token_ids: torch.Tensor,
947
+ ms_attention_mask: torch.Tensor,
948
+ has_ms: torch.Tensor,
949
+ struct_token_ids: Optional[torch.Tensor] = None,
950
+ struct_attention_mask: Optional[torch.Tensor] = None,
951
+ struct_residue_ids: Optional[torch.Tensor] = None,
952
+ has_3d: Optional[torch.Tensor] = None,
953
+ ) -> torch.Tensor:
954
+ """Get fused multimodal representation (for inference)."""
955
+ outputs = self.forward(
956
+ seq_token_ids=seq_token_ids,
957
+ seq_attention_mask=seq_attention_mask,
958
+ seq_residue_ids=seq_residue_ids,
959
+ ms_token_ids=ms_token_ids,
960
+ ms_attention_mask=ms_attention_mask,
961
+ has_ms=has_ms,
962
+ struct_token_ids=struct_token_ids,
963
+ struct_attention_mask=struct_attention_mask,
964
+ struct_residue_ids=struct_residue_ids,
965
+ has_3d=has_3d,
966
+ return_dict=True,
967
+ )
968
+ return outputs['fused_repr']
969
+
970
+
971
+ if __name__ == "__main__":
972
+ # Test the model
973
+ print("="*80)
974
+ print("Testing Multimodal GlycanBERT v3")
975
+ print("="*80)
976
+
977
+ # Create config
978
+ config = MultimodalGlycanBERTConfig(
979
+ seq_vocab_size=166,
980
+ seq_hidden_size=768,
981
+ seq_num_layers=12,
982
+ seq_num_heads=12,
983
+ ms_vocab_size=242,
984
+ ms_hidden_size=384,
985
+ ms_num_layers=6,
986
+ ms_num_heads=6,
987
+ struct_vocab_size=1024,
988
+ struct_hidden_size=512,
989
+ struct_num_layers=8,
990
+ struct_num_heads=8,
991
+ use_3d=True,
992
+ use_cross_attention=True,
993
+ seq_loss_weight=0.60,
994
+ ms_loss_weight=0.15,
995
+ struct_loss_weight=0.25,
996
+ )
997
+
998
+ print(f"\nConfig:")
999
+ print(f" Sequence vocab: {config.seq_vocab_size}")
1000
+ print(f" MS vocab: {config.ms_vocab_size}")
1001
+ print(f" Structure vocab: {config.struct_vocab_size}")
1002
+ print(f" Loss weights: seq={config.seq_loss_weight}, ms={config.ms_loss_weight}, struct={config.struct_loss_weight}")
1003
+
1004
+ # Create model
1005
+ model = MultimodalGlycanBERT(config)
1006
+
1007
+ # Count parameters
1008
+ total_params = sum(p.numel() for p in model.parameters())
1009
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
1010
+
1011
+ print(f"\nModel Parameters:")
1012
+ print(f" Total: {total_params:,}")
1013
+ print(f" Trainable: {trainable_params:,}")
1014
+
1015
+ # Test forward pass
1016
+ print(f"\n{'='*80}")
1017
+ print("Testing Forward Pass (with Conv front-end)")
1018
+ print("="*80)
1019
+
1020
+ batch_size = 4
1021
+ seq_len = 128
1022
+ ms_len = 50
1023
+ struct_len = 40
1024
+
1025
+ # Create dummy inputs
1026
+ seq_token_ids = torch.randint(0, config.seq_vocab_size, (batch_size, seq_len))
1027
+ seq_attention_mask = torch.ones(batch_size, seq_len)
1028
+ # Approximate: ~5 tokens per residue
1029
+ seq_residue_ids = torch.div(
1030
+ torch.arange(seq_len), 5, rounding_mode="floor"
1031
+ ).unsqueeze(0).expand(batch_size, -1)
1032
+
1033
+ ms_token_ids = torch.randint(config.ms_vocab_offset, config.ms_total_vocab_size, (batch_size, ms_len))
1034
+ ms_attention_mask = torch.ones(batch_size, ms_len)
1035
+ struct_token_ids = torch.randint(0, config.struct_vocab_size, (batch_size, struct_len))
1036
+ struct_attention_mask = torch.ones(batch_size, struct_len)
1037
+ # Approximate: 4 tokens per residue for VQ-VAE tokens
1038
+ struct_residue_ids = torch.div(
1039
+ torch.arange(struct_len), 4, rounding_mode="floor"
1040
+ ).unsqueeze(0).expand(batch_size, -1)
1041
+
1042
+ has_ms = torch.tensor([True, True, False, True])
1043
+ has_3d = torch.tensor([True, False, True, True])
1044
+
1045
+ # Create labels for MLM
1046
+ seq_labels = seq_token_ids.clone()
1047
+ seq_labels[seq_labels != config.mask_token_id] = -100
1048
+ ms_labels = ms_token_ids.clone()
1049
+ ms_labels[ms_labels != config.mask_token_id] = -100
1050
+ struct_labels = struct_token_ids.clone()
1051
+ struct_labels[struct_labels != config.mask_token_id] = -100
1052
+
1053
+ # Forward pass
1054
+ outputs = model(
1055
+ seq_token_ids=seq_token_ids,
1056
+ seq_attention_mask=seq_attention_mask,
1057
+ seq_residue_ids=seq_residue_ids,
1058
+ ms_token_ids=ms_token_ids,
1059
+ ms_attention_mask=ms_attention_mask,
1060
+ has_ms=has_ms,
1061
+ struct_token_ids=struct_token_ids,
1062
+ struct_attention_mask=struct_attention_mask,
1063
+ struct_residue_ids=struct_residue_ids,
1064
+ has_3d=has_3d,
1065
+ seq_labels=seq_labels,
1066
+ ms_labels=ms_labels,
1067
+ struct_labels=struct_labels,
1068
+ )
1069
+
1070
+ print(f"\nOutput shapes:")
1071
+ print(f" seq_logits: {outputs['seq_logits'].shape}")
1072
+ print(f" ms_logits: {outputs['ms_logits'].shape}")
1073
+ print(f" struct_logits: {outputs['struct_logits'].shape}")
1074
+ print(f" fused_repr: {outputs['fused_repr'].shape}")
1075
+
1076
+ print(f"\nLosses:")
1077
+ print(f" Total loss: {outputs['loss'].item():.4f}")
1078
+ print(f" Sequence loss: {outputs['seq_loss'].item():.4f}")
1079
+ print(f" MS loss: {outputs['ms_loss'].item():.4f}")
1080
+ print(f" Structure loss: {outputs['struct_loss'].item():.4f}")
1081
+
1082
+ print(f"\n{'='*80}")
1083
+ print("Model Test Complete!")
1084
+ print("="*80)
releases/2026-10-02/bertose_source/training/a5_wurcs_bpe.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic train-only WURCS BPE utilities for A5."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import Counter, defaultdict
7
+ from pathlib import Path
8
+ from typing import Iterable, Optional
9
+
10
+ from .a5_topology import node_index, node_label
11
+
12
+ try:
13
+ from bert_training_v4.downstream_tasks.utils.wurcs_bpe_tokenizer import (
14
+ WURCSBPETokenizer,
15
+ WURCSPreTokenizer,
16
+ )
17
+ except ImportError:
18
+ from ..bert_training_v4.downstream_tasks.utils.wurcs_bpe_tokenizer import (
19
+ WURCSBPETokenizer,
20
+ WURCSPreTokenizer,
21
+ )
22
+
23
+
24
+ SUBMITTED_FIT_ALPHABET = tuple(
25
+ "#$%()*-./0123456789<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ^abcdefghijklmnopqrstuvwxyz{|}~"
26
+ )
27
+
28
+
29
+ class _TopLevelBracketPattern:
30
+ """Expose regex-like findall while respecting nested SMILES brackets."""
31
+
32
+ @staticmethod
33
+ def findall(text: str) -> list[str]:
34
+ residues: list[str] = []
35
+ depth = 0
36
+ start = -1
37
+ for index, character in enumerate(text):
38
+ if character == "[":
39
+ if depth == 0:
40
+ start = index + 1
41
+ depth += 1
42
+ elif character == "]" and depth:
43
+ depth -= 1
44
+ if depth == 0 and start >= 0:
45
+ residues.append(text[start:index])
46
+ start = -1
47
+ if depth:
48
+ return []
49
+ return residues
50
+
51
+
52
+ class A5WURCSPreTokenizer(WURCSPreTokenizer):
53
+ """Submitted pre-tokenizer with nested bracket-safe residue extraction."""
54
+
55
+ A5_LINKAGE_PATTERN = re.compile(
56
+ r"([A-Za-z]+)(\d+|\?)-([A-Za-z]+)(\d+|\?)"
57
+ )
58
+ A5_ANOMERIC_PATTERN = re.compile(r"-([0-9]+)([abuxo])(?:_|$)")
59
+
60
+ def __init__(self) -> None:
61
+ super().__init__()
62
+ self.RESIDUE_PATTERN = _TopLevelBracketPattern()
63
+
64
+ def _split_wurcs_sections(self, wurcs: str) -> list[str]:
65
+ sections = super()._split_wurcs_sections(wurcs)
66
+ if len(sections) <= 5:
67
+ return sections
68
+ # WURCS LIN modifications may contain unbracketed slashes. The format
69
+ # has exactly five top-level sections, so every trailing fragment
70
+ # belongs to the linkage section rather than a new section.
71
+ return sections[:4] + ["/".join(sections[4:])]
72
+
73
+ def _instance_anomeric_positions(
74
+ self, residue_definitions: list[str], index_list: list[str]
75
+ ) -> list[int | None]:
76
+ positions: list[int | None] = []
77
+ for definition_index in index_list:
78
+ try:
79
+ residue_definition = residue_definitions[int(definition_index) - 1]
80
+ except (IndexError, ValueError):
81
+ positions.append(None)
82
+ continue
83
+ match = self.A5_ANOMERIC_PATTERN.search(residue_definition)
84
+ positions.append(int(match.group(1)) if match is not None else None)
85
+ return positions
86
+
87
+ def _analyze_a5_tree(
88
+ self,
89
+ linkages: list[str],
90
+ residue_count: int,
91
+ anomeric_positions: list[int | None] | None = None,
92
+ ) -> tuple[dict[str, int], dict[str, int], dict[int, list[int]]]:
93
+ branch_points: dict[str, int] = defaultdict(int)
94
+ children: dict[str, list[str]] = defaultdict(list)
95
+ all_residues: set[str] = set()
96
+ adjacency: dict[int, list[int]] = defaultdict(list)
97
+ for linkage in linkages:
98
+ match = self.A5_LINKAGE_PATTERN.match(linkage)
99
+ if not match:
100
+ continue
101
+ left, left_position, right, right_position = match.groups()
102
+ try:
103
+ left_index = node_index(left) - 1
104
+ right_index = node_index(right) - 1
105
+ except ValueError:
106
+ continue
107
+ if not (
108
+ 0 <= left_index < residue_count
109
+ and 0 <= right_index < residue_count
110
+ and left_index != right_index
111
+ ):
112
+ continue
113
+ parent, child = left, right
114
+ if anomeric_positions is not None:
115
+ left_anomeric = anomeric_positions[left_index]
116
+ right_anomeric = anomeric_positions[right_index]
117
+ left_is_donor = (
118
+ left_position != "?"
119
+ and left_anomeric is not None
120
+ and int(left_position) == left_anomeric
121
+ )
122
+ right_is_donor = (
123
+ right_position != "?"
124
+ and right_anomeric is not None
125
+ and int(right_position) == right_anomeric
126
+ )
127
+ if left_is_donor and not right_is_donor:
128
+ parent, child = right, left
129
+ elif right_is_donor and not left_is_donor:
130
+ parent, child = left, right
131
+ children[parent].append(child)
132
+ all_residues.update((parent, child))
133
+ adjacency[left_index].append(right_index)
134
+ adjacency[right_index].append(left_index)
135
+
136
+ for residue, child_labels in children.items():
137
+ if len(child_labels) > 1:
138
+ branch_points[residue] = len(child_labels) - 1
139
+ child_set = {child for values in children.values() for child in values}
140
+ roots = all_residues - child_set
141
+ root = min(roots, key=node_index) if roots else node_label(1)
142
+ depths = {root: 0}
143
+ queue = [root]
144
+ while queue:
145
+ current = queue.pop(0)
146
+ for child in children.get(current, []):
147
+ if child not in depths:
148
+ depths[child] = depths[current] + 1
149
+ queue.append(child)
150
+ return dict(branch_points), depths, adjacency
151
+
152
+ def _a5_linkage_type(self, linkage: str) -> int:
153
+ match = self.A5_LINKAGE_PATTERN.match(linkage)
154
+ if not match:
155
+ return 8
156
+ _, from_position, _, to_position = match.groups()
157
+ try:
158
+ positions = (int(from_position), int(to_position))
159
+ except ValueError:
160
+ return 8
161
+ linkage_types = {
162
+ (1, 2): 0,
163
+ (2, 1): 0,
164
+ (1, 3): 1,
165
+ (3, 1): 1,
166
+ (1, 4): 2,
167
+ (4, 1): 2,
168
+ (1, 6): 3,
169
+ (6, 1): 3,
170
+ (2, 3): 4,
171
+ (3, 2): 4,
172
+ (2, 6): 5,
173
+ (6, 2): 5,
174
+ (3, 6): 6,
175
+ (6, 3): 6,
176
+ }
177
+ return linkage_types.get(positions, 7)
178
+
179
+ def pre_tokenize(self, wurcs: str) -> list[dict]:
180
+ # Reuse the submitted semantic split, then correct only its tree
181
+ # annotations. This keeps the BPE unit text and special-token behavior.
182
+ inherited_units = super().pre_tokenize(wurcs)
183
+ try:
184
+ parts = self._split_wurcs_sections(wurcs)
185
+ index_list = parts[3].split("-") if len(parts) > 3 and parts[3] else []
186
+ linkages = parts[4].split("_") if len(parts) > 4 and parts[4] else []
187
+ residue_definitions = self.RESIDUE_PATTERN.findall(parts[2])
188
+ anomeric_positions = self._instance_anomeric_positions(
189
+ residue_definitions, index_list
190
+ )
191
+ branch_points, depths, adjacency = self._analyze_a5_tree(
192
+ linkages, len(index_list), anomeric_positions
193
+ )
194
+ except (IndexError, ValueError):
195
+ return inherited_units
196
+
197
+ units: list[dict] = []
198
+ opened_residues: set[int] = set()
199
+ for inherited in inherited_units:
200
+ if inherited.get("text") == "[BRANCH_OPEN]":
201
+ continue
202
+ unit = dict(inherited)
203
+ residue_id = int(unit.get("residue_id", -1))
204
+ if residue_id >= 0:
205
+ label = node_label(residue_id + 1)
206
+ if residue_id not in opened_residues:
207
+ for _ in range(branch_points.get(label, 0)):
208
+ units.append(
209
+ {
210
+ "text": "[BRANCH_OPEN]",
211
+ "type": "special",
212
+ "residue_id": -1,
213
+ "branch_depth": depths.get(label, 0),
214
+ "linkage_type": 0,
215
+ }
216
+ )
217
+ opened_residues.add(residue_id)
218
+ unit["branch_depth"] = depths.get(label, 0)
219
+ elif unit.get("type") == "linkage":
220
+ unit["linkage_type"] = self._a5_linkage_type(str(unit["text"]))
221
+ units.append(unit)
222
+
223
+ if units:
224
+ units[0]["distance_matrix"] = self._compute_distance_matrix(
225
+ adjacency, len(index_list)
226
+ )
227
+ return units
228
+
229
+
230
+ class A5WURCSBPETokenizer(WURCSBPETokenizer):
231
+ """Train-only BPE with a fit-derived alphabet and deterministic merge ties."""
232
+
233
+ def __init__(self, vocab_path: Optional[str | Path] = None) -> None:
234
+ super().__init__(str(vocab_path) if vocab_path is not None else None)
235
+ self.pre_tokenizer = A5WURCSPreTokenizer()
236
+
237
+ @classmethod
238
+ def train_from_fit_corpus(
239
+ cls,
240
+ wurcs_strings: Iterable[str],
241
+ num_merges: int = 2000,
242
+ output_path: Optional[str | Path] = None,
243
+ min_frequency: int = 2,
244
+ max_token_length: Optional[int] = None,
245
+ ) -> "A5WURCSBPETokenizer":
246
+ tokenizer = cls()
247
+ pre_tokenizer = tokenizer.pre_tokenizer
248
+ unit_counts: Counter[str] = Counter()
249
+ for wurcs in wurcs_strings:
250
+ for unit in pre_tokenizer.pre_tokenize(wurcs):
251
+ if unit["type"] != "special":
252
+ unit_counts[unit["text"]] += 1
253
+
254
+ # The submitted tokenizer learned the base character inventory from its
255
+ # corpus. Preserve that behavior, but fit it from training parents only.
256
+ tokenizer.fit_alphabet = tuple(
257
+ sorted({char for unit in unit_counts for char in unit})
258
+ )
259
+
260
+ next_id = len(tokenizer.special_tokens)
261
+ for char in tokenizer.fit_alphabet:
262
+ tokenizer.token_to_id[char] = next_id
263
+ tokenizer.id_to_token[next_id] = char
264
+ next_id += 1
265
+
266
+ word_freqs: dict[tuple[str, ...], int] = {
267
+ tuple(unit): count
268
+ for unit, count in sorted(unit_counts.items())
269
+ if count >= min_frequency
270
+ }
271
+ merges: list[tuple[str, str]] = []
272
+ for _ in range(num_merges):
273
+ pair_counts: Counter[tuple[str, str]] = Counter()
274
+ for word, frequency in word_freqs.items():
275
+ for index in range(len(word) - 1):
276
+ pair_counts[(word[index], word[index + 1])] += frequency
277
+ candidates = [
278
+ (pair, count)
279
+ for pair, count in pair_counts.items()
280
+ if count >= min_frequency
281
+ and (
282
+ max_token_length is None
283
+ or len(pair[0]) + len(pair[1]) <= max_token_length
284
+ )
285
+ ]
286
+ if not candidates:
287
+ break
288
+ best_pair, _ = min(
289
+ candidates,
290
+ key=lambda item: (-item[1], item[0][0], item[0][1]),
291
+ )
292
+ new_token = best_pair[0] + best_pair[1]
293
+ merges.append(best_pair)
294
+ tokenizer.token_to_id[new_token] = next_id
295
+ tokenizer.id_to_token[next_id] = new_token
296
+ next_id += 1
297
+
298
+ merged_freqs: Counter[tuple[str, ...]] = Counter()
299
+ for word, frequency in word_freqs.items():
300
+ merged_word: list[str] = []
301
+ index = 0
302
+ while index < len(word):
303
+ if (
304
+ index < len(word) - 1
305
+ and word[index] == best_pair[0]
306
+ and word[index + 1] == best_pair[1]
307
+ ):
308
+ merged_word.append(new_token)
309
+ index += 2
310
+ else:
311
+ merged_word.append(word[index])
312
+ index += 1
313
+ merged_freqs[tuple(merged_word)] += frequency
314
+ word_freqs = dict(merged_freqs)
315
+
316
+ tokenizer.merges = merges
317
+ if output_path is not None:
318
+ tokenizer.save_vocab(str(output_path))
319
+ return tokenizer
320
+
321
+ def tokenize_sequence(self, wurcs: str, max_length: int = 256) -> dict:
322
+ units = self.pre_tokenizer.pre_tokenize(wurcs)
323
+ tokens: list[str] = []
324
+ token_ids: list[int] = []
325
+ residue_ids: list[int] = []
326
+ branch_depths: list[int] = []
327
+ linkage_types: list[int] = []
328
+
329
+ for unit in units:
330
+ unit_tokens = (
331
+ [unit["text"]]
332
+ if unit["type"] == "special"
333
+ else self.apply_bpe(unit["text"])
334
+ )
335
+ for token in unit_tokens:
336
+ tokens.append(token)
337
+ token_ids.append(
338
+ self.token_to_id.get(token, self.token_to_id["[UNK]"])
339
+ )
340
+ residue_ids.append(int(unit["residue_id"]))
341
+ branch_depths.append(int(unit["branch_depth"]))
342
+ linkage_types.append(int(unit["linkage_type"]))
343
+
344
+ if len(tokens) > max_length:
345
+ tokens = tokens[: max_length - 1] + ["[END]"]
346
+ token_ids = token_ids[: max_length - 1] + [self.token_to_id["[END]"]]
347
+ residue_ids = residue_ids[: max_length - 1] + [-1]
348
+ branch_depths = branch_depths[: max_length - 1] + [0]
349
+ linkage_types = linkage_types[: max_length - 1] + [0]
350
+
351
+ length = len(tokens)
352
+ attention_mask = [1] * length
353
+ padding = max_length - length
354
+ tokens.extend(["[PAD]"] * padding)
355
+ token_ids.extend([self.token_to_id["[PAD]"]] * padding)
356
+ residue_ids.extend([-1] * padding)
357
+ branch_depths.extend([0] * padding)
358
+ linkage_types.extend([0] * padding)
359
+ attention_mask.extend([0] * padding)
360
+ return {
361
+ "tokens": tokens,
362
+ "token_ids": token_ids,
363
+ "residue_ids": residue_ids,
364
+ "branch_depths": branch_depths,
365
+ "linkage_types": linkage_types,
366
+ "attention_mask": attention_mask,
367
+ "length": length,
368
+ }
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_submission_readback_11767377/review_a5_native_eight_cuda_resume_diagnostic_r5_11767377_terminal_certification_receipt.json ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "bertose_review_a5_native_eight_cuda_resume_diagnostic_r5_terminal_certification_20260727",
3
+ "status": "PASS_TERMINAL_CERTIFIED_REAL_CUDA_EXACT_RESUME",
4
+ "job": {
5
+ "job_id": "11767377",
6
+ "job_name": "a5-n8-cuda-resume-r5",
7
+ "state": "COMPLETED",
8
+ "exit_code": "0:0",
9
+ "submit_time": "2026-07-26T16:38:45",
10
+ "eligible_time": "2026-07-26T16:38:45",
11
+ "start_time": "2026-07-26T16:38:48",
12
+ "end_time": "2026-07-26T16:41:19",
13
+ "scheduler_wait_seconds": 3,
14
+ "elapsed": "00:02:31",
15
+ "restarts": 0,
16
+ "node": "nova18-singularity"
17
+ },
18
+ "route": {
19
+ "partition": "interactive",
20
+ "account": "ratul",
21
+ "qos": "normal",
22
+ "requested_gres": "gpu:1",
23
+ "gpu_feature_constraint": null,
24
+ "allocated_gpu": "v100-sxm2-32g",
25
+ "allocated_gpu_count": 1,
26
+ "cpus": 4,
27
+ "memory": "16G",
28
+ "time_limit": "00:30:00",
29
+ "requeue": false
30
+ },
31
+ "frozen_submission": {
32
+ "runner_sha256": "3e561dceed9a4a05b720b46598d18b7f98cf04e49dbe8ccf228afb1aa5f236b5",
33
+ "runner_bytes": 11473,
34
+ "sbatch_sha256": "87dadccc4b99db12acdd5f62f5429444489f403fddcd12ceaa9225208defb547",
35
+ "sbatch_bytes": 2346,
36
+ "submission_readback_receipt_sha256": "4824c00d3e28ee15cab63401204ff2c6b2aaa250012b97fd1776dacc6e36266c",
37
+ "submission_readback_receipt_bytes": 3307,
38
+ "submission_count": 1,
39
+ "other_runnable_copy_count_after_terminal": 0
40
+ },
41
+ "terminal_scheduler_readback": {
42
+ "scontrol_sha256": "b5e8b47da0a15a3cedfea54a85a397b744affddae2d5bffae394c40b754b1d10",
43
+ "scontrol_bytes": 2450,
44
+ "sacct_sha256": "fd3a55dc63abd830ec2620f7f637e51871552f663466c6496a9a72ed7eb18216",
45
+ "sacct_bytes": 300
46
+ },
47
+ "cuda_success_receipt": {
48
+ "path": "review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_a5_native_eight_cuda_resume_diagnostic_receipt_r5.json",
49
+ "file_sha256": "6a1bc65c568a17dc45f1102cd122ac43c62a8ee3b28e06ec855577857cc41ce0",
50
+ "bytes": 127461,
51
+ "self_domain_sha256": "0e491d188a71df9c51dc9deb5df8faea2f88f5096f65ac83a5cde5b53c1762a3",
52
+ "self_domain_sha256_verified": true,
53
+ "schema": "bertose_review_a5_native_eight_cuda_resume_diagnostic_20260727_r5",
54
+ "status": "PASS_REAL_CUDA_R7_BOTH_LOADS_EXACT_RESUME_DRAWS_AND_DEVICE_RECEIPT"
55
+ },
56
+ "cuda_exact_resume_proof": {
57
+ "load_latest_executed": true,
58
+ "load_best_executed": true,
59
+ "interrupted_uninterrupted_exact_match": true,
60
+ "python_numpy_torch_cpu_torch_cuda_next_draws_exact": true,
61
+ "private_generators_did_not_mutate_global_rng": true,
62
+ "latest_model_tensor_devices": [
63
+ "cuda:0"
64
+ ],
65
+ "latest_optimizer_tensor_devices": [
66
+ "cuda:0"
67
+ ],
68
+ "latest_rng_tensor_devices": [
69
+ "cpu",
70
+ "cpu",
71
+ "cpu"
72
+ ],
73
+ "best_model_tensor_devices": [
74
+ "cuda:0"
75
+ ],
76
+ "best_optimizer_tensor_devices": [
77
+ "cuda:0"
78
+ ],
79
+ "best_rng_tensor_devices": [
80
+ "cpu",
81
+ "cpu",
82
+ "cpu"
83
+ ],
84
+ "latest_provenance_record_lifecycle": [
85
+ 1,
86
+ 1,
87
+ 0
88
+ ],
89
+ "best_provenance_record_lifecycle": [
90
+ 1,
91
+ 1,
92
+ 0
93
+ ],
94
+ "negative_cuda_boundary_cases_rejected": [
95
+ "dtype",
96
+ "shape",
97
+ "value",
98
+ "mixed_device",
99
+ "requested_device"
100
+ ],
101
+ "negative_case_checkpoint_model_optimizer_unchanged": true,
102
+ "negative_case_global_rng_unchanged": true,
103
+ "production_runtime_source_modified": false,
104
+ "r7_source_modified": false,
105
+ "production_live_cell_claim_requested_by_synthetic_diagnostic": false
106
+ },
107
+ "output_integrity": {
108
+ "checkpoint_index_sha256": "9292c31195a7eddfe9f987a8cfe8cda843b283bb10a1bc8fb1b05d88f82a92d9",
109
+ "checkpoint_index_bytes": 796,
110
+ "checkpoint_payload_sha256": "6885400540a9701725519e4e476cfbca0dec700836c52c0777e9ae5858ef6bb3",
111
+ "checkpoint_payload_bytes": 1683243,
112
+ "slurm_stdout_sha256": "60c00132b50b4fcfd887f4fef35828f55c5cb3fe5562fa7a6f81d20fc64244ae",
113
+ "slurm_stdout_bytes": 131246,
114
+ "slurm_stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
115
+ "slurm_stderr_bytes": 0,
116
+ "terminal_hash_ledger_sha256": "19036f29acc263a1b3c1a764e87bfc3c420c4420c052c7c37500a90128350808",
117
+ "terminal_hash_ledger_bytes": 2644,
118
+ "terminal_hash_ledger_entry_count": 10
119
+ },
120
+ "scope": {
121
+ "synthetic_isolated_fixture": true,
122
+ "production_review_artifacts_opened_read_only": true,
123
+ "production_training_examples_used": false,
124
+ "production_training_occurred": false,
125
+ "official_submitted_artifacts_modified": false,
126
+ "cuda_exact_resume_gate_certified": true,
127
+ "training_authorized": false,
128
+ "scientific_launch_authorized": false,
129
+ "nova_submission_authorized": false,
130
+ "native_eight_array_submission_authorized_by_this_receipt": false
131
+ },
132
+ "resubmit_authorized": false,
133
+ "reroute_authorized": false,
134
+ "cancel_authorized": false
135
+ }
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_a5_native_eight_cuda_resume_diagnostic_receipt_r5.json ADDED
The diff for this file is too large to render. See raw diff
 
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_cuda_resume_diagnostic_20260727_r5_synthetic_device_receipt_fix/review_checkpoints_r1/review_checkpoint_index.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"cell_id":"A111","generation":1,"index_sha256":"68c0dedf930e9e4cb54f58a8cf2ab15fe6a52a2c0567b68873265856d67cc087","roles":{"best":{"bytes":1683243,"development_loss":1.0,"epoch":1,"filename":"review_checkpoint_payload_e001_6885400540a9701725519e4e476cfbca0dec700836c52c0777e9ae5858ef6bb3.pt","sha256":"6885400540a9701725519e4e476cfbca0dec700836c52c0777e9ae5858ef6bb3"},"latest":{"bytes":1683243,"development_loss":1.0,"epoch":1,"filename":"review_checkpoint_payload_e001_6885400540a9701725519e4e476cfbca0dec700836c52c0777e9ae5858ef6bb3.pt","sha256":"6885400540a9701725519e4e476cfbca0dec700836c52c0777e9ae5858ef6bb3"},"milestones":{}},"run_identity_sha256":"d35d4d8041d8a5448c68584b0c8ab9b0aeaa939fee7b1fe4fca1b606aef92f9a","schema":"bertose_review_a5_native_eight_checkpoint_index_20260722_r3"}
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r1/review_a5_native_eight_data_adapter_receipt_r1.json ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifacts": {
3
+ "development_manifest": {
4
+ "bytes": 3982015,
5
+ "canonical_row_surface_sha256": "92f46de77c5e1831f01dbe4f672bad19b180d8fb0ea6798040bd07ed7009112b",
6
+ "name": "review_a5_native_eight_joined_development_manifest_r1.tsv.gz",
7
+ "rows": 55440,
8
+ "sha256": "4eea50c1bd4eee0ea10062c287f31c619cef5835e2e660facd098698859f7e70"
9
+ },
10
+ "train_manifest": {
11
+ "bytes": 15455767,
12
+ "canonical_row_surface_sha256": "94f8e3b13bf95bed3f2768c9303d8602ecfa78d3c1d7956699c68856f33e8eef",
13
+ "name": "review_a5_native_eight_joined_train_manifest_r1.tsv.gz",
14
+ "rows": 221827,
15
+ "sha256": "13b7ebc78b22baa1a38d2ae40da202dcc1e20e56f44220b4d4d5003ca5f3b8f2"
16
+ }
17
+ },
18
+ "authority_bindings": {
19
+ "composite_r5": "24c35257fc61e1c9c77630d2b0075775a7f46b6500ecb5f175ef8f42bd214e0e",
20
+ "executor_r5": "a482a70a2edcb7c9936b091a86a0f7d98714b5bab09a0bdc80cd927f015a46f2",
21
+ "ms_authority": "48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4",
22
+ "ms_certificate": "53098620a5c22dafdc951ba743515b4b10b1673c02f0aa4d881db669584902fa",
23
+ "ms_receipt": "f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4",
24
+ "ms_runtime": "68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6",
25
+ "planner_r5": "1b9b05c812603db7b216aa989214a3c3715f80032b3a8b82577ae2b9e48f048d",
26
+ "sequence_receipt": "53b058a2032c424718a96757b3aa63df5160c20910001fcb128b0ace03a13a4c",
27
+ "sequence_runtime": "0b212043878ebfc7edb75929aa0cfe025df7eaf69bc526ed57b85e5cd0b4ba69",
28
+ "sequence_summary": "31fd5a9e015b370681a9d2bacdc1b3ebce0e9408f0cf2835affa35596110460a",
29
+ "static_r7_authority": "59cf794d52341847b08aaa7f4212d1203dec4df89609157d924fda50d54f7602",
30
+ "static_r7_receipt": "0729fa35e7a34a51ee77d2752f008a783d582b845e6c8dd9ae694eb9edb61d9c",
31
+ "static_r7_summary": "f95235a74962fd8aec04b0a14ad17aea3e2bcec27826102d5d26d3f907871908"
32
+ },
33
+ "availability_pattern_counts": {
34
+ "development": {
35
+ "000": 37213,
36
+ "001": 238,
37
+ "010": 339,
38
+ "011": 0,
39
+ "100": 12351,
40
+ "101": 5058,
41
+ "110": 147,
42
+ "111": 94
43
+ },
44
+ "train": {
45
+ "000": 147444,
46
+ "001": 978,
47
+ "010": 1491,
48
+ "011": 0,
49
+ "100": 49345,
50
+ "101": 21605,
51
+ "110": 567,
52
+ "111": 397
53
+ }
54
+ },
55
+ "counts": {
56
+ "development_sequence_parents": 55440,
57
+ "ms_development_acquisitions": 49573,
58
+ "ms_development_parents": 580,
59
+ "ms_train_parents": 2455,
60
+ "static_development_parents": 5390,
61
+ "static_development_states": 7964,
62
+ "static_exact_single_state_parents": 14368,
63
+ "static_open_unspecified_two_state_parents": 8951,
64
+ "static_train_parents": 22980,
65
+ "static_train_states": 34408,
66
+ "static_unknown_x_anomer_two_state_parents": 5051,
67
+ "topology_development_parents": 17650,
68
+ "topology_train_parents": 71914,
69
+ "train_sequence_parents": 221827
70
+ },
71
+ "execution_certified": false,
72
+ "gpu_launch_authorized": false,
73
+ "join_contract": {
74
+ "ms": "exact_parent_id_role_and_canonical_wurcs",
75
+ "official_or_submitted_artifacts_modified": false,
76
+ "sequence_topology": "authoritative_sequence_row_parent_role_and_topology_row",
77
+ "static": "exact_parent_id_and_role; preserve parent canonical WURCS plus every R7 state effective WURCS; x-anomer or open/unspecified parent identities expand only through the R7-declared alpha/beta states"
78
+ },
79
+ "nova_submission_authorized": false,
80
+ "runtime_source": {
81
+ "bytes": 79461,
82
+ "name": "review_a5_native_eight_data_adapter_r1.py",
83
+ "sha256": "bc34703f189f240a27282542a4e6268b8eeab7d92ad276dc0e375efa66841ce1"
84
+ },
85
+ "schema": "bertose_a5_native_eight_data_adapter_receipt_20260721_r1",
86
+ "scientific_launch_authorized": false,
87
+ "split_roster_sha256": {
88
+ "development": "dbadd12a88e29cc1a7ca1da85895d89f0162fa8bc9f3dcf8aebf5b8781a1e698",
89
+ "train": "2a7c8121c9ec88418e930cdec47302b7ef2b4d92e01fafa523218461a249de6e"
90
+ },
91
+ "static_active_production_materialization_authorized": false,
92
+ "static_final_token_package_bound": false,
93
+ "status": "PASS_IMMUTABLE_JOIN_PREPARATION_ONLY_STATIC_TOKENS_ABSENT",
94
+ "training_authorized": false
95
+ }
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r2/review_a5_native_eight_data_adapter_receipt_r2.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"affinose_supported":false,"authority_bindings":{"adapter_r1_receipt":"cacec838740d64a493c2e56fd1efa8a85ba7d11fce3ec26e40905c33db05e58d","composite_r5":"24c35257fc61e1c9c77630d2b0075775a7f46b6500ecb5f175ef8f42bd214e0e","executor_r5":"a482a70a2edcb7c9936b091a86a0f7d98714b5bab09a0bdc80cd927f015a46f2","joined_development_manifest_r1":"4eea50c1bd4eee0ea10062c287f31c619cef5835e2e660facd098698859f7e70","joined_train_manifest_r1":"13b7ebc78b22baa1a38d2ae40da202dcc1e20e56f44220b4d4d5003ca5f3b8f2","ms_authority":"48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4","ms_certificate":"53098620a5c22dafdc951ba743515b4b10b1673c02f0aa4d881db669584902fa","ms_parent_manifest":"10dc3fa7622e2a7e448d775f4082fdc84aa0622f9a7a74bcfaeed491c0eb7d39","ms_receipt":"f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4","ms_runtime":"68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6","planner_r5":"1b9b05c812603db7b216aa989214a3c3715f80032b3a8b82577ae2b9e48f048d","planner_r5_certificate":"4b3198f26e45d84ffced87280e7ac6cd59932bb92cf4da66db1eefb099b897bf","sequence_mask_runtime":"831bebd2dcf6a8308d01ec62eb6e1132fe7edbbc666c1c83787335acc576b19c","sequence_null":"b10556643abf560b9e8622e8ae67936d2d3ca8867433db485e785f31d049a8fb","sequence_null_receipt":"f4eb239f56e472d57dd3a3848c1a6c142e193cb326320ac8bf134b96ac35102a","sequence_package_receipt":"399df3de2b6c4d5e88aa442c8326214ab9cff2e8f0411e6921762bbade1475ba","sequence_receipt":"53b058a2032c424718a96757b3aa63df5160c20910001fcb128b0ace03a13a4c","sequence_runtime":"0b212043878ebfc7edb75929aa0cfe025df7eaf69bc526ed57b85e5cd0b4ba69","sequence_summary":"31fd5a9e015b370681a9d2bacdc1b3ebce0e9408f0cf2835affa35596110460a","static_r7_authority":"59cf794d52341847b08aaa7f4212d1203dec4df89609157d924fda50d54f7602","static_r7_receipt":"0729fa35e7a34a51ee77d2752f008a783d582b845e6c8dd9ae694eb9edb61d9c","static_r7_summary":"f95235a74962fd8aec04b0a14ad17aea3e2bcec27826102d5d26d3f907871908","topology_null":"a752ffd350fbe38aa0e6f6b42b76137f4f72d76fa9a7f5d3609bc47c65c552de","topology_null_receipt":"dace4cb5df71912f5aa7531bb0ce87db3b63d726b2a12aa41facabf9d54b5c35","topology_package_receipt":"333ee6f34e69f89e26b810cc565415b89d6874615ea4ac57850371fb159c1b71"},"cells":["A000","A100","A010","A001","A110","A101","A011","A111"],"common_seed":43,"downstream_supported":false,"execution_certified":false,"fresh_corrected_fit_required_for_all_cells":true,"gpu_launch_authorized":false,"historical_a000_a111_reuse_for_training":false,"nova_submission_authorized":false,"official_or_submitted_artifacts_modified":false,"reused_r1_artifacts":{"development_manifest":{"bytes":3982015,"canonical_row_surface_sha256":"92f46de77c5e1831f01dbe4f672bad19b180d8fb0ea6798040bd07ed7009112b","name":"review_a5_native_eight_joined_development_manifest_r1.tsv.gz","rows":55440,"sha256":"4eea50c1bd4eee0ea10062c287f31c619cef5835e2e660facd098698859f7e70"},"train_manifest":{"bytes":15455767,"canonical_row_surface_sha256":"94f8e3b13bf95bed3f2768c9303d8602ecfa78d3c1d7956699c68856f33e8eef","name":"review_a5_native_eight_joined_train_manifest_r1.tsv.gz","rows":221827,"sha256":"13b7ebc78b22baa1a38d2ae40da202dcc1e20e56f44220b4d4d5003ca5f3b8f2"}},"runtime_source":{"bytes":102990,"name":"review_a5_native_eight_data_adapter_r2.py","sha256":"9f8f167148aa234df413e4c3d4e1846881781c8e5a19bd91b0b178b260a3a537"},"schema":"bertose_a5_native_eight_data_adapter_receipt_20260721_r2","scientific_launch_authorized":false,"static_active_production_materialization_authorized":false,"static_final_token_package_bound":false,"status":"PASS_PREPARATION_ONLY_STATIC_FINAL_TOKENS_ABSENT_LAUNCH_CLOSED","training_authorized":false,"world_size":1}
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_receipt_r3.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"affinose_supported":false,"cells":["A000","A100","A010","A001","A110","A101","A011","A111"],"common_seed":43,"downstream_supported":false,"execution_certified":false,"external_authority_bindings":{"adapter_r2_receipt":"3b2118e128a5550ba4918b4f4f2c6d8045e319b7cff37b76d74dce6b3220c9f0","adapter_r2_source":"9f8f167148aa234df413e4c3d4e1846881781c8e5a19bd91b0b178b260a3a537","ms_authority":"48914855c4e458ff02452ad4db3743a4fbef12221e0313584db4c2b1bd684cd4","ms_certificate":"53098620a5c22dafdc951ba743515b4b10b1673c02f0aa4d881db669584902fa","ms_parent_manifest":"10dc3fa7622e2a7e448d775f4082fdc84aa0622f9a7a74bcfaeed491c0eb7d39","ms_receipt":"f654177eb6fde7289b6755812c2be298084697cf0d85159b3ec9da310fe3d4b4","ms_runtime":"68c0d543c5411276ef56c7072bded4ebda886f46b9fa4ebf3c95c1e54c1a08c6","scientific_boundary_authority":"1238f76e5da5184d16d33077bf3e5342033918fadb4eba84242c47cb1b1322cc","scientific_boundary_receipt":"fb65dddf0bc61b4c73b5dc5ec796967e65263e9d7bdc4882e07684e882280fdb","sequence_topology_r3_predecessor_receipt":"53b058a2032c424718a96757b3aa63df5160c20910001fcb128b0ace03a13a4c","sequence_topology_r4_bfs_receipt":"f20a1dd444f2760ccd8605d53ad716ceb55112fd32d3409826bcaf78ff2c6c3d","sequence_topology_r4_bfs_result":"75914916591d1db97a77d64b9e7a51e0f23526aeb87bd6e394d49c6583309469","sequence_topology_r4_receipt":"654f5b9728c3dfd2588c4b570b0e3a1bcab8feb366af28eeead36f9beb37d921","sequence_topology_r4_runtime":"efc56a84cb82f30c0566da83a38bf0f02fdd507e02d116d827c1bbd827151ecf","sequence_topology_r4_summary":"ccd4b81d3afac8ae789c538e0af332f74f2468d403da1d243fefefd4df013538","static_r7_authority":"59cf794d52341847b08aaa7f4212d1203dec4df89609157d924fda50d54f7602","static_r7_receipt":"0729fa35e7a34a51ee77d2752f008a783d582b845e6c8dd9ae694eb9edb61d9c","static_r7_summary":"f95235a74962fd8aec04b0a14ad17aea3e2bcec27826102d5d26d3f907871908","topology_loss_source":"64eaf9174f523ae909c84987049d071bf3869ca8e83919f14f0572ef77b0530e"},"fixed_loss_coefficients":[1,"topology_bit","ms_bit","static_bit"],"fixed_loss_denominator":4,"gpu_launch_authorized":false,"md_supported":false,"nova_submission_authorized":false,"official_or_submitted_artifacts_modified":false,"predecessor_adapter_r2":{"receipt_sha256":"3b2118e128a5550ba4918b4f4f2c6d8045e319b7cff37b76d74dce6b3220c9f0","source_sha256":"9f8f167148aa234df413e4c3d4e1846881781c8e5a19bd91b0b178b260a3a537"},"provenance_fields":["sequence_rows","roles","seq_token_categories","topology_rows","topology_residue_counts","topology_target_counts"],"runtime_source":{"bytes":41304,"name":"review_a5_native_eight_data_adapter_r3.py","sha256":"a76c24cb0a3b904ae95bfda21bdeb2ba96e36293368fe44a218429be2b10d43e"},"schema":"bertose_a5_native_eight_data_adapter_receipt_20260721_r3","scientific_launch_authorized":false,"static_active_production_materialization_authorized":false,"static_final_token_package_bound":false,"status":"PASS_PREPARATION_ONLY_R4_BOUND_STATIC_FINAL_TOKENS_ABSENT_LAUNCH_CLOSED","training_authorized":false,"weight_ablation_supported":false}
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_data_adapter_20260721_r3/review_a5_native_eight_data_adapter_verification_receipt_r1.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"affinose_supported":false,"artifacts":{"migration_handoff":{"bytes":4796,"name":"review_a5_native_eight_adapter_r3_r4_migration_handoff_20260721.md","sha256":"400bcb62d730f8449060f9becbacbec89e90f64c487be0d35c3dc43067e74c0e"},"release_receipt":{"bytes":3016,"name":"review_a5_native_eight_data_adapter_receipt_r3.json","sha256":"e29df7a91d0c30238abe9155e600fb1b77b0d3d3481f6599d2ca0d2185371b9f"},"runtime_source":{"bytes":41304,"name":"review_a5_native_eight_data_adapter_r3.py","sha256":"a76c24cb0a3b904ae95bfda21bdeb2ba96e36293368fe44a218429be2b10d43e"},"test_source":{"bytes":14372,"name":"test_review_a5_native_eight_data_adapter_r3_20260721.py","sha256":"44d54ed68291527ae98c2f5970ea339d791f8372a7a5d5a5b114b5a10f5d5360"}},"contracts":{"collate_only_through_r4_store":true,"corrected_scientific_boundary_bound":true,"direct_ms_v3_authorities_bound":true,"direct_static_r7_authorities_bound":true,"eight_fresh_seed43_cells":true,"explicit_frozen_r3_predecessor":true,"fixed_coefficients_constant_denominator_4":true,"r4_masker_only_sequence_checkpoint_restore":true,"r4_provenance_fields_retained":["sequence_rows","roles","seq_token_categories","topology_rows","topology_residue_counts","topology_target_counts"],"receipt_bound_topology_loss":true,"rows_only_through_r4_store_sample":true,"static_active_materialization_blocked":true},"downstream_supported":false,"execution_certified":false,"gpu_launch_authorized":false,"identities":{"a000_epoch_2_step_0_sha256":"05ac100b4f77b0ff33e126fe5560b24d7c4f732d07987862acbde8dcea6ee61c","adapter_contract_sha256":"a3b681f307bad96abb8e446eb8efd769f29ccb65728d424295c89c9ba065224f","authority_set_sha256":"7804332dfc2be432b45f6a82459f21f0b577fc5c358f16b1fea90ba4cb6bb52f","epoch_2_plan_sha256":"b3bdd276a7582748937c461e5c2ba6f927068c380af7d52eb3ad14701ad97330","store_manifest_identity_sha256":"7bd44c2f15715924bce78c7bbdbe57d40fc69aaf90ed834e17d9632bded0fc17"},"md_supported":false,"nova_submission_authorized":false,"publication":{"no_overwrite":true,"official_or_submitted_artifacts_modified":false,"receipt_written_last":true},"schema":"bertose_a5_native_eight_data_adapter_r3_verification_receipt_20260721_r1","scientific_launch_authorized":false,"status":"PASS_PREPARATION_ONLY_R4_MIGRATION_CERTIFIED_LAUNCH_CLOSED","tests":{"adapter_r3":{"clean_process_replays":2,"failed":0,"passed":11,"warnings":0},"certified_r4_runtime":{"failed":0,"passed":12,"warnings":0},"combined_passed":33,"independent_readback":{"remaining_p0":0,"remaining_p1":0,"remaining_p2":0,"status":"PASS"},"predecessor_adapter_r2":{"failed":0,"passed":10,"warnings":0}},"training_authorized":false,"weight_ablation_supported":false}
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r4/review_a5_native_eight_hotpath_r4_integrity_receipt.json ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "authorization": {
3
+ "nova_accessed": false,
4
+ "nova_submission_authorized": false,
5
+ "production_epoch_executed": false,
6
+ "scientific_launch_authorized": false,
7
+ "training_authorized": false
8
+ },
9
+ "caveats": {
10
+ "cuda_available_in_certification_environment": false,
11
+ "cuda_disposable_generator_positive_path_executed": false,
12
+ "cuda_state_behavior": "R4 requires a CPU uint8 CUDA state and a live CUDA runtime, then calls set_state on a disposable torch.Generator bound to the executor's exact CUDA device; when no executor is available it uses the current CUDA device, and without CUDA any CUDA-bearing checkpoint fails closed before mutation",
13
+ "scientific_or_training_semantics_changed": false,
14
+ "test_scope": "isolated one-step exact-R9 checkpoint/resume fixture plus frozen predecessor/controller regressions; no production epoch or Nova execution"
15
+ },
16
+ "closure": {
17
+ "loader": {
18
+ "bytes": 4013,
19
+ "mode": "0444",
20
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r4/load_review_a5_native_eight_hotpath_r4.py",
21
+ "sha256": "662d0766a8d68638480e9c44ca4070387481d43e1c666a20363025cf5b9bc958"
22
+ },
23
+ "runtime": {
24
+ "bytes": 23798,
25
+ "mode": "0444",
26
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r4/review_a5_native_eight_hotpath_r4.py",
27
+ "sha256": "600af4b6b0fab63b7780bb1e6be8233eb28bd0808ff390c3096369afec9c6649"
28
+ },
29
+ "test": {
30
+ "bytes": 19081,
31
+ "mode": "0444",
32
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r4/test_review_a5_native_eight_hotpath_r4.py",
33
+ "sha256": "ebbbdb35644e84b566278fec702afdf7bb23b77cc0b9da7dec226d68feb0504d"
34
+ }
35
+ },
36
+ "guards": {
37
+ "checkpoint_creation_chain_recomputed_from_exact_one_per_step_identity_list": true,
38
+ "checkpoint_creation_or_manager_verified_load_provenance_required": true,
39
+ "checkpoint_cursor_prevalidated_before_model_optimizer_state_mutation": true,
40
+ "checkpoint_history_max_epochs": 100,
41
+ "checkpoint_lightweight_provenance_fields": [
42
+ "step_identity_chain_sha256",
43
+ "epoch",
44
+ "plan_sha256",
45
+ "run_identity_sha256",
46
+ "cell_id",
47
+ "trusted_manager_entry_sha256_on_load"
48
+ ],
49
+ "checkpoint_lightweight_provenance_retired_after_successful_commit_or_restore": true,
50
+ "checkpoint_restore_chain_recomputed_from_step_identities": false,
51
+ "checkpoint_top_level_chain_equals_last_development_history_row_at_commit": true,
52
+ "checkpoint_top_level_chain_equals_last_development_history_row_at_validation": true,
53
+ "development_history_epochs_sequential_from_one": true,
54
+ "development_history_step_chain_fields_exact_lowercase_sha256": true,
55
+ "invalid_rng_rejected_before_model_optimizer_or_global_rng_mutation": true,
56
+ "additional_full_model_optimizer_or_checkpoint_state_hashes": 0,
57
+ "numpy_state_exact_serialized_shape_and_types": {
58
+ "cached_gaussian": "finite builtin float",
59
+ "has_gauss": "builtin int in {0,1}",
60
+ "keys": "plain CPU contiguous torch.int64 tensor shape [624], values in uint32 range",
61
+ "name": "builtin str exactly MT19937",
62
+ "position": "builtin int in [0,624]"
63
+ },
64
+ "python_state_parsed_by_disposable_random_instance": true,
65
+ "torch_cpu_state_parsed_by_disposable_generator_set_state": true,
66
+ "torch_cuda_state_parsed_by_disposable_generator_set_state": true,
67
+ "valid_checkpoint_bytes_changed": false
68
+ },
69
+ "predecessor_r3": {
70
+ "loader": {
71
+ "bytes": 4013,
72
+ "sha256": "e99f4556a8e3b3721d1f9155738d2bf4e9765e43a0e9f7734259e33533ff4b30"
73
+ },
74
+ "receipt": {
75
+ "bytes": 2464,
76
+ "sha256": "bbce190f463159fe5ed7f80d317ec84e93c08d3119bcd0ee343a3793a13cd8ba"
77
+ },
78
+ "runtime": {
79
+ "bytes": 36449,
80
+ "sha256": "a81dd58c727a28b1a383477338e41922a2d746dac5cb33f351b4501ee8c5d0dd"
81
+ },
82
+ "test": {
83
+ "bytes": 22249,
84
+ "sha256": "0d9c387ff7d7314ca5645dd0fc052bd3a4f814637094c5901e5c7a51078ad07b"
85
+ }
86
+ },
87
+ "r13_nonmutation": {
88
+ "frozen_runner": {
89
+ "bytes": 55423,
90
+ "sha256": "f5d5eceff5377c7146f9ee000559483f2779117661945342fd112059a1c7a6b1"
91
+ },
92
+ "frozen_test": {
93
+ "bytes": 49920,
94
+ "sha256": "765b20221105571ae8aa031d7926130ea604172824472b633d404ff1f28abaf5"
95
+ },
96
+ "current_frozen_r13_accepts_r4_receipt": false,
97
+ "integration_note": "R4 intentionally omits R3's broad cursor-prevalidation claim and exposes the narrower model/optimizer boundary; a future no-overwrite R13 successor must bind this exact R4 surface."
98
+ },
99
+ "schema": "bertose_review_a5_native_eight_hotpath_integrity_receipt_20260726_r4",
100
+ "status": "PASS_LOCAL_EXACT_INTEGRITY_SUCCESSOR_PREPARATION_ONLY_LAUNCH_CLOSED",
101
+ "tests": {
102
+ "certification_process_order": [
103
+ "fresh_interpreter_1_frozen_r3_8_of_8_pass_warning_fatal",
104
+ "fresh_interpreter_2_frozen_r13_invocation_collected_no_tests_due_missing_PYTHONPATH",
105
+ "fresh_interpreter_3_frozen_r13_19_of_19_pass_warning_fatal_with_PYTHONPATH",
106
+ "fresh_interpreter_4_r4_7_of_7_pass_warning_fatal",
107
+ "fresh_interpreter_5_r4_after_final_provenance_revision_7_of_7_pass_warning_fatal"
108
+ ],
109
+ "fresh_interpreter_note": "Each listed command was a separate OS Python process. The first R13 invocation was an environment-path invocation error, not a test failure; the corrected fresh process collected and passed all 19 tests. The final R4 process ran after replacement of recursive checkpoint fingerprinting with lightweight chain/epoch/plan/run/cell provenance.",
110
+ "frozen_r13_regression": "19/19 PASS warning-fatal",
111
+ "frozen_r3_regression": "8/8 PASS warning-fatal",
112
+ "no_pycache_in_r4_runtime_tree": true,
113
+ "r4_focused": "7/7 PASS warning-fatal"
114
+ }
115
+ }
releases/2026-10-02/bertose_source/writing/review/multimodal/outputs/review_a5_native_eight_hotpath_20260726_r5/review_a5_native_eight_hotpath_r5_integrity_receipt.json ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "authorization": {
3
+ "nova_accessed": false,
4
+ "nova_submission_authorized": false,
5
+ "production_epoch_executed": false,
6
+ "scientific_launch_authorized": false,
7
+ "training_authorized": false
8
+ },
9
+ "closure": {
10
+ "loader": {
11
+ "bytes": 4013,
12
+ "mode": "0444",
13
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r5/load_review_a5_native_eight_hotpath_r5.py",
14
+ "sha256": "864a05e86c5ea91318b11373d2cdffcdd2ddd0777f141cb7ef0305f779a42ea6"
15
+ },
16
+ "runtime": {
17
+ "bytes": 17677,
18
+ "mode": "0444",
19
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r5/review_a5_native_eight_hotpath_r5.py",
20
+ "sha256": "ce4bad9195229341cab0890ed775b78126c17a23d82f2244757f120c12b346c8"
21
+ },
22
+ "test": {
23
+ "bytes": 20423,
24
+ "mode": "0444",
25
+ "path": "review/multimodal/runtime/a5_native_eight_hotpath_20260726_r5/test_review_a5_native_eight_hotpath_r5.py",
26
+ "sha256": "fb36b4b6e474ecce6c61565a55e50092e6bbe8405d1e4bdd6032af4fdbdacd01"
27
+ }
28
+ },
29
+ "guards": {
30
+ "abandoned_created_or_loaded_checkpoint_gc_cleanup": true,
31
+ "additional_full_model_optimizer_or_checkpoint_state_hashes": 0,
32
+ "checkpoint_bytes_changed": false,
33
+ "commit_success_and_failure_cleanup": true,
34
+ "deterministic_id_reuse_simulation_rejected": true,
35
+ "history_epoch_chain_pair_digest": {
36
+ "domain": "bertose-review-a5-native8-history-chain-pairs-20260726-r5",
37
+ "maximum_rows": 100,
38
+ "scope": "all sequential epoch and step_identity_chain_sha256 pairs"
39
+ },
40
+ "provenance_identity_binding": [
41
+ "strong development_history owner identity",
42
+ "weak Torch CPU RNG tensor identity sentinel",
43
+ "chain",
44
+ "history-pair digest",
45
+ "epoch",
46
+ "plan_sha256",
47
+ "run_identity_sha256",
48
+ "cell_id",
49
+ "trusted manager entry SHA on load"
50
+ ],
51
+ "restore_success_and_failure_cleanup": true,
52
+ "validation_failure_cleanup": true
53
+ },
54
+ "integration": {
55
+ "current_r13_binding_sha256": "c30b6b03dae852a4b10c6d01596e977d8a071e738dc3074f1ca0592a50778dd3",
56
+ "current_r13_runner_sha256": "17cf8ab216b354ec1de7574ebebd7bfe02b5935da605b4131e45c7aa13982d5b",
57
+ "current_r13_test_sha256": "1fb1a9cbf6149f706786301fa57644cde5db57eb068fbc754fc939303f25b425",
58
+ "exact_source_chain": [
59
+ "R5 exact-byte loader/runtime",
60
+ "current exact R13 runner",
61
+ "R13-pinned exact R12 predecessor",
62
+ "R13-pinned exact R11 release gate",
63
+ "R13-pinned exact R9 runtime"
64
+ ],
65
+ "fresh_process_result": "PASS_EXACT_R5_R13_R12_R11_R9",
66
+ "optimization_kind_on_current_r13_surface": "r4_successor_over_frozen_r3",
67
+ "outer_scientific_launch_authorized": false,
68
+ "predecessor_r3_scientific_launch_authorized": false,
69
+ "predecessor_r4_scientific_launch_authorized": false,
70
+ "terminal_binding_caveat": "The current binding references an absent live terminal-binding file. The integration regression used one isolated read-only terminal-binding fixture, removed automatically at process exit; no production authority or project output was created."
71
+ },
72
+ "predecessor_r4": {
73
+ "loader": {
74
+ "bytes": 4013,
75
+ "sha256": "662d0766a8d68638480e9c44ca4070387481d43e1c666a20363025cf5b9bc958"
76
+ },
77
+ "receipt": {
78
+ "bytes": 5728,
79
+ "sha256": "9aa5860bfc032afd82561a6db6f8df1439245b73aa3f7575b2b88836e384a1f9"
80
+ },
81
+ "runtime": {
82
+ "bytes": 23798,
83
+ "sha256": "600af4b6b0fab63b7780bb1e6be8233eb28bd0808ff390c3096369afec9c6649"
84
+ },
85
+ "test": {
86
+ "bytes": 19081,
87
+ "sha256": "ebbbdb35644e84b566278fec702afdf7bb23b77cc0b9da7dec226d68feb0504d"
88
+ }
89
+ },
90
+ "schema": "bertose_review_a5_native_eight_hotpath_integrity_receipt_20260726_r5",
91
+ "status": "PASS_LOCAL_PROVENANCE_LIFECYCLE_SUCCESSOR_PREPARATION_ONLY_LAUNCH_CLOSED",
92
+ "tests": {
93
+ "exact_loader_mutable_copy_negative": true,
94
+ "focused_warning_fatal": "8/8 PASS",
95
+ "fresh_process_integration": true,
96
+ "no_pycache_in_r5_runtime_tree": true
97
+ }
98
+ }