Aleksei Ustimenko commited on
Commit
5ccb4fd
·
0 Parent(s):

Initial HamiltonZero release

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +38 -0
  2. .gitignore +13 -0
  3. LICENSE +202 -0
  4. NOTICE +5 -0
  5. README.md +304 -0
  6. THIRD_PARTY_NOTICES.md +13 -0
  7. datasets/README.md +38 -0
  8. datasets/eval/j1j2_45x45_obc.json +3 -0
  9. datasets/eval/j1j2_64x64_obc.json +3 -0
  10. datasets/eval/j1j2_90x90_obc.json +3 -0
  11. datasets/eval/ppp_ohno_n1024.json +3 -0
  12. datasets/eval/ppp_ohno_n256.json +3 -0
  13. datasets/eval/ppp_ohno_n512.json +3 -0
  14. datasets/eval/rudy_n1024.json +3 -0
  15. datasets/eval/rudy_n256.json +3 -0
  16. datasets/eval/rudy_n512.json +3 -0
  17. datasets/eval/triangular_42x42_pbc.json +3 -0
  18. datasets/train/foundation_5000.jsonl +3 -0
  19. examples/compiled_inference.py +71 -0
  20. examples/eval.json +5 -0
  21. examples/eval_large_n.json +5 -0
  22. examples/finetune.json +6 -0
  23. examples/j1j2_4x4_route.ipynb +205 -0
  24. examples/networkx_system.py +16 -0
  25. examples/train.json +6 -0
  26. pyproject.toml +47 -0
  27. src/hamiltonzero/__init__.py +40 -0
  28. src/hamiltonzero/checkpoint.py +65 -0
  29. src/hamiltonzero/cli.py +69 -0
  30. src/hamiltonzero/compiled/__init__.py +2 -0
  31. src/hamiltonzero/compiled/api.py +34 -0
  32. src/hamiltonzero/compiled/execute.py +118 -0
  33. src/hamiltonzero/compiled/model.py +456 -0
  34. src/hamiltonzero/compiled/tree.py +582 -0
  35. src/hamiltonzero/compiled/trunk.py +129 -0
  36. src/hamiltonzero/compiled/types.py +169 -0
  37. src/hamiltonzero/config.py +301 -0
  38. src/hamiltonzero/data/__init__.py +22 -0
  39. src/hamiltonzero/data/systems.py +264 -0
  40. src/hamiltonzero/energy/__init__.py +22 -0
  41. src/hamiltonzero/energy/compiled.py +57 -0
  42. src/hamiltonzero/energy/custom_lap.py +1491 -0
  43. src/hamiltonzero/energy/frame.py +152 -0
  44. src/hamiltonzero/energy/kernel.py +196 -0
  45. src/hamiltonzero/evaluation/__init__.py +42 -0
  46. src/hamiltonzero/evaluation/backend.py +148 -0
  47. src/hamiltonzero/evaluation/greedy_router.py +125 -0
  48. src/hamiltonzero/evaluation/large_n.py +371 -0
  49. src/hamiltonzero/evaluation/pallas_mha.py +164 -0
  50. src/hamiltonzero/evaluation/runner.py +509 -0
.gitattributes ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ datasets/train/*.jsonl filter=lfs diff=lfs merge=lfs -text
37
+ datasets/eval/*.json filter=lfs diff=lfs merge=lfs -text
38
+ weights/*.eqx filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ outputs/
8
+ weights/
9
+ *.eqx
10
+ *.hzc
11
+ *.zst
12
+ !weights/
13
+ !weights/hamiltonzero_v1.eqx
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ HamiltonZero
2
+ Copyright 2026 Simulacra Research Inc.
3
+
4
+ This product includes third-party software. See THIRD_PARTY_NOTICES.md for
5
+ attribution and license information.
README.md ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ ---
4
+
5
+ # HamiltonZero
6
+
7
+ HamiltonZero is Simulacra Research's research release for compiled neural
8
+ wavefunctions of quantum spin Hamiltonians. It exposes three workflows:
9
+
10
+ - learned-router multisystem training;
11
+ - compiled single-system fine-tuning;
12
+ - compiled single-system evaluation, with optional router contest or large-N
13
+ execution.
14
+
15
+ ## Installation
16
+
17
+ HamiltonZero requires Python 3.12 and JAX-compatible accelerator drivers.
18
+
19
+ ```bash
20
+ python -m pip install .
21
+ ```
22
+
23
+ The package pins the Python `jax` package to a
24
+ [`TakeOver/jax` commit](https://github.com/TakeOver/jax/commit/79f82535b15a444516d4a5e2beb71d283665b2ff),
25
+ also published as `hamiltonzero-jax-v0.11.0-spin.1`, and pins
26
+ `jaxlib==0.11.0`. The fork contains the symbolic-zero JVP support used by the
27
+ tuned Pallas attention kernel; stock Python JAX 0.11.0 is not sufficient for
28
+ that pathway. Install the accelerator plugin appropriate for the host using
29
+ the standard JAX instructions.
30
+
31
+ Learned-router training uses eight visible accelerators and requires an MCMC
32
+ batch size divisible by eight. Fine-tuning uses all visible accelerators and
33
+ requires its MCMC batch size to be divisible by their count. Evaluation chooses
34
+ a visible-device subset compatible with its walker batch.
35
+
36
+ ## Foundation checkpoint
37
+
38
+ This Hugging Face repository stores the directly loadable HamiltonZero v1
39
+ foundation checkpoint at `weights/hamiltonzero_v1.eqx`. To download the
40
+ checkpoint without cloning the repository:
41
+
42
+ ```bash
43
+ hf download simulacra-research/HamiltonZero \
44
+ weights/hamiltonzero_v1.eqx \
45
+ --local-dir .
46
+ ```
47
+
48
+ The checkpoint contains the complete foundation wavefunction and its learned
49
+ router. `router` is the checkpoint kind, not a router-only parameter file.
50
+
51
+ To load the model directly, construct an architecture template and deserialize
52
+ its array leaves:
53
+
54
+ ```python
55
+ import jax
56
+
57
+ from hamiltonzero.checkpoint import load_model
58
+ from hamiltonzero.config import ModelConfig
59
+ from hamiltonzero.model import build_model
60
+
61
+ template = build_model(
62
+ ModelConfig(),
63
+ jax.random.PRNGKey(0),
64
+ n_max=64,
65
+ )
66
+ model = load_model("weights/hamiltonzero_v1.eqx", template)
67
+ ```
68
+
69
+ The template key initializes placeholder values only; deserialization replaces
70
+ all serialized array leaves. Set `n_max` to the padded width of the system when
71
+ constructing a template for direct model use. The command-line evaluation path
72
+ does this from the input system automatically.
73
+
74
+ ## Hamiltonians and NetworkX
75
+
76
+ The public API follows the textbook convention
77
+
78
+ \[
79
+ H = \sum_{i<j} S_i^T J_{ij} S_j + \sum_i h_i^T S_i,
80
+ \qquad S=\sigma/2.
81
+ \]
82
+
83
+ Construct and save a system from a simple undirected NetworkX graph:
84
+
85
+ ```python
86
+ from pathlib import Path
87
+
88
+ import networkx as nx
89
+
90
+ from hamiltonzero import SpinHamiltonian
91
+ from hamiltonzero.data import save_system
92
+
93
+ graph = nx.path_graph(8)
94
+ nx.set_edge_attributes(graph, 1.0, "J")
95
+ nx.set_node_attributes(graph, 0.0, "h")
96
+
97
+ system = SpinHamiltonian.from_networkx(graph)
98
+ save_system(Path("outputs/systems/chain_8.json"), system)
99
+ ```
100
+
101
+ The same example is runnable as `python examples/networkx_system.py`. An edge
102
+ `J` may be an isotropic scalar, a length-three diagonal, or a 3-by-3 exchange
103
+ matrix. A node `h` may be a scalar z-field or a length-three field vector.
104
+ `SpinHamiltonian.from_arrays` accepts dense arrays instead.
105
+
106
+ HamiltonZero converts public inputs to the model's internal `-J/2` and `-h`
107
+ representation. The `SpinHamiltonian.J` and `SpinHamiltonian.h` properties
108
+ return the public textbook values. If `mu` is omitted, a conservative value is
109
+ computed from the Hamiltonian.
110
+
111
+ ## Standalone compiled inference
112
+
113
+ The compact inference API loads the foundation checkpoint, runs the
114
+ beam-8 router, permutes the Hamiltonian, and compiles the selected physical
115
+ wavefunction in one call:
116
+
117
+ ```python
118
+ import jax
119
+ import networkx as nx
120
+
121
+ from hamiltonzero import SpinHamiltonian, burn_in, energy, prepare, spin, step
122
+
123
+ graph = nx.path_graph(8)
124
+ nx.set_edge_attributes(graph, 1.0, "J")
125
+ system = SpinHamiltonian.from_networkx(graph)
126
+
127
+ route_key, mcmc_key = jax.random.split(jax.random.PRNGKey(0))
128
+ compiled, order = prepare(
129
+ system,
130
+ "weights/hamiltonzero_v1.eqx",
131
+ route_key,
132
+ )
133
+ state, q = burn_in(
134
+ compiled,
135
+ mcmc_key,
136
+ batch_size=256,
137
+ replicas=8,
138
+ burn_in=1024,
139
+ walker_chunk_size=16,
140
+ )
141
+ local_energy = energy(compiled, q)
142
+ local_spin = spin(compiled, q)
143
+ state, q = step(compiled, state, steps=24, walker_chunk_size=16)
144
+ ```
145
+
146
+ `state` is the complete replica-exchange MCMC state and `q` is its cold-chain
147
+ population. `energy` returns named `total`, `exchange`, `casimir`, and `field`
148
+ local-energy samples. `spin` returns the complex local spin estimator in the
149
+ routed `(site, x/y/z)` order; contracting it with the routed public field
150
+ reproduces the energy field channel. The selected padded-site permutation is
151
+ returned as `order`. `order.leaf_to_input[leaf]` is the public input-site index
152
+ assigned to a compiled tree leaf; `order.input_to_leaf[site]` is its inverse.
153
+ The first mapping is also available as `compiled.route`. The public NetworkX
154
+ path starts in exactly the supplied `system.nodes` order, applies this route
155
+ once to the context and walkers, and then compiles an identity-routed tree.
156
+ There is no additional bit reversal: applying one would corrupt the mapping.
157
+ Both arrays include padded virtual leaves when the model width exceeds the
158
+ physical site count. A complete runnable version that prints sample means and
159
+ standard deviations is in
160
+ [`examples/compiled_inference.py`](examples/compiled_inference.py).
161
+ [`examples/j1j2_4x4_route.ipynb`](examples/j1j2_4x4_route.ipynb) constructs a
162
+ periodic 4-by-4 J1-J2 model from NetworkX and visualizes the returned order as
163
+ the successive cells of the compiled binary merge tree. Install its plotting
164
+ dependencies with `python -m pip install '.[notebooks]'`.
165
+
166
+ For a normalized pure state, the full-state `Tr(|psi><psi|)` is exactly one.
167
+ The nontrivial purity observable is the subsystem second Renyi value
168
+ `Tr(rho_A^2)`. It uses two independent computational-basis chains and a
169
+ two-replica SWAP estimator:
170
+
171
+ ```python
172
+ import jax
173
+
174
+ from hamiltonzero import burn_in_basis, measure_renyi2
175
+
176
+ x_key, y_key = jax.random.split(jax.random.PRNGKey(1))
177
+ x_state, x = burn_in_basis(compiled, x_key, batch_size=256, burn_in=1024)
178
+ y_state, y = burn_in_basis(compiled, y_key, batch_size=256, burn_in=1024)
179
+ x_state, y_state, result = measure_renyi2(
180
+ compiled,
181
+ x_state,
182
+ y_state,
183
+ subsystem=range(4),
184
+ blocks=16,
185
+ steps_between=24,
186
+ )
187
+ print(result.purity, result.standard_error, result.renyi2_nats)
188
+ print(result.resolved, result.failure_reasons)
189
+ ```
190
+
191
+ `subsystem` accepts public site indices or a boolean mask. The result also
192
+ retains each SWAP sample in stable log-polar form. Entropy is reported only
193
+ when the block-count, effective-sample-size, autocorrelation,
194
+ heavy-tail, imaginary-null, and physical-bound checks resolve the estimate;
195
+ otherwise `renyi2_nats` is `None` and `failure_reasons` says why.
196
+ `burn_in_basis` samples the computational basis required by this estimator.
197
+ The SU(2)-quaternion walkers returned by `burn_in` cannot be substituted for
198
+ those samples.
199
+
200
+ ## Datasets
201
+
202
+ The repository includes the exact 5,000-system foundation training panel and
203
+ the evaluation systems with at least 256 physical spins. Every file uses the
204
+ public textbook units above.
205
+
206
+ - `datasets/train/foundation_5000.jsonl` contains systems from 2 through 64
207
+ spins, fixed WL1/FWL2 dispatch, and available exact-diagonalization energies.
208
+ - `datasets/eval/` contains the PPP-Ohno, RUDY, square-lattice J1-J2, and
209
+ triangular-Heisenberg evaluation systems from 256 through 8,100 physical
210
+ spins.
211
+
212
+ Large-N files store physical sites only; the loader reconstructs power-of-two
213
+ padding in memory. See [`datasets/README.md`](datasets/README.md) for the full
214
+ inventory and sparse exchange encoding.
215
+
216
+ ## Train
217
+
218
+ The training command starts a new learned-router multisystem run and writes one
219
+ final full foundation-model checkpoint, including its learned router, plus a
220
+ metadata sidecar:
221
+
222
+ ```bash
223
+ hamiltonzero train examples/train.json
224
+ ```
225
+
226
+ The example uses `datasets/train/foundation_5000.jsonl`, writes
227
+ `outputs/foundation.eqx`, and exposes model, MCMC, KFAC, router, and energy
228
+ parameters through JSON. The command writes the trained model at the end of
229
+ the run.
230
+
231
+ To skip burn-in using compatible post-burn-in sampler states:
232
+
233
+ ```bash
234
+ hamiltonzero train examples/train.json --reuse-mcmc path/to/mcmc-states
235
+ ```
236
+
237
+ For multisystem training, the path is a directory containing
238
+ `<system-index>.eqx` files. For a one-system training panel it may be a single
239
+ file.
240
+
241
+ ## Fine-tune
242
+
243
+ Fine-tuning selects and freezes a route from a router checkpoint, compiles the
244
+ single-system wavefunction, and optimizes that compiled model:
245
+
246
+ ```bash
247
+ hamiltonzero finetune examples/finetune.json
248
+ ```
249
+
250
+ The example fine-tunes on the 256-spin PPP-Ohno system and writes
251
+ `outputs/ppp_ohno_n256.eqx`. A neighboring `.eqx.json` sidecar records the
252
+ compiled-fine-tune kind, frozen model width, and configured ranks. A compatible
253
+ single post-burn-in state can also be supplied:
254
+
255
+ ```bash
256
+ hamiltonzero finetune examples/finetune.json --reuse-mcmc path/to/state.eqx
257
+ ```
258
+
259
+ ## Evaluate
260
+
261
+ Compiled evaluation uses the route selected by a router checkpoint, or the
262
+ embedded frozen route in a compiled fine-tune checkpoint:
263
+
264
+ ```bash
265
+ hamiltonzero eval examples/eval.json
266
+ ```
267
+
268
+ Use router contest to compare candidate routes before evaluating the winner:
269
+
270
+ ```bash
271
+ hamiltonzero eval examples/eval.json --contest
272
+ ```
273
+
274
+ Use the sequence-sharded large-N implementation for the large systems:
275
+
276
+ ```bash
277
+ hamiltonzero eval examples/eval_large_n.json --large-n
278
+ ```
279
+
280
+ Each evaluation writes `eval.json` and `eval.metrics.jsonl` inside its
281
+ configured output directory.
282
+
283
+ Training and fine-tuning metrics are written beside the final checkpoint as
284
+ `<checkpoint>.metrics.jsonl`. Evaluation writes the same per-measurement fields
285
+ to `eval.metrics.jsonl`. These JSONL rows contain step, energy, energy standard
286
+ deviation, step wall time, and total wall time. Final `eval.json` additionally
287
+ reports exchange/field channels and lag-one autocorrelation when available.
288
+
289
+ ## Configuration
290
+
291
+ Every command accepts one JSON configuration. The files in `examples/` are
292
+ minimal runnable configurations; omitted parameters use the defaults in
293
+ `hamiltonzero.config`.
294
+
295
+ The KFAC-JAX fork is vendored under `src/kfac_jax`.
296
+
297
+ ## License
298
+
299
+ HamiltonZero first-party source, datasets, and released model weights are
300
+ licensed under Apache-2.0, copyright Simulacra Research Inc. The vendored
301
+ KFAC-JAX fork and JAX-derived large-N attention kernel remain under
302
+ Apache-2.0. The Microsoft-Folx-derived attention forward and reverse-mode
303
+ kernels remain under MIT. See
304
+ [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ The KFAC-JAX fork under `src/kfac_jax`, including modifications copyright
4
+ 2026 Simulacra Research Inc., is licensed under Apache-2.0. Its license is in
5
+ `third_party/kfac_jax/LICENSE`.
6
+
7
+ The attention forward and reverse-mode kernels derived from Microsoft Folx,
8
+ with modifications copyright 2026 Simulacra Research Inc., are licensed
9
+ under MIT. Its license is in `third_party/folx/LICENSE`.
10
+
11
+ The large-N Pallas attention kernel derived from JAX, with modifications
12
+ copyright 2026 Simulacra Research Inc., is licensed under Apache-2.0. Its
13
+ license is in `third_party/jax/LICENSE`.
datasets/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Datasets
2
+
3
+ All files use the public textbook convention
4
+
5
+ \[
6
+ H=\sum_{i<j}S_i^T J_{ij}S_j+\sum_i h_i^T S_i,\qquad S=\sigma/2.
7
+ \]
8
+
9
+ Sparse exchange terms are encoded as `[i, j, J]`, where `J` is a scalar,
10
+ a length-three diagonal, or a 3-by-3 matrix. Missing fields are zero.
11
+ HamiltonZero expands this representation when loading a system.
12
+
13
+ `train/foundation_5000.jsonl` contains the 5,000 systems used by the released
14
+ foundation checkpoint, with sizes from 2 to 64 spins. Each row includes the
15
+ fixed WL1/FWL2 dispatch and, where available, `e_ed` in textbook energy units.
16
+ There are 3,169 non-null exact-diagonalization references.
17
+
18
+ The N≥256 evaluation systems are:
19
+
20
+ | File | Physical spins | Family |
21
+ |---|---:|---|
22
+ | `eval/ppp_ohno_n256.json` | 256 | PPP–Ohno C128H130 |
23
+ | `eval/ppp_ohno_n512.json` | 512 | PPP–Ohno C256H258 |
24
+ | `eval/ppp_ohno_n1024.json` | 1,024 | PPP–Ohno C512H514 |
25
+ | `eval/rudy_n256.json` | 256 | RUDY-12 MaxCut |
26
+ | `eval/rudy_n512.json` | 512 | RUDY-12 MaxCut |
27
+ | `eval/rudy_n1024.json` | 1,024 | RUDY-12 MaxCut |
28
+ | `eval/j1j2_45x45_obc.json` | 2,025 | square-lattice J1–J2 |
29
+ | `eval/triangular_42x42_pbc.json` | 1,764 | triangular Heisenberg |
30
+ | `eval/j1j2_64x64_obc.json` | 4,096 | square-lattice J1–J2 |
31
+ | `eval/j1j2_90x90_obc.json` | 8,100 | square-lattice J1–J2 |
32
+
33
+ Each evaluation record stores its fixed WL1 routing dispatch.
34
+
35
+ All stored couplings and reference energies use the textbook convention above.
36
+ Padding is reconstructed in memory and is not stored as zero-valued sites.
37
+
38
+ These datasets are distributed under the repository Apache-2.0 license.
datasets/eval/j1j2_45x45_obc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:524a4ab53dea1f05fab99b827f4e8aec8d991ac160c58a59b10d07a094ec2730
3
+ size 117093
datasets/eval/j1j2_64x64_obc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5177badfdd22af24182fc3a8ff358c4639457f0e6ed4bc2c332e818adb602f6
3
+ size 247879
datasets/eval/j1j2_90x90_obc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33ea4692401413b9afbf8caf5a02efceae1f2a9e16d3d38df4137d0913cf9315
3
+ size 501771
datasets/eval/ppp_ohno_n1024.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c5934d263227de2520b7fe9843de59aa493528f4666c2bf996eb97d39743e9f
3
+ size 21848168
datasets/eval/ppp_ohno_n256.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:07e3dc5b262c7429bfa8cc7d08d561d7aa32a6031a8b68b5944fda84a124362e
3
+ size 1322178
datasets/eval/ppp_ohno_n512.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c346bbf5ed672d18a782f6cc2753416f10a141fbf5bb38d0977c67bca818d2e9
3
+ size 5385846
datasets/eval/rudy_n1024.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5bff99762c67a1ffda145f96da213475ae3f8280ae89d5b2022ed86a34182918
3
+ size 1623829
datasets/eval/rudy_n256.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6dae5999b8f02b7e513c581cec5469549606bc3cfee95dce732ab7c06d33d9a7
3
+ size 98782
datasets/eval/rudy_n512.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0416837f2d2bcb7421b2c8f483bcf9413365cc0f0c27c6d5c11288bad656b2ad
3
+ size 401666
datasets/eval/triangular_42x42_pbc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:347f7dc3d215ef5c6f422aafb3bfe1dac7a5c9c01b954903ad6b7bcfecad85be
3
+ size 78234
datasets/train/foundation_5000.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba07575057cc04fe69edebbf1c2083a25e6920991e6e7c606bbc994200744e48
3
+ size 17788002
examples/compiled_inference.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from pathlib import Path
5
+
6
+ import jax
7
+ import jax.numpy as jnp
8
+ import networkx as nx
9
+ import numpy as np
10
+
11
+ from hamiltonzero import (
12
+ SpinHamiltonian,
13
+ burn_in,
14
+ burn_in_basis,
15
+ energy,
16
+ measure_renyi2,
17
+ prepare,
18
+ spin,
19
+ step,
20
+ step_basis,
21
+ )
22
+
23
+
24
+ graph = nx.path_graph(8)
25
+ nx.set_edge_attributes(graph, 1.0, "J")
26
+ system = SpinHamiltonian.from_networkx(graph)
27
+
28
+ route_key, mcmc_key, basis_x_key, basis_y_key = jax.random.split(
29
+ jax.random.PRNGKey(0), 4
30
+ )
31
+ compiled, order = prepare(
32
+ system,
33
+ Path("weights/hamiltonzero_v1.eqx"),
34
+ route_key,
35
+ )
36
+ state, q = burn_in(
37
+ compiled,
38
+ mcmc_key,
39
+ batch_size=256,
40
+ replicas=8,
41
+ burn_in=1024,
42
+ walker_chunk_size=16,
43
+ )
44
+
45
+ local_energy = energy(compiled, q)
46
+ local_spin = spin(compiled, q)
47
+ print("leaf_to_input", np.asarray(order.leaf_to_input))
48
+ print("input_to_leaf", np.asarray(order.input_to_leaf))
49
+ print("energy", float(jnp.mean(local_energy.total.real)))
50
+ print("energy_std", float(jnp.std(local_energy.total.real)))
51
+ print("spin", np.asarray(jnp.mean(local_spin.real, axis=0)))
52
+
53
+ state, q = step(compiled, state, steps=24, walker_chunk_size=16)
54
+
55
+ basis_x, bits_x = burn_in_basis(compiled, basis_x_key, batch_size=256, burn_in=1024)
56
+ basis_y, bits_y = burn_in_basis(compiled, basis_y_key, batch_size=256, burn_in=1024)
57
+ basis_x, bits_x = step_basis(compiled, basis_x, steps=24)
58
+ basis_y, bits_y = step_basis(compiled, basis_y, steps=24)
59
+ basis_x, basis_y, purity = measure_renyi2(
60
+ compiled,
61
+ basis_x,
62
+ basis_y,
63
+ subsystem=range(4),
64
+ blocks=16,
65
+ samples_per_block=1,
66
+ steps_between=24,
67
+ )
68
+ print("purity", purity.purity)
69
+ print("purity_standard_error", purity.standard_error)
70
+ print("purity_resolved", purity.resolved, purity.failure_reasons)
71
+ print("renyi2_nats", purity.renyi2_nats)
examples/eval.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "system": "datasets/eval/ppp_ohno_n256.json",
3
+ "checkpoint": "weights/hamiltonzero_v1.eqx",
4
+ "output": "outputs/ppp_ohno_n256_eval"
5
+ }
examples/eval_large_n.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "system": "datasets/eval/j1j2_45x45_obc.json",
3
+ "checkpoint": "weights/hamiltonzero_v1.eqx",
4
+ "output": "outputs/j1j2_45x45_eval"
5
+ }
examples/finetune.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "system": "datasets/eval/ppp_ohno_n256.json",
3
+ "checkpoint": "weights/hamiltonzero_v1.eqx",
4
+ "output": "outputs/ppp_ohno_n256.eqx",
5
+ "steps": 1000
6
+ }
examples/j1j2_4x4_route.ipynb ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "license",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "# Copyright (c) 2026 Simulacra Research Inc.\n",
11
+ "# SPDX-License-Identifier: Apache-2.0"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "markdown",
16
+ "id": "title",
17
+ "metadata": {},
18
+ "source": [
19
+ "# Learned merge-tree order for a 4×4 J1–J2 torus\n",
20
+ "\n",
21
+ "The public Hamiltonian is built in textbook units. `prepare` loads HamiltonZero v1, selects the beam-8 route, compiles the wavefunction, and returns both directions of the mapping between public input sites and binary-tree leaves."
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": 2,
27
+ "id": "imports",
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "from pathlib import Path\n",
32
+ "\n",
33
+ "import jax\n",
34
+ "import matplotlib.pyplot as plt\n",
35
+ "from matplotlib.colors import ListedColormap\n",
36
+ "import networkx as nx\n",
37
+ "import numpy as np\n",
38
+ "\n",
39
+ "from hamiltonzero import SpinHamiltonian, prepare"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": 3,
45
+ "id": "hamiltonian",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "L = 4\n",
50
+ "J1 = 1.0\n",
51
+ "J2 = 0.3\n",
52
+ "nodes = tuple((row, column) for row in range(L) for column in range(L))\n",
53
+ "graph = nx.Graph()\n",
54
+ "graph.add_nodes_from(nodes)\n",
55
+ "for row, column in nodes:\n",
56
+ " graph.add_edge((row, column), (row, (column + 1) % L), J=J1)\n",
57
+ " graph.add_edge((row, column), ((row + 1) % L, column), J=J1)\n",
58
+ " graph.add_edge((row, column), ((row + 1) % L, (column + 1) % L), J=J2)\n",
59
+ " graph.add_edge((row, column), ((row + 1) % L, (column - 1) % L), J=J2)\n",
60
+ "graph.graph[\"needs_fwl2\"] = False\n",
61
+ "system = SpinHamiltonian.from_networkx(graph, nodes=nodes)"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": 4,
67
+ "id": "prepare",
68
+ "metadata": {},
69
+ "outputs": [
70
+ {
71
+ "name": "stdout",
72
+ "output_type": "stream",
73
+ "text": [
74
+ "leaf input lattice site\n",
75
+ " 0 0 (0, 0)\n",
76
+ " 1 1 (0, 1)\n",
77
+ " 2 2 (0, 2)\n",
78
+ " 3 3 (0, 3)\n",
79
+ " 4 7 (1, 3)\n",
80
+ " 5 6 (1, 2)\n",
81
+ " 6 5 (1, 1)\n",
82
+ " 7 4 (1, 0)\n",
83
+ " 8 8 (2, 0)\n",
84
+ " 9 9 (2, 1)\n",
85
+ " 10 10 (2, 2)\n",
86
+ " 11 11 (2, 3)\n",
87
+ " 12 15 (3, 3)\n",
88
+ " 13 14 (3, 2)\n",
89
+ " 14 13 (3, 1)\n",
90
+ " 15 12 (3, 0)\n"
91
+ ]
92
+ }
93
+ ],
94
+ "source": [
95
+ "route_key = jax.random.PRNGKey(0)\n",
96
+ "compiled, order = prepare(\n",
97
+ " system,\n",
98
+ " Path(\"weights/hamiltonzero_v1.eqx\"),\n",
99
+ " route_key,\n",
100
+ ")\n",
101
+ "leaf_to_input = np.asarray(order.leaf_to_input, dtype=np.int32)\n",
102
+ "input_to_leaf = np.asarray(order.input_to_leaf, dtype=np.int32)\n",
103
+ "print(\"leaf input lattice site\")\n",
104
+ "for leaf, site in enumerate(leaf_to_input):\n",
105
+ " print(f\"{leaf:>4} {site:>5} {system.nodes[site]}\")"
106
+ ]
107
+ },
108
+ {
109
+ "cell_type": "markdown",
110
+ "id": "permutation-convention",
111
+ "metadata": {},
112
+ "source": [
113
+ "## Permutation convention\n",
114
+ "\n",
115
+ "`leaf_to_input[leaf]` gives the index in `system.nodes` placed at that compiled leaf. `input_to_leaf[site]` is the inverse used below to color each public lattice site by its merge-tree cell. The NetworkX loader preserves the supplied node order. HamiltonZero applies the learned permutation once to the context and walkers, then compiles an identity-routed tree, so **no extra bit reversal is needed**. For padded systems the arrays also contain virtual leaves, which must remain when determining merge groups even though they are not drawn as physical sites."
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "execution_count": 5,
121
+ "id": "visualize",
122
+ "metadata": {},
123
+ "outputs": [
124
+ {
125
+ "data": {
126
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAB0QAAAGYCAYAAADSo1HRAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAASdAAAEnQB3mYfeAAAhqxJREFUeJzs3Xd0VOX69vErnRZKgFASem8aQIqgotKlK1WpUgQsCCqi6EEEURQVRTgqKCqiHJBmpEiRDhJACAJSJZAAgQABQhJSn/cP38zPIYUJBHZm+H7Wcp3M3s/sueZhnTs7+97FzRhjBNwFzpw5oypVqig2Nlb33HOP+vTpo3Llyuny5cs6duyYfvnlFyUlJengwYN273Nzc1P9+vW1c+dOi5JnbtasWRo8eLDy5Mmjl19+Wffee69CQ0M1ZcoUXbt2Td9884369etndUwAuCWNGjVSSEiI/P39NXDgQNWqVUuSdPz4cW3YsEHr1q1TWFiYAgMDbe9p0aKF1q5dq+joaBUuXNii5BmbNWuWhg0bpvvvv19t27bVhx9+qAsXLuTKrABws8aOHatJkybJx8dHTz75pB544AH5+vrq5MmT2r17t5YuXapp06apf//+tve89957eu2117RgwQJ17drVuvAZeOCBB3TmzBl17NhRVatWVbFixXTgwAFNmzZNFy5cUJ06dRQaGio3NzerowLATVu3bp0effRRSf/sT3fp0kUlSpTQ2bNndfDgQS1evFjNmjXT999/b3vPzp071aBBAz3zzDP6/PPPrYqeocjISJUvX15t27ZVo0aNVLFiRcXGxmrt2rX64YcfZIzR3Llz9eSTT1odFQBu2qVLl1SpUiVdvHhR1apV04ABA1ShQgXFxsbq2LFjWrFihU6fPq0zZ87Yva9w4cIqVqyYjh49alFyxxw7dky1a9dWgwYNtGnTJrVu3VorV660OhaclKfVAYA7ZeHChYqNjVWjRo20ceNGeXt7261/9913FRYWZk24m/TOO+9Ikr755hv16NFDktS1a1fVqlVLvXr10sSJE2mIAnBqBw8eVEhIiHx9fRUSEqJy5crZrX/jjTcUGRmpIkWKWJQw+9q3b69u3bqpUKFCkqTp06dbnAgAct63334rSfruu+/UvXv3dOvj4uJ09erVOx3rpn3//fcqX758uuU9e/ZUvXr19Oeff2rPnj2qW7funQ8HADkkrXYPHjxYX375Zbr1n3zyicLDw+90rJvm5+eniIgIFStWzG75gAEDVL58eb3zzjv6+eefaYgCcGq//PKLLl68qNq1a2v79u3Kly+f3fqJEyc63THvf3v++edVokQJvfHGG2rdurXVceDkaIjirnH27FlJUrt27dI1Q9P8+yDHli1b9O6770qSjh49qvbt29vWPfnkk3Y7zCkpKVq8eLHWrl2rs2fPqlChQnrooYf05JNPysfHxzbu2rVrtobl5MmTtXjxYv3yyy+6cuWKatasqSFDhiggIMCh7xMaGqqwsDBVrFgx3UGmHj166PXXX9fRo0e1f/9+29VUAOBs0mp33bp10zVD05QsWdL2c2xsrHr06KHQ0FBJ/xyo9vT8Z3cnrfb+29atW7VkyRIdP35cnp6eCgoKUr9+/ey2KUkvv/yy7az4P//8U999950iIiJUunRp9e7dWw0bNnT4O12/bQBwRWfPnpWbm5s6d+6c4fp8+fLZHawZOXKk7Uzv9957T998841t3eLFi+Xl5WV7HRYWph9++EH79+9XYmKiKlasqJ49e6ZrRn7//feaN2+exo0bp8DAQM2cOVP79u1T/vz59dhjj6lr164OX9GZUTNUkqpVq6bGjRvrt99+U2RkpEPbAoDcKm3fu0uXLhmud3NzU9myZW2vv/32W82aNUuStGLFCrvjJuPGjVODBg1sr69evaoffvhB27Zt0+XLl1WyZEm1adNGHTp0sKvFf/75p1577TV16tRJffv21TfffKPNmzcrKSlJjRo10pAhQ5Q/f36Hvo+3t3e6Zmia++67T5Lk4eHh0LYAILdKq92tWrVK1wxN8+992dDQUI0dO1axsbFKSkqyq92dOnXS4MGDba+NMfr555+1evVqnTlzRgUKFFCTJk3Up0+fdJ/VqVMnBQYGavr06VqxYoUWL15su2p10KBBqlChQra/26JFi7RixQotWbIk0+8GZIsB7hJff/21kWTatGnj0PgFCxYYSRn+N27cONu48PBwExQUlOG46tWrm5MnT9rGxsTEGEmmadOmZuDAgenGFyxY0GzZssWhfLNnzzaSTP/+/TNc369fPyPJzJ0716HtAUBudPLkSSPJFC9e3Fy8ePGG46OjozOt3U2bNrWNS0xMNH369MlwnK+vr/n111/tttuoUSMjycycOdN4eHjYjXdzczMfffTRTX/HgIAAI8lER0ff9DYAILepWrWqkWTWr1/v0Pj69etnWr/j4+Nt46ZPn268vb3TjXFzczNvv/223TbHjh1rJJnJkyeb4sWLp3tPly5dTFJS0i1/13r16hlJ5siRI7e8LQCw0vDhw40k88Ybbzg0/tVXX820dgcHB9vGbdu2zZQsWTLDca1bt7ar8+vWrTOSzDPPPGPbB//3f5UrVzbh4eG39D0vX75sWrRoYSSZefPm3dK2AMBqixYtMpJMkyZNTEpKyg3Hr169OtPaPWLECNu4s2fPmiZNmmQ4rkKFCubw4cN22/Xw8DC1atUyo0aNSjc+X758ZtWqVdn6XlevXjVlypQx7du3N8YYs2nTJtvvDeBmcYUo7hpdunTR2LFjtXLlSjVu3FjdunVTw4YNFRQUJF9f33TjH3jgAQUHB6tDhw6qXLmyPv74Y9u6qlWrSvrnytCOHTtqz549atmypTp37qwSJUro/PnzmjdvntavX68+ffpo/fr1dtvetWuXtmzZov79+6t58+a6dOmSZs+erT/++EPdu3fXoUOHbnjGY0REhCRlenZN2pk/znQ7GwC4XpkyZdShQwcFBwcrKChIvXv3VpMmTVS/fv0Mr7QsUKCAgoOD9cYbbyg0NFTz5s2z1VM/Pz/buFdffVVz5sxRjRo11LdvX1WsWFHXrl3T+vXrbbd3PHbsmIoWLWq3/eeff15NmzZVr169lDdvXi1btkwLFizQyy+/rCZNmqhRo0a3d0IAwEk8++yzGjFihNq1a6ennnpKDz/8sOrXr68qVapkeFXm1KlTNWvWLH377bcaM2aMmjZtaluXdneX5cuX69lnn1XRokXVv39/BQUFydvbW6Ghofr888/1n//8Rw0aNFCbNm3stj1u3DgVL15cH3zwgQIDA7Vr1y59+umnWrx4saZMmaIxY8bc9Pf87bff9Mcff+jRRx9V5cqVb3o7AJAbDBkyRDNnztQ777yj0NBQtWvXTvfdd5/q1KmT4Z22+vfvr5IlS2rkyJFq27athg8fbluXdgeVM2fOqH379rp8+bKeeuopPfLIIypcuLBOnjypmTNn6tdff9Ubb7yhKVOm2G077fa9o0ePVt26dXXy5El98sknOnr0qPr37681a9Y4/L1SU1PVsWNHSVJ0dLRCQ0OVnJyscePG2R4/BADOqk2bNqpUqZK2bt2q+vXrq2fPnmrYsKHq1atne1TPvwUFBSk4OFg9e/ZUwYIF7W6RXrFiRdvP3bp109atW/XQQw+pa9euKl26tKKjo7Vw4UKtXLlSPXr00K5du+z27dPuVtizZ0+1adNGcXFx+v7777V161b16tVLhw4dSnecJTNvv/22zp8/r2nTpt3C7ADXsbojC9xJu3fvNvfdd5/dGSoeHh6mQYMG5osvvjDJycnp3iPJ1K9fP8PtzZ8/30gyL774Yrp1qamp5rHHHjOSzF9//WWM+b8rRCWZKVOm2I1PSEgwDRo0MJLMrFmzbvhd0s7E/PDDDzNcP2XKlGyd2QkAudWFCxdMjx49jLu7u139rlixonn99dfNhQsX0r2nefPmmV51GRkZaby9vU2DBg3szkZP8+GHHxpJZtq0abZlaWenP/bYY+nOuHzppZeMJNOzZ8+b+n5cIQrAFaWkpJjx48eb/Pnz29VuPz8/07dvX7N///5073n33XeNJLNgwYIMt1mvXj2TL18+8/fff6dbFxoaaiSZTp062ZalXSHq7++f7nfFihUrbOscOZM+IydOnDAlS5Y0vr6+XB0KwGUsWbLElC1b1q5258mTx7Rq1cruqs80O3bssF3RmZHRo0cbSeb7779Pty4mJsZUqFDBFCxY0HY8Ju0KUUlm3bp1duPPnj1r/Pz8jCSzZ88eh79TUlJSuquVOnfubEJDQx3eBgDkZgcPHjRNmza1q3Pu7u6mbt265tNPPzUJCQnp3lOoUCFTqVKlDLe3atUqI8kMGDAgw/W9evUyksy2bdtsy9LupnX9sejk5GTbMZrrj4dnZv/+/cbLy8tMnDjRtowrRJETuEIUd5WgoCDt2LFDe/bssZ3NvXnzZu3YsUM7duzQokWLFBwcbPeMoqz8+uuvkv654rNz584yxkiSjDEyxuj48eOSpH379ql69eq29/n6+uqFF16w25a3t7dGjx6tbt26aePGjRo4cGCWn532TLzk5OQM1yclJdmNAwBn5efnp3nz5mnKlClatWqVQkJCFBISoj179mjSpEmaM2eONm3alOkzRq+3bt06JSYmKiYmRj179pT0f3Vbki5duiTpn9p9vddff13u7u52y8aOHasPP/xQGzduvIVvCQCuxd3dXf/5z380atQorVq1Stu2bdMff/yhrVu36rvvvtP//vc/LViwQB06dHBoe+fPn9cff/yhokWLauTIkZLsa7cxRl5eXhnW7uHDh9vdJUD650z6+vXra9euXTp06JBq1KiRre8XGRmpVq1aKTo6WsHBwVwdCsBldOrUSe3bt9fmzZu1ceNG23GTVatWadWqVRo9erQmT57s8PbSjpuk1f3rj5tcu3ZNV65cUXh4uN0z7h544AE9/PDDdtvy9/fX4MGDNXnyZG3cuFH33nuvQxk8PDwUHBwsY4zOnj2rDRs2aN68efr111+1YsUKNWvWzOHvAwC5UbVq1bR582bt379fa9eutd2dcPfu3dq9e7f+97//afXq1cqbN69D20ur3X/99Ve6Y96SFBYWJumf4yaNGze2vc/DwyPd3VfSlq1du1YbN27USy+9dMPPf/bZZ1WhQgW98sorDuUFHEWnBHeloKAgBQUF2V5v3LhR3bt316+//qpvv/1WgwYNcmg7abet3bRpU5bjYmNj7V5XqlQpw6ZrWtP0zJkzN/zstFseXLhwIcP1Fy9elCQVLlz4htsCAGcQGBiop59+Wk8//bSkf3bA+/Tpo82bN+vVV1/VvHnzHNpOWu0+ePCgDh48mOm462u3JLuTW9IUKVJEJUqUUGRkpEOfDwB3kwIFCujxxx/X448/Lkm6evWqXnvtNX322WcaPHiwIiIiHDqBL612X7hwQUuXLs10nKO1O235rl27dObMmWw1RE+fPq1HH31Uf//9txYtWqSWLVs6/F4AcAYeHh5q1qyZrVFojNGcOXM0cOBAvf/+++rZs6fq1q3r0LbS6ndwcHCW466v31nVbsmx4yZp3Nzc1L59e9vrQYMGqU2bNurdu7deeukl7dy50+FtAUBuVqtWLdWqVcv2OiQkRN27d9eWLVs0ffp0vfzyyw5tJ612//7771mOu752lylTJsPHwGWndn///fdav369Vq9eneHt2oFbQUMUkPTQQw/plVde0csvv6w1a9Y43BBNO3gzZcoUVatWLdNx/26+SlJMTEyG49KW+/j43PCz055jun///gzXpy2vUqXKDbcFAM6ofPny+uyzzxQUFJStZwil1e4+ffqoe/fumY4LDAxMtywmJibD511cvXrV4bsLAMDdrECBAvr00081f/58nT17Vvv27Uu3r5yRtNpdp04dTZo0KdNxefLkSbcsJ/a905w8eVLNmzfXiRMnNH/+fLsD7ADgqtzc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7dry8vJSp06dNHjwYEnS1KlTNXLkSElSsWLFVLVqVXl5eSksLEzh4eFKTU1VfHy87ZEVnp6e8vf3V2JiouLj43XvvfcqLi5O+/fvV3Jyspo3b67Vq1fLzc3tpr7n5s2b9eCDD6p169ZauXLlrUwZ7mYGuEuEhoaaQYMGmRIlShhJdv95eXmZrl27mrCwsHTv27VrlyldurTd+HHjxtmN+eabb0z58uXTbbdq1armvffes42LiYkxkkzTpk3NmjVrTMmSJW1j3dzczJNPPmni4uKy9b02b95sqlSpku5zt27delPzBAC5yYULF8xrr71matSoka7GSjINGzY0v/32W7r3Xb582TRp0sRubNOmTe3GbN++3TRt2jTdNv38/Mxzzz1noqKibGMbNWpkJJlDhw6Zhg0b2o2vX7++OXz4sMPfKTw8PMPv8u//VqxYcfOTBgC5wKeffmqaNWtmPDw80tW4smXLms8++yzD9z3//PPG3d3dbnx8fLxtfWRkpHnyySeNp6en3Rhvb2/z+OOPm127dtnGjh071kgyCxcuNP3797fbbokSJczPP//s8PepVKnSDWv3zJkzb37CACAXWLlypenataspUKBAuhpXsGBB8/zzz5srV66ke9+8efNM/vz57cYHBwfb1iclJZm33nrLFClSJN12GzdubP73v//Zxq5bt85IMiNGjDDTp083+fLls6v11x+Pycr27dtNp06djJeXl91nuru7mxYtWpg9e/bc0nwBQG5w8OBBM2zYMBMQEJCuxnp4eJj27dubQ4cOpXvfX3/9le549ogRI+zGLFiwwFStWjXdditUqGDGjRtnUlJSbGM9PDxMrVq1zNatW03ZsmXtxnfu3Nlcvnz5lr7npk2bjCTTunXrW9oO7m5cIYq7jjFGERERCg8P19WrV+Xn56caNWoof/78mb4nKSlJe/fu1blz55SSkqKqVatmeM/zQ4cOKTw8XPny5VO5cuUUEBBgtz7tCtG0sx+Tk5P1xx9/6MqVK6pRo0a68dn5Tn/++afOnj2rkiVLqk6dOje1HQDIzc6fP6+TJ08qKipK+fPnV7Vq1VS8ePEs33Po0CGdOHFCiYmJ8vPzy/AqoMjISB06dEipqakqW7asypUrZ7tSM03aFaJpZz8eOHBAp06dUunSpW3P1nDUtWvXMrx11781atToht8NAJxBbGysTpw4odOnT8vDw0Nly5ZVxYoVszwz/OzZs9q/f7/i4+NljNFjjz2W7naGV69e1Z9//qmrV6+qVKlSqlChQrr9+bQrRIODg9W+fXudPn1aBw8eVL58+VS/fn15eXk5/D3WrVun2NjYLMfce++9dmfdA4CzSkpK0smTJxUREaHExESVLFlS1atXz7JuXr16VXv37tWlS5eUmpqqhg0bprsleXJysvbt26dz587ZrtosWrSo3Zi0K0RHjBihqVOnKiYmRnv27FFycrLq1q2rwoULZ/v7xMbG6ujRo4qMjJSvr69q1qx5U9sBgNzMGKPTp08rPDxcly9fVpEiRVSjRg35+vpm+p6UlBTt3btXZ8+eVXJysipWrKiaNWumG3f06FGdOHFCPj4+KleunAIDA9Ptz3t6eqp69eq2u3nt2bNHFy9eVNWqVVWuXLlb/n7R0dHasmWL/P391bBhw1veHu5ONESBO+j6higAwDlc3xAFAOR+1zdEAQC53/UNUQCAc/h3QxTIrXhqOAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMvyvPEQADklb968Cg4Olp+fn9VRAADZ8OGHHyo6Olre3t5WRwEAOKhPnz5q3LgxzxgCACdSp04dBQcHq2LFilZHAQBkw88//6wCBQpYHQPIEs8QBQAAAAAAAAAAAOCyuGUuAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBleVodIKdcunRJGzZsUJkyZeTj42N1HABwCgkJCQoPD1ezZs1UuHBhSzJQvwEge6jdAOB8qN0A4Jysrt/UbgDIvsxqt8s0RDds2KDOnTtbHQMAnNKSJUvUqVMnSz6b+g0AN4faDQDOh9oNAM7JqvpN7QaAm3d97XaZhmiZMmUkSQWfmSrP4mUtTuP8Ln0+QpJUeOgnFidxDcxnzmEuc1b0Z8NlLkXaaqgVqN/IrS59PkKpSUnSE+OsjgLYm/+GdPUCtRsAnAj73QDgnKyu39RuAMi+zGq3yzRE024Z4Fm8rDxLV7Y4jfNz8/KWJOYyhzCfOYe5zFlunl4ykqW3XaF+I7dy8/KWjJtUvJzVUQB7Hl6SqN0A4EzY7wYA52R1/aZ2A0D2ZVa73a2JAwAAAAAAAAAAAAC3Hw1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABclqfVATZv3qz169crOTlZ9913n9q1ayc3NzerYwEAsnDs2DEtXbpUFy9eVOXKlfXEE0/I19fX6lgAgCxcunRJCxcu1PHjx1W8eHF17txZ5cqVszoWACALKSkpCg4O1u7du+Xj46PmzZurUaNGVscCANxASEiI1qxZo4SEBAUFBaljx47y8PCwOhYA3NUsu0LUGKOBAweqTZs2On/+vJKSkjR48GC1atVK165dsyoWAOAGZs+erZo1ayokJEQ+Pj6aNm2aatWqpSNHjlgdDQCQidDQUFWvXl0zZ85Unjx5tGnTJlWvXl0//fST1dEAAJmIiYnRQw89pOeee06pqak6c+aMHnnkEb344otWRwMAZGHkyJFq1qyZTp8+LWOMRowYoQcffFAxMTFWRwOAu5plV4h+/fXX+vrrr7Vy5Uq1bt1aktSvXz/dc889Gj9+vN59912rogEAMnH06FE988wzGjFihD744ANJ0ujRo1W/fn099dRTCgkJsTghAOB6KSkp6tmzp8qWLavNmzfL0/OfPwGGDRum/v37q0mTJipdurTFKQEA1xs9erRCQ0O1f/9+2xX9Dz/8sLp27aoHH3xQTzzxhMUJAQDXW7hwoaZOnap58+apR48ekqQhQ4aoZs2aGj16tP773/9anBAA7l6WXSE6Y8YM1axZ09YMlaSqVauqXbt2+uKLL5SUlGRVNABAJmbOnKmkpCSNHDnStszHx0fPPvusduzYoe3bt1uYDgCQkd9++00HDx7U888/b2uGStKoUaMUGxur2bNnW5gOAJCR2NhYffvtt3riiSfsbm+e9vqzzz6zMB0AIDMzZsxQQECAunfvblsWGBiobt266dtvv9XVq1ctTAcAdzdLGqKxsbHavXt3hs+9uP/++xUdHa39+/dbkAwAkJVNmzapTJky6a4kuv/++23rAQC5S1ptvn7fu0qVKipatCi1GwByoV27dik+Pj7D4yaNGzfWtm3blJKSYkEyAEBmUlNTtW3bNjVs2FBubm526+6//37Fx8dr586dFqUDAFhyy9zw8HAZYzK8NVfashMnTigoKCjD9587d05RUVF2y44ePZrjOQEA9k6ePKnAwMB0y/9du7NC/QaAO+/kyZOSlOm+N7UbAHKfG9XuhIQERUZGKiAgIMP3U7sB4M47d+6c4uPjb3jMO6v3U7sB4PaxpCEaHx8v6Z/bLF4vT548dmMyMmPGDI0fP/72hAMAZCo+Pv6ma7dE/QYAK9xo3/vKlStZvp/aDQB3HsdNAMD5ULsBIHezpCGaP39+SRn/AkhbljYmI8OHD1e3bt3slh09elSdO3fOuZAAgHTy589/07Vbon4DgBX+ve/t5eVlty4+Pp7aDQC5EMdNAMD5ULsBIHezpCFatmxZubu7KyIiIt268PBwSVKFChUyfb+/v7/8/f1vWz4AQMbKly+f4e1aHKndEvUbAKxQvnx5SVJERIRq1qxpty4iIkJNmzbN8v3UbgC48/5du68XHh6ufPnyqUSJEpm+n9oNAHde8eLFlT9/fo55A0Au5W7Fh+bJk0eNGjXS1q1b063bvHmz/P39VaNGDQuSAQCy8vDDD+vMmTM6fvy43fLNmzfb1gMAcpe02rxlyxa75fv27dOlS5eo3QCQC9WrV0++vr7pandqaqq2bdumBx98UO7ulhzSAQBkws3NTQ899JC2b9+ulJQUu3WbN29WgQIFVL9+fYvSAQAs23t+4YUXdOzYMc2bN8+2bNeuXVq5cqWef/55eXh4WBUNAJCJwYMHK2/evHrnnXdsy65cuaJp06apWbNmCgoKsi4cACBDDz74oOrWraupU6cqLi7OtnzSpEkqVKiQ+vfvb104AECG8uTJoyFDhmjp0qXav3+/bfns2bN1+vRpvfjii9aFAwBk6oUXXtC5c+c0c+ZM27KDBw9q4cKFeuaZZ5Q3b14L0wHA3c2SW+ZKUs+ePbVnzx7169dPS5cuVb58+bRgwQJ169ZNY8aMsSoWACALZcqU0Q8//KA+ffro2LFjqlWrllasWKG8efNq7ty5VscDAGTAzc1N8+fPV9u2bVW3bl21bNlSoaGh2rdvnxYsWKBixYpZHREAkIGJEyfq0KFDeuCBB9S1a1ddvnxZS5cu1cSJE9WmTRur4wEAMtCmTRu98847GjFihNasWSM/Pz/99NNPat68uSZOnGh1PAC4q1nWEJWk9957TwMGDNDGjRuVnJysoUOHqkGDBlZGAgDcQOfOnXX8+HH9+uuvunjxotq1a6cWLVrIy8vL6mgAgExUrlxZ+/bt0+rVq3X8+HE1bdpUbdq0UZEiRayOBgDIRJ48eRQcHKxt27Zp9+7d8vb21rvvvqtKlSpZHQ0AkIXXX39dPXv21Lp165SQkKD+/furSZMmVscCgLuepQ1RSapWrZqqVatmdQwAQDYUK1ZMTz31lNUxAADZ4OPjo/bt21sdAwCQTffff7/uv/9+q2MAALKhYsWKqlixotUxAAD/YtkzRAEAAAAAAAAAAADgdqMhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy/K0OkBOu/T5CLl5eVsdw+mlnDkmSbowoYvFSVwD85lzmMuclRp9xuoINtTvW5dy8YzcJLn7lbI6iktIqzeaNdTaIK7iSpTkJsm3uNVJnF/MeasT2FC7cwb1O+cwlzmL+cw5uWm/GwAAALgbuVxDFADgnFKTkiTjZnUM55ZqZNyklMQkq5O4FA9vL6sjuIQUd7d/Dqozn7csJReVSmp3DqF+5xzmMmcxnznHWB0AAAAAuLu5XEO08NBP5Fm6stUxnF7a1XdF31xscRLXwHzmHOYyZ53/Tzulng+3OsY/nhgnFS9ndQrg/8waKg9vL+oNch1qNwA4oc+fli5HWp0CAAAAuGvxDFEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXZWlDNCQkRC+//LKqV6+uwMBAHThwwMo4AAAHnDhxQh988IEaNWqkwMBAffHFF1ZHAgDcwOXLl/Xdd9+pffv2KlOmjPr06WN1JADADSQnJ+vXX3/VwIEDVaFCBZUvX97qSAAAB+zZs0evv/66atWqpcDAQG3bts3qSAAAWdgQ/fTTT/Xcc8+pZMmSevTRR3Xq1CklJiZaFQcA4IB9+/bp4Ycf1rlz5zRgwACdOnVKMTExVscCANxAo0aNtGbNGg0dOlQXL15UVFSU1ZEAADcwZMgQffTRR2ratKkqVaqkiIgIqyMBAG7g22+/Vf/+/eXr66t27drp1KlTSkhIsDoWAECSp1UfPHz4cL3wwguSpIkTJ1oVAwCQDTVq1NDx48clSb///rvFaQAAjtq3b588Pf/Z9Xdzc7M4DQDAEV9++aWtdv/8888WpwEAOOKpp55Sv379JEmfffaZxWkAAP9m2RWiaTv1AADn4eHhYXUEAMBNYN8bAJwPtRsAnA+1GwByL4caok2bNtVbb72lLVu2KDk5+XZnAgAAAAAAAAAAAIAc4dApKx4eHpo0aZLGjx8vX19fPfLII2rZsqVatmypatWq3e6M6Zw7dy7dc4+OHj16x3MAALKH+g0AzofaDQDOh9oNAM6H2g0At5dDDdGNGzfq6tWrWr9+vVavXq3Vq1fbnl9RpkwZtWzZUl999dVtDfpvM2bM0Pjx4+/Y5wEAcgb1GwCcD7UbAJwPtRsAnA+1GwBuL4dval6gQAG1b99e7du3lyTt2bNHY8aM0a+//qqvv/76jjZEhw8frm7dutktO3r0qDp37nzHMgAAso/6DQDOh9oNAM6H2g0AzofaDQC3l8MN0cTERG3ZskWrVq3S6tWr9ccff8jb21vNmzdXy5Ytb2fGdPz9/eXv739HPxMAcOuo3wDgfKjdAOB8qN0A4Hyo3QBweznUEH3ssce0YcMGxcXFqU6dOmrZsqXeeecdPfTQQ8qbN+/tzggAAAAAAAAAAAAAN8WhhuiKFSuUN29evfbaa+rTp49q1Khxu3MBAAAAAAAAAAAAwC1zd2TQt99+qyeeeEKzZ89WzZo1VaZMGT399NP68ccfFRUVdVMfvHv3bgUGBiowMFAffPCBJKl169YKDAzUPffcc1PbBADcftWqVVNgYKA6duwoSZo4caKtnh84cMDidACAjAwaNMhWq+Pi4rR+/Xrb6y+++MLqeACADHzzzTe2Wv3rr78qJSXF9rpPnz5WxwMAZODYsWO2Wv3mm29Kkrp166bAwECVL1/e2nAAcJdz6ArRvn37qm/fvpKkP//8U6tXr9bq1as1aNAgxcfHKygoSH/88Ue2PrhWrVr6/fffM1zn4eGRrW0BAO6cdevWKTU1NcN1JUqUuMNpAACOmDx5st56660M1xUuXPiOZgEAOKZbt25q0aJFhuvy5Mlzh9MAABxRrly5TI95u7m53eE0AIB/c6gh+m+1a9dWamqqUlJSFBcXp40bN2r37t3Z/mBvb28FBgZm+30AAGuVLl3a6ggAgGwqWrSo1REAANmUP39+5c+f3+oYAIBs8PT05Jg3AORSDjVEIyMjtXr1aq1atUqrV6/W2bNn5ebmpnvuuUcvv/xypmcsAgAAAAAAAAAAAICVHGqIli5dWsYYBQYGqm3btmrZsqVatGghf3//250PAAAAAAAAAAAAAG6aQw3RqVOnqmXLlqpRo8btzgMAAAAAAAAAAAAAOcahhugLL7xg+/ns2bM6f/68ihUrphIlSty2YAAAAAAAAAAAAABwq9wdHbhy5UrVrl1bJUuWtPvfVatW3c58AAAAAAAAAAAAAHDTHLpCdP369Wrfvr0qVKigl156SSVLltTZs2e1ZMkStWvXTr/99psefPDB250VAAAAAAAAAAAAALLFoYbo22+/rV69eumbb76Rh4eHbfl7772n/v3766233tLatWtvW0gAAAAAAAAAAAAAuBkONURDQkK0f/9+u2aoJHl4eGjChAmqU6fObQkHAAAAAAAAAAAAALfCoWeIpqSkKE+ePBmuy5s3r1JSUnI0FAAAAAAAAAAAAADkBIcaorVq1dK0adMyXDd9+nTVqlUrR0MBAAAAAAAAAAAAQE5w6Ja5I0eOVO/evbV79249/vjjKlmypM6ePavFixfrl19+0Y8//ni7cwIAAAAAAAAAAABAtjnUEH3qqad07tw5jRs3TsuXL7ct9/X11SeffKKePXvetoAAAAAAAAAAAAAAcLMcaoheunRJTz/9tIYMGaKQkBBdvHhRRYsWVYMGDZQ/f/7bnREAAAAAAAAAAAAAbopDDVE/Pz/NmzdP3bt31yOPPHK7MwEAAAAAAAAAAABAjnB3ZFCpUqXUvHnz250FAAAAAAAAAAAAAHKUQw3RAQMGaN68ebc7CwAAAAAAAAAAAADkKIdumdurVy+NHz9e+/fvV8eOHRUQECAvLy+7MdWrV78tAQEAAAAAAAAAAADgZjnUEK1du7bt5//+978ZjjHG5EwiAAAAAAAAAAAAAMghDjVEZ8+efbtzAAAAAAAAAAAAAECOc6gh2r9//9scAwAAAAAAAAAAAABynrvVAQAAAAAAAAAAAADgdqEhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgshxuiqamp+uWXX/TKK6/o6aefti1fu3atkpOTb0s4AAAAAAAAAAAAALgVno4Munr1qtq3b68NGzYob968io+P19dffy1J+vrrr3XhwgV17979tgYFAAAAAAAAAAAAgOxy6ArRN954Q2fOnNHmzZsVGxtrt27w4MH68ssvb0s4AAAAAAAAAAAAALgVDl0humDBAv3000+6//77062rVauWduzYkePBAAAAAAAAAAAAAOBWOXSF6Llz53TPPffYXru5udl+9vb2VkJCQs4nAwAAAAAAAAAAAIBb5FBDtGTJkgoNDbW9/ndDNCQkROXKlcv5ZAAAAAAAAAAAAABwixxqiHbs2FHPPvusTpw4Ybf89OnTevnll9W5c+fbkQ0AAAAAAAAAAAAAbolDDdFx48bp8uXLqlatmh588EEZY9S2bVtVq1ZN8fHxeu211253TgAAAAAAAAAAAADINk9HBvn7+2v79u2aMGGCgoOD5ePjo3379qlPnz4aP368ChcufJtjOu7S5yPk5uVtdQynl3LmmCTpwoQuFidxDcxnzmEuc1Zq9BmrI/yfheMlT+r3LbkSJblJ8i1udRLXcOGkUkS9ySkpF8/ITZK7Xymrozg9arcLon7nHOYyZzGfOSfmvNUJbDhukjPYt8k5zGXOYj5zVq7a9wYA3BKHGqKSVLx4cX366af69NNPb2ceAMBdyt3LS25eXlbHcGop7m7//OHrzTzmhJS0/01MsjSHy0g1Mm7MZ44wVgf4P9TunEH9zjnMZc5iPnNOipvVCf5PalKSZHJRIGfFvk3OYS5zFvOZs3LRvjcA4NY41BCNi4vTsmXL1K1bt3TrFixYoHbt2ilfvnw5Hu5mFB76iTxLV7Y6htNLuxqm6JuLLU7iGpjPnMNc5qzz/2mn1PPhVseQRP1G7nNhQpd/DiIM+tzqKIC9z5+WLkdanUIStRsAHJWb9rv1xDipeDmrUwCAc8hF+94AgFvj0DNEP/jgA/31118Zrjtw4ICmTJmSo6EAAAAAAAAAAAAAICc41BCdO3eunnzyyQzXPfnkk/rhhx9yNBQAAAAAAAAAAAAA5ASHGqInTpxQiRIlMlxXokQJhYWF5WQmAAAAAAAAAAAAAMgRDjVES5UqpZCQkAzXhYSEyN/fP0dDAQAAAAAAAAAAAEBOcKgh2qFDBz333HM6duyY3fJjx47pueeeU4cOHW5LOAAAAAAAAAAAAAC4FZ6ODHrzzTcVHBysGjVqqFGjRgoICNCpU6e0fft2lS5dWuPGjbvdOQEAAAAAAAAAAAAg2xy6QtTf31/bt2/XwIEDdfLkSS1dulQnTpzQoEGDtH37dm6ZCwAAAAAAAAAAACBXcugKUUkqUaKE/vvf/97OLAAAAAAAAAAAAACQoxy6QhQAAAAAAAAAAAAAnFGGV4ju3LlTknTffffZvc5K2lgAAAAAAAAAAAAAyC0ybIg2aNBAkmSMsXudlbSxAAAAAAAAAAAAAJBbZNgQXbx4cZavAQAAAAAAAAAAAMAZZNgQ7dy5c5avAQAAAAAAAAAAAMAZuDsyaN68ebe0HgAAAAAAAAAAAACs4FBDtFevXre0HgAAAAAAAAAAAACs4FBDNCsJCQny8PDIiSwAAAAAAAAAAAAAkKMyfIaoJO3ZsyfL19I/zdDly5crMDAwp3MBAAAAAAAAAAAAwC3LtCFat27dLF+ncXd315QpU3I2FQAAAAAAAAAAAADkgEwboj/++KPt5169etm9TpM/f37VrFlTlSpVuj3pAAAAAAAAAAAAAOAWZNoQ7dmzp+3nsLAwu9cAAAAAAAAAAAAA4AzcHRlUvnz5LNfPmzcvJ7IAAAAAAAAAAAAAQI5yqCHaq1evW1oPAAAAAAAAAAAAAFZwqCGalYSEBHl4eOREFgAAAAAAAAAAAADIUZk+Q3TPnj1Zvpb+aYYuX75cgYGBOZ0LAAAAAAAAAAAAAG5Zpg3RunXrZvk6jbu7u6ZMmZKzqQAAAAAAAAAAAAAgB2TaEP3xxx9tP/fq1cvudZr8+fOrZs2aqlSp0u1JBwAAAAAAAAAAAAC3INOGaM+ePW0/h4WF2b0GAAAAAAAAAAAAAGfg7sigMWPG3O4cAAAAAAAAAAAAAJDjHGqIStKsWbPUoEEDFSlSRAUKFEj3HwAAAAAAAAAAAADkNg41RD/77DMNHz5clStX1qVLl9SrVy81btxYCQkJeuihh9S/f//bHBMAAAAAAAAAAAAAss+hhujMmTP12Wef6ccff7S9XrNmjY4cOaK4uDj17t37toYEAAAAAAAAAAAAgJvhUEP00KFDeuKJJ2yvU1JSJEnly5fXtGnT9PLLL9+edAAAAAAAAAAAAABwCxxqiCYkJKho0aKSpHz58uns2bO2dZUqVdLu3btvTzoAAAAAAAAAAAAAuAUONUT/rWbNmvrpp59sr3/55Rf5+fnddIDIyEiFhIQoIiJCxpib3g4A4M6JiYnRrl27dOTIESUlJVkdBwDggMTERB04cEChoaG6evWq1XEAAA4wxigsLEw7duxQVFSU1XEAAA6KiopSSEiITp48qdTUVKvjAAB0Ew3RAQMGaOTIkWrVqpXat2+vp556Sr169cr2B3/00UeqUaOGqlWrpueee05BQUGqXr26li1blu1tAQDujOXLl6t58+YqVqyYhgwZolatWsnf31/vv/8+J7UAQC71119/qW/fvipcuLC6dOmiPn36qGjRoho0aJAuXrxodTwAQAYuX76s1157TaVKldL999+v4cOHq0KFCmrWrJlCQ0OtjgcAyMTnn3+ue++9VxUrVtRzzz2nBg0aqFKlSlqwYIHV0QDgrudQQ3Tbtm22n4cNG6Z33nlHERER+vvvv/Xyyy9rwoQJ2f7g0aNHq1u3bjp9+rTtCtFGjRqpY8eO2rRpU7a3BwC4/T766CMVKFBAx44d065du3T8+HG9//77evXVV2/qdwEA4PZbunSpduzYoTVr1ujQoUPau3evtm3bpp9++kmdO3e2Oh4AIANHjhzRjBkzNGnSJEVERGjHjh06fvy4EhIS9Mgjj9g9yggAkHu8/vrratGihU6dOmU75t2uXTt1796dC4EAwGIONUQbN25s+9nNzU1jxozRgQMHdODAAb377rvy8fHJ9gcvXLhQb7/9tvLnzy9JypMnjz7++GOlpqbq66+/zvb2AAC33wsvvKAlS5YoMDDQtmzw4MEKCgrSzJkzLUwGAMhM48aN9fvvv6tJkya2ZfXq1dPgwYO1adMmHTp0yMJ0AICM+Pn5afPmzXr66afl4eEhSSpevLjGjx+v6Ohou0cZAQByj9mzZ+vDDz9UwYIFJUleXl6aMmWKvLy8NGvWLIvTAcDdzdOqD+7UqVO6Zd7e3nJzc9OlS5fufCAAwA117Ngxw+U+Pj7UbgDIpR5++OEMl6ed1Ej9BoDcp2LFihkup3YDQO6W0TFvDw8PeXp6UrsBwGIZNkR37tyZ7Q3dd999txzmq6++kjFGDzzwQJbjzp07p6ioKLtlR48eveXPBwBk3969exUSEqLWrVvfcCz1GwByh2vXrmnu3LkqWLCg7rnnnizHUrsBIPdIu7qI4yYA4Dzmzp2r+Ph4ajcAWCzDhmiDBg2yvSFjzC0F2b9/v9544w1VqFBBzzzzTJZjZ8yYofHjx9/S5wEAbl1cXJz69OkjT09Ph54hSv0GgNzhxRdfVFhYmD788EPlzZs3y7HUbgDIHX766SfNnTtXbdu2VbNmzbIcS+0GgNzh+PHjGjVqlEqWLKkXX3wxy7HUbgC4vTJsiC5evPiOhjh58qQee+wxeXp6auHChSpQoECW44cPH65u3brZLTt69Kg6d+58G1MCAP4tKSlJ3bp10969e/XFF184dKcA6jcAWG/y5Mn64osv1KNHD40cOfKG46ndAGC9TZs2qW/fvqpWrZq+/fbbG46ndgOA9c6dO6c2bdooISFBP//8s4oWLZrleGo3ANxeGTZE72SRPXPmjFq0aKHo6GitWrVKdevWveF7/P395e/vfwfSAQAykpKSol69emn58uWaOnWqhgwZ4tD7qN8AYK1p06ZpzJgx6tKli77//nu5ubnd8D3UbgCwVkhIiNq1a6eAgACtXbtWxYsXv+F7qN0AYK2LFy+qZcuWOnnypH755Zcb3i5XonYDwO2WYUP0TomKilLz5s115swZ/frrr2rcuLGVcQAADkhNTVXfvn21cOFCffTRRxoxYoTVkQAADpg5c6ZGjBihTp066X//+588PS39UwAA4IDdu3erdevWKl68uNatW6eAgACrIwEAbuDy5ctq1aqVDh8+rKVLl6p58+ZWRwIASHK36oOjo6PVokULhYeHa8WKFWrSpIlVUQAADjLGaPDgwfrhhx80ZcoUh261CACw3pw5czR06FB16NBB8+fPl5eXl9WRAAA3sH//frVq1Up+fn5av369AgMDrY4EALiBq1evqm3bttq/f7+WLFmiVq1aWR0JAPD/WXJaeFJSktq0aaO9e/dqwoQJSk5O1vr1623rCxYsqHr16lkRDQCQhdGjR+vrr79Wly5dVL9+fbvaLUkPPvigPDw8rAkHAMjQsmXLNGDAAFWsWFHPPvustm7dare+du3aKlasmEXpAAAZiYiIUMuWLRUbG6uPPvpIx44d07Fjx2zrAwICVKVKFQsTAgCuZ4xRx44dtW3bNo0ZM0Y+Pj52x03y5cunhg0bWhcQAO5yljRE4+LilDdvXjVr1kxr1qzRmjVr7NbXrFlTM2bMsCIaACAL0dHRatasmS5evKi33nor3foVK1Yob968dz4YACBTJ0+etD2zaNKkSenWT5w40aFnGgEA7pzTp0+ratWqkqSvvvoq3fouXbrw6AoAyGVSU1OVmpqqZs2aadu2bdq2bZvd+rJly+q7776zKB0AwJKGaKFChdJdVQQAyP1mzZpldQQAQDYNGzZMw4YNszoGACAbGjZsyHETAHAyHh4e1G4AyMUse4YoAAAAAAAAAAAAANxuNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABcFg1RAAAAAAAAAAAAAC6LhigAAAAAAAAAAAAAl0VDFAAAAAAAAAAAAIDLoiEKAAAAAAAAAAAAwGXREAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZnlYHyGmXPh8hNy9vq2M4vZQzxyRJFyZ0sTiJa2A+cw5zmbNSo89YHcGG+n3rUi6ekZskd79SVkdxCWn1RrOGWhvEVVyJktwk+Ra3OonzizlvdQIbanfOoH7nHOYyZzGfOSc37XcDAAAAdyOXa4gCAJxTalKSZNysjuHcUo2Mm5SSmGR1Epfi4e1ldQSXkOLu9s9BdebzlqXkolJJ7c4h1O+cw1zmLOYz5xirAwAAAAB3N5driBYe+ok8S1e2OobTS7v6ruibiy1O4hqYz5zDXOas8/9pp9Tz4VbH+McT46Ti5axOAfyfWUPl4e1FvUGuQ+0GACf0+dPS5UirUwAAAAB3LZ4hCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LJoiAIAAAAAAAAAAABwWTREAQAAAAAAAAAAALgsGqIAAAAAAAAAAAAAXBYNUQAAAAAAAAAAAAAui4YoAAAAAAAAAAAAAJdFQxQAAAAAAAAAAACAy6IhCgAAAAAAAAAAAMBl0RAFAAAAAAAAAAAA4LI8rfzwM2fOKDg4WIcPH1aBAgVUp04dderUSZ6elsYCAGQhPj5ewcHB2rt3r5KTk1WxYkV169ZNRYoUsToaACATxhitW7dO27dvV1RUlAIDA9W+fXtVrVrV6mgAgCzs379fa9asUVhYmIoXL677779fjzzyiNWxAABZiIqK0s8//6xDhw4pT548qlmzph5//HF5e3tbHQ0A7mqWXSE6adIkPfroowoNDVVAQIDi4uI0fPhwVa1aVUeOHLEqFgAgC2vXrlXt2rUVHBwsX19f5c+fXzNmzFCZMmW0cOFCq+MBADJw4cIF1alTRx9++KGuXbumUqVKaf369apRo4Zeeuklq+MBADLRvXt3DRgwQOHh4SpbtqxOnDihdu3a6aGHHlJMTIzV8QAAGfjss8/UtGlT7dixQyVLllRycrJefvllVaxYUXv37rU6HgDc1Sy7FLNLly567bXX5ObmZls2dOhQVatWTa+++qoWLVpkVTQAQCYqV66svXv3Kn/+/LZlr776qurXr6/BgwdzlT8A5EI+Pj5avny5ypYta1v2yiuv6Nlnn9VHH32kHj16qGHDhhYmBABk5K233lLNmjXtlrVp00aPP/64pk2bptdff92iZACAzLRs2VLDhg2Th4eHbdnzzz+vKlWq6MUXX9Rvv/1mYToAuLtZdoVojRo17JqhklSxYkWVLl1aJ0+etCgVACAr5cqVs2uGSpK3t7caNmyo6OhozlQHgFyoQIECds3QNA888IAkse8NALnU9c1QidoNALldtWrV7JqhklSqVClVqlSJ2g0AFrOsIZqRnTt3KiIiQm3btrU6CgDAQZcvX9a6devUuHFjniMKAE7CGKOlS5eqQIECtoPrAIDcb8mSJZLEcRMAcCJ//fWXDh06RO0GAItZfl/D1157TRcuXNCpU6e0a9cuTZw4UaNHj87yPefOnVNUVJTdsqNHj97OmACAf/n222+1bds2RUdHa/369WrXrp3ef//9G76P+g0A1gkJCdHXX3+tuLg47dixQyVKlNCGDRtUsmTJLN9H7QYA61y5ckWjR49WcnKyDh8+rNOnT2vu3Lnq1KlTlu+jdgOAtSZMmKCIiAhFRkZq27ZtGj16tN54440s30PtBoDby/KGaK1atXT58mUVKlRIO3fu1M8//6wePXqoYsWKmb5nxowZGj9+/B1MCQD4t7Jlyyo+Pl6RkZE6dOiQVq1apW7dut3wbEfqNwBYp0iRIgoKCtKVK1d06dIlrVu3TsuWLVO9evWyfB+1GwCs4+XlpaCgICUkJMjDw0O7d+/WkiVL1L59exUsWDDT91G7AcBa1apVU9GiRVWkSBHt2rVLwcHB6tmzZ4a3Q09D7QaA28vyhmjv3r1tP7/yyiu655571L17d+3cuTPT9wwfPlzdunWzW3b06FF17tz5dsUEAPzLI488okceeUSS9J///Edt27ZV165ddeTIEZUuXTrT91G/AcA6VapUUZUqVSRJo0eP1uTJkzVmzBjVqFFDXbt2zfR91G4AsE7evHk1dOhQ2+uBAweqSZMm8vPz0+eff57p+6jdAGCt7t27235+7bXXFBQUpC5duuivv/6Su3vGT7GjdgPA7WV5Q/Tf/P399dhjj2n27Nm6ePGi/Pz8Mh3n7+9/h9MBADLi7u6ufv36adWqVdq8ebPdTv/1qN8AkHsMGDBAY8aM0apVq7JsiFK7ASD3aNiwoWrWrKlVq1ZlOY7aDQC5R6FChdSlSxd9/PHHCgsLy/TOiNRuALi9Mj4dxUIxMTHy8PCQj4+P1VEAAA6KiYmRJOXPn9/iJAAAR1G7AcA5xcTEULsBwMmk7Xvny5fP4iQAcPeypCGanJysBQsWyBhjt3zDhg0KDg5Wly5d2LkHgFxo6dKlunr1qt2yM2fOaMqUKQoICLDdRhcAkHts3rxZJ06csFuWkJCgsWPHyt3dXU8++aRFyQAAmQkLC9OWLVvSLZ8+fbrCwsLsHj8EAMg95s2bp5SUFLtlO3fu1I8//qgWLVqoZMmSFiUDAFhyy1w3NzctXbpUr7/+umrUqKEiRYro6NGj+v3339W1a1fNmjXLilgAgBs4fvy4goKCVL58eQUGBurs2bNav369atSooSVLlnCmIwDkQomJierQoYMKFCigSpUqKTY2Vtu2bVNqaqr+97//qUGDBlZHBABcx8vLS2+99ZYiIyNVvXp1eXl5ae/evTp69Khee+01vfLKK1ZHBABk4LffftMbb7yh6tWrq1ixYjp+/Lg2b96sxx57TN98843V8QDgrmZJQ9TDw0Pff/+9zp07p127dun06dPq3LmzGjZsqICAACsiAQAc8OKLL+qZZ55RSEiIjh8/rjx58uiDDz5Q7dq1rY4GAMjEo48+qtDQUO3evVsHDx5UcnKyRo0apUaNGsnLy8vqeACADAQEBGj16tX6+++/FRoaqujoaPXp00f333+/ChcubHU8AEAmvvzyS124cEE7d+5URESEOnTooO+++07lypWzOhoA3PUsaYim8ff3V9u2ba2MAADIprx586pZs2Zq1qyZ1VEAAA5yc3NTvXr1VK9ePaujAACyoWLFiqpYsaLVMQAA2VC0aFG1bt3a6hgAgOtY8gxRAAAAAAAAAAAAALgTaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyPK0OkFMSEhIkSdGfDZebp5fFaZxfavQZSdL5/7SzOIlrYD5zDnOZs1Ivnpb0fzXUCrbPnv+G5EH9Ri4Sc14pbtQb5D7UbgBwQleiJFG7AcDpWFy/OeYNANmX2XETl2mIhoeHS5LMpUgZi7O4ktTz4VZHcCnMZ85hLnNWeHi46tWrZ9lnS5KuXrDk84Ebod4gt6J2A4DzoXYDgHOyqn5zzBsAbt71tdvNGOMStfTSpUvasGGDypQpIx8fH6vjZOro0aPq3LmzlixZosqVK1sdx+kxnzmHucxZzjKfCQkJCg8PV7NmzVS4cGFLMjhD/XaWf09nwXzmLOYz5zjLXFK7Hecs/6bOgLnMWcxnznKG+aR2O84Z/j2dCfOZc5jLnOUs82l1/aZ2352Yz5zFfOYcZ5nLzGq3y1whWrhwYXXq1MnqGA6rXLmyatWqZXUMl8F85hzmMmc5w3xadYZ6Gmeq387w7+lMmM+cxXzmHGeYS2p39jjDv6mzYC5zFvOZs3L7fFK7sye3/3s6G+Yz5zCXOcsZ5tPK+k3tvrsxnzmL+cw5zjCXGdVudwtyAAAAAAAAAAAAAMAdQUMUAAAAAAAAAAAAgMuiIQoAAAAAAAAAAADAZdEQvcOKFy+ucePGqXjx4lZHcQnMZ85hLnMW8+la+PfMWcxnzmI+cw5z6Xr4N805zGXOYj5zFvPpWvj3zFnMZ85hLnMW8+la+PfMWcxnzmI+c46zz6WbMcZYHQIAAAAAAAAAAAAAbgeuEAUAAAAAAAAAAADgsmiIAgAAAAAAAAAAAHBZNEQBAAAAAAAAAAAAuCwaogAAAAAAAAAAAABclqfVAe4WsbGx+uSTT7RhwwalpKSoYcOGGjVqlIoVK2Z1NKdjjNHvv/+u+fPna/v27apQoYLmzp1rdSynZIzR+vXrtWjRIh06dEgFChTQvffeq+eee05Fixa1Op7TSU1N1YoVK/Tzzz/r6NGjKlSokIKCgvTMM8+oRIkSVsfDTZo7d64WLlyoixcvqmrVqnr22Wd17733Wh3LKf3999+aP3++Vq5cqWvXrmn58uXy8/OzOpZTOnTokL7//nvt3btXiYmJqlKlioYMGaLatWtbHc0p7d27V3PmzNG+ffvk5uamypUra+DAgfx/3Ylt27ZNX3zxhY4fP64SJUroySefVOfOna2O5ZSio6O1ZMkSLVq0SFFRURo/frxat25tdSyndOHCBX3//ffavn27zp49q3LlyqlLly7q0KGD1dGc0tmzZ/XNN99ox44dio6OVtmyZfX444+rffv2cnNzszoebkJERIQ+/vhj7d69W3ny5FHLli317LPPytvb2+poTicpKUmrV6/W/PnzdfDgQbVr105vvvmm1bGcUmJiopYsWaJVq1bp+PHj8vf3V9OmTTV48GD5+PhYHc/pXLt2TfPmzdO6det08uRJlShRQg8++KAGDBigfPnyWR0PN+HatWuaPn261qxZo8TERNWtW1cvvfSSSpUqZXU0p7Rr1y7Nnz9fmzdvVtGiRfXzzz9bHclpbdmyRT/99JP++usv+fj4qHbt2nr++edVsmRJq6M5pTVr1mjRokU6fPiwChQooDp16uiZZ55RYGCg1dGyhStE74CYmBg1adJE33//vYYMGaJRo0Zpw4YNql+/vs6cOWN1PKfTsmVLjRo1SmXLltWpU6cUGhpqdSSn1adPH7Vq1UrGGL388svq1auXli9frkqVKikkJMTqeE6nb9++mjNnjho1aqQ33nhDnTt31oIFC1SlShXt2rXL6ni4CYMHD9awYcPUokULjRs3TsYYNWjQQKtXr7Y6mtOZNGmSWrVqpUuXLsnX11fbt29XYmKi1bGc0uzZs1W9enXt3LlT/fr10wsvvKDLly/r3nvv1bRp06yO53Rmzpyp4cOHKyAgQKNGjdIzzzyjkydPKigoSDNmzLA6Hm7Cjz/+qAceeEB+fn4aP368GjdurJ49e3Ig+CZs2bJF1atX18aNG1W9enVt375dUVFRVsdySseOHVNAQIC++OILPfLII3r99ddVoUIF9ejRQ127dpUxxuqITuXQoUNq1qyZ4uPj1bdvX7366qsq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3NysjgcHcdwEzmTSpElavHix+vfvr/bt21sdxymVKFFCU6dO1bVr17R3715NnjxZffv21fLly1WyZEmr48FB1G44k02bNumll15S9erVNXHiRKvjOK3x48crLi5OYWFhmjZtmoYOHaoSJUrowQcftDqaw2iI3maFChWSlPF98GNiYuzGAFbauHGjnnjiCVWtWlXLly/nrOpbkPZc1gceeECPP/64atSooaefflqhoaEWJ0N2FCpUiNqNXO/s2bNq0aKFLl26pJUrV6pq1apWR3Ja/567xx9/XE888YSeffZZtW3bNt1zo5B7/Xvfu2DBgnbrYmJiVLhwYQtSAfZSUlLUu3dv/frrr3r//ffVu3dvqyM5rRIlSthuWdmmTRuVK1dOzz77rFq3bq1evXpZnA6O4rgJnMV///tfjR07Vu3bt9eXX35pdRynlSdPHjVu3FiS9PDDD6tVq1aqVauWxo4dq6+++sridHAUtRvOYteuXWrfvr1Kly6tVatWZfg8TDimbt26kqSmTZvqiSee0L333qs+ffooLCzM2mDZwLXBt1n58uXl7e2tv//+O926tGXVqlW707EAO7///rvatWunsmXL6rfffuMM9Rzk6+urpk2bau/evUpISLA6DrKhWrVq1G7kaufPn1fz5s0VERGhZcuW6YEHHrA6kktp06aNEhMTOZnFyaTV5uvrd1JSkiIiIqjdsFxqaqoGDBig+fPna9KkSXrllVesjuRS0h5TsWPHDouTIDsyq91py/z9/TmhBZabPXu27WS5n376SV5eXlZHchnVq1dX+fLlqd1Opnjx4vLz8+O4CXK1vXv3qlWrVipSpIh+++03lSlTxupILiNPnjx6+OGHdeLECZ07d87qOA6jIXqbeXl5qUWLFlq/fr2Sk5Pt1q1atUq1a9fm/4iw1K5du9SmTRuVLl1a69at4/Ykt8GJEyfk5+fHVbdOpm3btrpw4YJ2795tt3zVqlVyd3dX69atLUoGSJcuXVKrVq10/Phx/fLLL2rWrJnVkVzOiRMnJEmlSpWyOAmyI60Zsnr1arvlGzZsUEJCgh577DErYgGSJGOMhg4dqjlz5mjixIk8ouI2oHY7p6CgIJUsWTJd7b569aq2bt1K7YblfvjhBw0aNEitW7fW4sWL+ds+hyUkJCgyMpLa7YTatGmjrVu3Ki4uzm75qlWrVKZMGdWuXduiZID0119/qUWLFipQoIDWrVun8uXLWx3J5Zw4cUJ58+Z1qqvBaYjeAWPHjtX58+c1btw427Ivv/xSu3bt0ltvvWVdMNz19u3bp9atW6tEiRJat24dO5+34OLFi5owYYIuXbpkW5aYmKh33nlH27dv18iRI60Lh5syaNAgBQQEaNSoUYqNjZX0z5llM2bM0DPPPKPSpUtbnBB3q6tXr6pNmzY6dOiQfvnlFz3yyCNWR3JqkyZN0pEjR+yWrVixQlOnTtXDDz9suyUMnEOdOnXUpUsXTZkyRUePHpX0zwkEr776qmrWrKnu3btbnBB3s5EjR2rmzJmaOHGixo4da3Ucp/b999/rt99+s1t28OBBvfDCCypatKj69OljUTLcDHd3d7355ptavXq1Fi5cKOmfq6lfeeUVpaamasyYMRYnxN1s8eLF6tevn1q1aqUlS5bQDL0FO3bs0FdffaXExETbsosXL2rgwIGKj4/XiBEjLEyHmzFmzBglJCRo9OjRSk1NlSTNnz9fa9eu1VtvvcXzvGGZY8eOqUWLFsqbN6/Wr1+vChUqWB3JaSUmJtr6W2mSk5P16aef6tdff9Xw4cOd6nejmzHGWB3ibrBw4UI9++yz8vHxkY+Pj86dO6d3331Xw4YNszqa0/nwww+1YMECSVJoaKjc3Nx0zz33SJJ69uypF1980cJ0zuWhhx7Spk2bVLlyZRUtWjTd+l9//dWpzvCwUlJSkt5//319/vnn8vDwUMGCBRUWFqaCBQvqlVdeYcfeSR04cEB9+vTR8ePHVaZMGR0+fFi9e/fW9OnT5e3tbXU8p7Jx40aNHj1akhQWFqazZ8+qXr168vLyUqVKlTR37lyLEzqPt99+W+PGjVPx4sVVsWLFdOsnTpyoFi1aWJDMOS1YsEDvvfeewsPDVbp0aZ05c0ZxcXHq06ePJk+eLF9fX6sjIptiYmI0aNAg/fzzz6pWrZqOHz+ue++9V3PmzFG5cuWsjudU4uPjbSddREdH6/Dhw6pUqZKKFSsmSVq+fLn8/PysjOg0tm7dqqZNm8rHx0dBQUHp1nfq1IkrRrNh9+7dGj9+vNavX6+yZcvqypUrOnXqlFq0aKGPP/5Y1atXtzoibsI777yjd999V2XLltXly5fl5eWlWbNmsV9zEwYNGqR9+/YpNTVVO3bsULFixVSpUiVJ0vjx47nbTTbky5dP8fHxCgoKSnfAt1SpUlq8eLFFyZxPVFSUJkyYoLlz56p48eJyc3PT8ePHVaVKFU2aNEkdOnSwOiJuwooVK/TMM88oNTVVvr6+ioiI0H/+8x8eC3ATvvjiC82ePVuStH//fiUmJtpO0G3Xrp3efPNNK+M5lY4dOyo4OFgVKlTI8NFwP/30kwIDAy1I5nyMMfrwww81ffp0paSkqEiRIjpx4oTy5MmjESNG6NVXX5W7u/Ncd0lD9A5KSUnRoUOHlJKSoqpVqzpV5zw3CQsLU2RkZIbrSpUqxYGubNi/f7/tQecZue++++Tp6XkHE7mGiIgInT17VsWLF1fZsmWtjoMcEBYWpujoaFWoUIHnF92k6OhoHTp0KMN1+fLls53YghuLiIhQREREpuurVKmS4UkuyFp0dLTCwsKUL18+VapUid9/LuD8+fM6efKk/P39+WP3JqWmpiokJCTT9fXr1+cZag66cuWKDhw4kOl6f3//DE9yQdauXbumo0ePyhijihUrKn/+/FZHwi2Kj4/XkSNH5OPjo6pVq3J10U3K6m/9ypUr205swY1t375dmR069fHx4W4iNyElJUXHjx9XTEyMAgICMmxWwLmkpqbq8OHDSkxMVJUqVZQ3b16rIzmlrP7WL168uO3EFtzYwYMH7e7id726devSm7kJZ86c0enTp+Xn56fy5cs75X4aDVEAAAAAAAAAAAAALst5rmUFAAAAAAAAAAAAgGyiIQoAAAAAAAAAAADAZdEQBQAAAAAAAAAAAOCyaIgCAAAAAAAAAAAAcFk0RAEAAAAAAAAAAAC4LBqiAAAAAAAAAAAAAFwWDVEAAAAAAAAAAAAALouGKAAAAAAAAAAAAACXRUMUlklISFBERIQSEhJc+jNzkrPnB+AaTp06pStXrrj8Z+YkZ88PwPmdP39e586dc/nPzEnOnh+A87t69aoiIiJkjHHpz8xJzp4fgPNLSkpSRESE4uPjXfozc5Kz54fzoCEKy2zbtk1lypTRpk2bsv3erBqDWa27lc/MDZw9PwDXUK5cOU2aNOmm3hsREZFpYzCrdbfymbmBs+cH4Px69+6txx577KbeGxUVpaioqGyvu5XPzA2cPT8A5/f555+rTJkyunz5crbfGxMTk2ljMKt1t/KZuYGz5wfg/Pbv368yZcpo2bJl2X5vYmKiIiIidO3atWytu5XPzA2cPT+cBw1ROKV169apTJky2rZtW7bW+fj4KCAgQHny5LkTMQEA/5KcnKwyZcro/fffz9Y6SQoMDFShQoVud0QAQAa6deumDh06ZHtd8eLFVaJEidsZDQCQiU8++URlypRRbGxsttb5+voqICBA7u4cMgSAO+2PP/5QmTJltHLlymyt8/b2VkBAgPLly3cnYgJOy9PqAMCddP/99ysiIsLqGACAbAoLC7M6AgAgm+bMmWN1BABANj3zzDN65plnrI4BAMiGmjVrcswbcACneyFXSU5OVkREhO2/8+fPp7uFS2xsrM6fPy/pn1t0pY2NjY3Ncp1042dwpqSk6Pz580pNTc0y47lz527qOZ6ObP/atWuKiopSSkqKQ9uMjY1VREREuvFpcxkXF2e37YiICCUmJkr651YLGd2eMikpSRcuXMg037+3kZSUZJtzAHevyMhIW809e/askpKS7NanpKTo1KlTkv7vFl0RERG6ePFiluvS3OgZnNHR0RneNiaNMUYXLlxQTEzMTX2/G20/u78bMqrRaSIjIxUdHW17bYxRRESELXtqaqrd3Px7XGa/Y67fRtrY5ORkh/ICcE3R0dG2mnv69OlMa1JCQoLtFl1pY2+0TrrxMzhjY2NveEvDmJiYTPdLb+RG27+Z3w3X1+g00dHROnPmjN2y628nHB0dneE+fnR0dKa/PzLaBs9WAu5ucXFxtpp76tQpXbp0Kd2Y6Oho277z6dOnbeOTkpKyXCfd+BmciYmJNzwGcO3aNZ07d+6m9jUd2X7a7wZHnxOaUY2W/m8u/53zypUrdk2F2NjYDOtuXFxcpn+fXL+NuLg4buEL3OVSU1PtjnlHRUWl+9s9Pj7ett934cIF29iYmJgs10k3fgZnamqqzp8/n+Xx5pSUFJ07d+6m9jUd2X5CQkK2fjdkVKPTPisiIkJXr1612/a/j/knJSVlWHeTk5Mz/dvi+m0kJydn2JuAkzOARdatW2ckmdWrV9uWHT161AQEBNj+y5s3rylcuLAZMWKESUhIMMYYM2fOHFOsWDEjyRQrVsw2ds6cOVmuy+wzjTHm5MmTpkePHiZ//vzG19fX+Pr6mt69e5tTp07ZjenWrZvJmzevKVSokPHy8jIdO3Y04eHhN/yujmz/jz/+MM2aNTMeHh7G19fXFChQwAwePNhcuXIlyzmbNm2akZQux19//WUkmdmzZ9uWLV682EgyGzduNMOGDTNFihQx7u7uplmzZiYqKsokJiaaoUOH2pYHBQWZo0eP2m03bRubN282o0aNMkWKFDGenp6mQoUKZt26dTecCwDOz8PDw7z66qt2y+677z5bzS1cuLDx9vY27du3NydOnDDGGHP8+HETEBBgJBlfX1/b2CFDhmS5LqvPvHbtmnnttddMiRIljLe3t/H19TVNmzY1v//+u92YMWPGmGLFipl8+fIZLy8vU7duXbNhw4Ybfk9Htn/hwgXTp08fky9fPuPr62s8PT1Ny5YtzV9//ZXlnGVUo9MEBASYp556yvY6JibGSDITJkwwX375pSlZsqTx8fExFSpUMNu2bTPGGPP555+bkiVLmjx58hh/f3/z888/223z39uYM2eOKV26tMmXL58pWLCg+eijj244FwCcX+vWrU39+vXtlo0cOdJWc/39/Y2Hh4epW7eu+e2332xjGjdubLy9vY23t7dtbM2aNW+4LrPPNMaY7777ztSsWdO4u7ubQoUKmYoVK5pvvvnGbsy3335rqlWrZjw9PU2BAgVMqVKlzPTp0x36rjfafkpKipk4caLx9/c3+fLlM56enqZmzZpm6dKlN5yz62t0mn79+pkSJUrYLWvUqJFp1qyZ2b59u6levbopUKCAKVCggPnvf/9rjDFm69atpmbNmsbX19fkyZPHvPnmm+m2m7aNPXv2mDp16piCBQsaT09P07t3bxMfH+/QfABwXh988IGRZKKjo23LFi1aZKu5pUuXNl5eXiYwMNBMnTrVNuaVV14xBQsWNJJM6dKlbeNDQ0OzXJfZZxpjzPbt282jjz5qPD09TaFChYyfn58ZM2aMiY2NtRvz8MMP28bkz5/fDBs2zMTFxd3wuzqy/SVLlphatWoZDw8Pky9fPuPv728mTpxoUlJSspyzjGq0Mf8cX5Jk/vzzT9uysWPHGknmzJkzpm3btqZgwYLG3d3dPP300yYpKcmcO3fOtGnTxhQsWNC4ubmZDh06mJiYGLvtpm0jKirKdOnSxRQqVMi4ubmZhg0bmmPHjt1wLgA4t927dxtJZsGCBbZl586dszvmnXaMeMiQIebq1avGGGOWLl1qihcvbiSZokWL2sZOnz49y3WZfaYxxkRGRpq+ffsaX19f22c+8cQT5vjx47YxZ8+eNb179zb5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",
127
+ "text/plain": [
128
+ "<Figure size 1860x408 with 5 Axes>"
129
+ ]
130
+ },
131
+ "metadata": {},
132
+ "output_type": "display_data"
133
+ }
134
+ ],
135
+ "source": [
136
+ "palette = ListedColormap([\n",
137
+ " \"#1696d2\", \"#0077b6\", \"#00a676\", \"#f59f00\",\n",
138
+ " \"#7b2cbf\", \"#e76f51\", \"#577590\", \"#90be6d\",\n",
139
+ "])\n",
140
+ "\n",
141
+ "def color_regions(labels):\n",
142
+ " regions = sorted(np.unique(labels))\n",
143
+ " neighbors = {region: set() for region in regions}\n",
144
+ " for row in range(L):\n",
145
+ " for column in range(L):\n",
146
+ " here = labels[row, column]\n",
147
+ " if row + 1 < L:\n",
148
+ " there = labels[row + 1, column]\n",
149
+ " if here != there:\n",
150
+ " neighbors[here].add(there)\n",
151
+ " neighbors[there].add(here)\n",
152
+ " if column + 1 < L:\n",
153
+ " there = labels[row, column + 1]\n",
154
+ " if here != there:\n",
155
+ " neighbors[here].add(there)\n",
156
+ " neighbors[there].add(here)\n",
157
+ " assigned = {}\n",
158
+ " for region in regions:\n",
159
+ " used = {assigned[value] for value in neighbors[region] if value in assigned}\n",
160
+ " assigned[region] = next(color for color in range(palette.N) if color not in used)\n",
161
+ " return np.vectorize(assigned.__getitem__)(labels)\n",
162
+ "\n",
163
+ "if leaf_to_input.shape != (L * L,) or not np.array_equal(np.sort(leaf_to_input), np.arange(L * L)):\n",
164
+ " raise ValueError(\"the returned order is not a permutation of the 4x4 public sites\")\n",
165
+ "leaf_position = input_to_leaf\n",
166
+ "levels = int(np.log2(leaf_to_input.size)) + 1\n",
167
+ "fig, axes = plt.subplots(1, levels, figsize=(3.1 * levels, 3.4), constrained_layout=True)\n",
168
+ "for level, axis in enumerate(axes):\n",
169
+ " labels = (leaf_position // (2**level)).reshape(L, L)\n",
170
+ " colors = np.zeros_like(labels) if level == 0 else color_regions(labels)\n",
171
+ " axis.imshow(colors, cmap=palette, vmin=0, vmax=palette.N - 1, origin=\"upper\")\n",
172
+ " for boundary in range(L + 1):\n",
173
+ " axis.plot([-0.5, L - 0.5], [boundary - 0.5, boundary - 0.5], color=\"black\", linewidth=1.0) if boundary in (0, L) else None\n",
174
+ " axis.plot([boundary - 0.5, boundary - 0.5], [-0.5, L - 0.5], color=\"black\", linewidth=1.0) if boundary in (0, L) else None\n",
175
+ " for row in range(L):\n",
176
+ " for column in range(L - 1):\n",
177
+ " if labels[row, column] != labels[row, column + 1]:\n",
178
+ " axis.plot([column + 0.5, column + 0.5], [row - 0.5, row + 0.5], color=\"black\", linewidth=1.0)\n",
179
+ " for row in range(L - 1):\n",
180
+ " for column in range(L):\n",
181
+ " if labels[row, column] != labels[row + 1, column]:\n",
182
+ " axis.plot([column - 0.5, column + 0.5], [row + 0.5, row + 0.5], color=\"black\", linewidth=1.0)\n",
183
+ " axis.set_title(f\"Step {level}\")\n",
184
+ " axis.set_xticks(range(L))\n",
185
+ " axis.set_yticks(range(L))\n",
186
+ " axis.set_xlabel(\"lattice column\")\n",
187
+ " axis.set_ylabel(\"lattice row\" if level == 0 else \"\")\n",
188
+ "plt.show()"
189
+ ]
190
+ }
191
+ ],
192
+ "metadata": {
193
+ "kernelspec": {
194
+ "display_name": "Python 3",
195
+ "language": "python",
196
+ "name": "python3"
197
+ },
198
+ "language_info": {
199
+ "name": "python",
200
+ "version": "3.12"
201
+ }
202
+ },
203
+ "nbformat": 4,
204
+ "nbformat_minor": 5
205
+ }
examples/networkx_system.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from pathlib import Path
5
+
6
+ import networkx as nx
7
+
8
+ from hamiltonzero import SpinHamiltonian
9
+ from hamiltonzero.data import save_system
10
+
11
+
12
+ graph = nx.path_graph(8)
13
+ nx.set_edge_attributes(graph, 1.0, "J")
14
+ nx.set_node_attributes(graph, 0.0, "h")
15
+ system = SpinHamiltonian.from_networkx(graph)
16
+ save_system(Path("outputs/systems/chain_8.json"), system)
examples/train.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "systems": "datasets/train/foundation_5000.jsonl",
3
+ "output": "outputs/foundation.eqx",
4
+ "steps": 1000,
5
+ "n_max": 64
6
+ }
pyproject.toml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hamiltonzero"
7
+ version = "0.1.0"
8
+ description = "Compiled neural wavefunctions for quantum spin systems"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "Apache-2.0 AND MIT"
12
+ license-files = [
13
+ "LICENSE",
14
+ "NOTICE",
15
+ "THIRD_PARTY_NOTICES.md",
16
+ "third_party/folx/LICENSE",
17
+ "third_party/jax/LICENSE",
18
+ "third_party/kfac_jax/LICENSE",
19
+ ]
20
+ dependencies = [
21
+ "absl-py>=2.5",
22
+ "distrax>=0.1.9",
23
+ "dm-tree>=0.1.10",
24
+ "equinox==0.13.6",
25
+ "immutabledict>=4.3",
26
+ "jax @ git+https://github.com/TakeOver/jax.git@79f82535b15a444516d4a5e2beb71d283665b2ff",
27
+ "jaxlib==0.11.0",
28
+ "jaxtyping==0.3.9",
29
+ "networkx>=3.5",
30
+ "numpy>=2.3",
31
+ "optax>=0.2.8",
32
+ "packaging>=26",
33
+ "typing-extensions>=4.15",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ notebooks = [
38
+ "jupyterlab>=4.4",
39
+ "matplotlib>=3.10",
40
+ ]
41
+
42
+ [project.scripts]
43
+ hamiltonzero = "hamiltonzero.cli:main"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+ include = ["hamiltonzero*", "kfac_jax*"]
src/hamiltonzero/__init__.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .hamiltonian import SpinHamiltonian
5
+ from .inference import (
6
+ CompiledOrder,
7
+ EnergySamples,
8
+ PreparedInference,
9
+ burn_in,
10
+ energy,
11
+ prepare,
12
+ spin,
13
+ step,
14
+ )
15
+ from .renyi2 import (
16
+ BasisSamplerState,
17
+ Renyi2Result,
18
+ burn_in_basis,
19
+ measure_renyi2,
20
+ renyi2_purity,
21
+ step_basis,
22
+ )
23
+
24
+ __all__ = [
25
+ "BasisSamplerState",
26
+ "CompiledOrder",
27
+ "EnergySamples",
28
+ "PreparedInference",
29
+ "Renyi2Result",
30
+ "SpinHamiltonian",
31
+ "burn_in",
32
+ "burn_in_basis",
33
+ "energy",
34
+ "measure_renyi2",
35
+ "prepare",
36
+ "renyi2_purity",
37
+ "spin",
38
+ "step",
39
+ "step_basis",
40
+ ]
src/hamiltonzero/checkpoint.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Literal, TypeVar
9
+
10
+ import equinox as eqx
11
+
12
+
13
+ T = TypeVar("T")
14
+ CheckpointKind = Literal["router", "compiled_finetune"]
15
+
16
+
17
+ def _metadata_path(path: str | Path) -> Path:
18
+ source = Path(path)
19
+ return source.with_name(source.name + ".json")
20
+
21
+
22
+ def save_model(
23
+ path: str | Path,
24
+ model: object,
25
+ *,
26
+ kind: CheckpointKind | None = None,
27
+ metadata: dict[str, Any] | None = None,
28
+ ) -> None:
29
+ destination = Path(path)
30
+ destination.parent.mkdir(parents=True, exist_ok=True)
31
+ eqx.tree_serialise_leaves(destination, model)
32
+ if kind is not None:
33
+ payload = {"kind": kind, **(metadata or {})}
34
+ _metadata_path(destination).write_text(
35
+ json.dumps(payload, indent=2, sort_keys=True) + "\n"
36
+ )
37
+
38
+
39
+ def load_model(path: str | Path, template: T) -> T:
40
+ return eqx.tree_deserialise_leaves(Path(path), template)
41
+
42
+
43
+ def load_model_metadata(path: str | Path) -> dict[str, Any] | None:
44
+ source = _metadata_path(path)
45
+ return json.loads(source.read_text()) if source.exists() else None
46
+
47
+
48
+ def save_mcmc(path: str | Path, state: object) -> None:
49
+ destination = Path(path)
50
+ destination.parent.mkdir(parents=True, exist_ok=True)
51
+ eqx.tree_serialise_leaves(destination, state)
52
+
53
+
54
+ def load_mcmc(path: str | Path, template: T) -> T:
55
+ return eqx.tree_deserialise_leaves(Path(path), template)
56
+
57
+
58
+ __all__ = [
59
+ "CheckpointKind",
60
+ "load_mcmc",
61
+ "load_model",
62
+ "load_model_metadata",
63
+ "save_mcmc",
64
+ "save_model",
65
+ ]
src/hamiltonzero/cli.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import dataclasses
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from .config import load_config
12
+
13
+
14
+ def _write_metric(path: Path, metric) -> None:
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ with path.open("a", encoding="utf-8") as stream:
17
+ stream.write(
18
+ json.dumps(dataclasses.asdict(metric), separators=(",", ":")) + "\n"
19
+ )
20
+
21
+
22
+ def _parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(prog="hamiltonzero")
24
+ commands = parser.add_subparsers(dest="mode", required=True)
25
+ for mode in ("train", "finetune"):
26
+ command = commands.add_parser(mode)
27
+ command.add_argument("config", type=Path)
28
+ command.add_argument("--reuse-mcmc", type=Path)
29
+ evaluate = commands.add_parser("eval")
30
+ evaluate.add_argument("config", type=Path)
31
+ pathway = evaluate.add_mutually_exclusive_group()
32
+ pathway.add_argument("--contest", action="store_true")
33
+ pathway.add_argument("--large-n", action="store_true")
34
+ return parser
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> None:
38
+ args = _parser().parse_args(argv)
39
+ config = load_config(args.config, args.mode)
40
+ if args.mode == "eval":
41
+ from .modes.eval import run
42
+
43
+ if args.contest or args.large_n:
44
+ config = dataclasses.replace(
45
+ config,
46
+ contest=bool(args.contest),
47
+ large_n=bool(args.large_n),
48
+ )
49
+ run(config)
50
+ return
51
+ if args.reuse_mcmc is not None:
52
+ config = dataclasses.replace(
53
+ config,
54
+ mcmc=dataclasses.replace(config.mcmc, reuse_mcmc=args.reuse_mcmc),
55
+ )
56
+ metrics_path = config.output.with_name(config.output.name + ".metrics.jsonl")
57
+ sink = lambda metric: _write_metric(metrics_path, metric)
58
+ if args.mode == "train":
59
+ from .modes.train import run_train
60
+
61
+ run_train(config, metric_sink=sink)
62
+ else:
63
+ from .modes.finetune import run_finetune
64
+
65
+ run_finetune(config, metric_sink=sink)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
src/hamiltonzero/compiled/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
src/hamiltonzero/compiled/api.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import jax
7
+ import jax.numpy as jnp
8
+
9
+ from .tree import compile_physical_tree_reference
10
+ from .trunk import bind_shared_kernel, compile_shared_trunk
11
+ from .types import CompiledWaveFunction, CompiledWaveFunctions
12
+
13
+
14
+ def compile_wavefunction(model, context, perm) -> CompiledWaveFunction:
15
+ trunk = compile_shared_trunk(model, context)
16
+ tree = compile_physical_tree_reference(model, trunk, perm)
17
+ return CompiledWaveFunction(kernel=bind_shared_kernel(model), tree=tree)
18
+
19
+
20
+ def compile_wavefunctions(model, context, perms) -> CompiledWaveFunctions:
21
+ trunk = compile_shared_trunk(model, context)
22
+ perms = jnp.asarray(perms, dtype=jnp.int32)
23
+ trees = jax.vmap(lambda perm: compile_physical_tree_reference(model, trunk, perm))(
24
+ perms
25
+ )
26
+ return CompiledWaveFunctions(kernel=bind_shared_kernel(model), trees=trees)
27
+
28
+
29
+ def select_compiled_wavefunction(
30
+ candidates: CompiledWaveFunctions,
31
+ winner,
32
+ ) -> CompiledWaveFunction:
33
+ tree = jax.tree_util.tree_map(lambda value: value[winner], candidates.trees)
34
+ return CompiledWaveFunction(kernel=candidates.kernel, tree=tree)
src/hamiltonzero/compiled/execute.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ import jax
9
+ import jax.numpy as jnp
10
+
11
+ from hamiltonzero.model import normalize_leaf_carriers
12
+
13
+ from .types import CARRY_LEFT, CARRY_RIGHT, EMPTY, MERGE
14
+
15
+
16
+ def _single(values: tuple[Any, ...], name: str) -> Any:
17
+ if len(values) != 1:
18
+ raise ValueError(
19
+ f"compiled HT executor requires exactly one {name}; got {len(values)}"
20
+ )
21
+ return values[0]
22
+
23
+
24
+ def _factorized_apply(factor: Any, h: jax.Array, x: jax.Array) -> jax.Array:
25
+ odd_dtype = jnp.float32
26
+ x_compute = x if x.dtype == odd_dtype else x.astype(odd_dtype)
27
+ V = factor.V if factor.V.dtype == odd_dtype else factor.V.astype(odd_dtype)
28
+ U = factor.U if factor.U.dtype == odd_dtype else factor.U.astype(odd_dtype)
29
+ mixed = (x_compute @ V) * h
30
+ mixed = mixed if mixed.dtype == odd_dtype else mixed.astype(odd_dtype)
31
+ return mixed @ U
32
+
33
+
34
+ def _compiled_quadrilinear_merge(
35
+ T: jax.Array,
36
+ u_a: jax.Array,
37
+ u_b: jax.Array,
38
+ ) -> jax.Array:
39
+ from hamiltonzero.energy import custom_lap_active, quadrilinear_merge_p
40
+
41
+ odd_dtype = jnp.float32
42
+ T = T if T.dtype == odd_dtype else T.astype(odd_dtype)
43
+ u_a = u_a if u_a.dtype == odd_dtype else u_a.astype(odd_dtype)
44
+ u_b = u_b if u_b.dtype == odd_dtype else u_b.astype(odd_dtype)
45
+ if custom_lap_active():
46
+ return quadrilinear_merge_p.bind(T, u_a, u_b)
47
+ G, d_r, _, _ = T.shape
48
+ leading = u_a.shape[:-1]
49
+ u_a_flat = u_a.reshape((-1, G, d_r))
50
+ u_b_flat = u_b.reshape((-1, G, d_r))
51
+ out_flat = jnp.einsum("ijkl,Bik,Bil->Bij", T, u_a_flat, u_b_flat)
52
+ return out_flat.reshape((*leading, G * d_r))
53
+
54
+
55
+ def _opcode_gates(opcodes: jax.Array, dtype: jnp.dtype) -> tuple[jax.Array, ...]:
56
+ both = (opcodes == MERGE).astype(dtype)
57
+ left = (opcodes == CARRY_LEFT).astype(dtype)
58
+ right = (opcodes == CARRY_RIGHT).astype(dtype)
59
+ return both, left, right
60
+
61
+
62
+ def _gate_reference(
63
+ candidate: jax.Array,
64
+ left_value: jax.Array,
65
+ right_value: jax.Array,
66
+ opcodes: jax.Array,
67
+ *,
68
+ feature_axis: bool,
69
+ ) -> jax.Array:
70
+ both, left, right = _opcode_gates(opcodes, candidate.dtype)
71
+ if feature_axis:
72
+ both, left, right = both[..., None], left[..., None], right[..., None]
73
+ pad = candidate.ndim - both.ndim
74
+ shape = (1,) * pad + both.shape
75
+ both, left, right = both.reshape(shape), left.reshape(shape), right.reshape(shape)
76
+ return both * candidate + left * left_value + right * right_value
77
+
78
+
79
+ def execute_wavefunction(kernel: Any, tree: Any, q_routed: jax.Array):
80
+ if len(tree.leaf_combiner_h) != 0 or len(tree.readout_combiner_h) != 0:
81
+ raise ValueError("single-head compiled HT executor does not accept combiners")
82
+ if len(tree.merge_h) != len(tree.opcodes):
83
+ raise ValueError("merge_h and opcodes must have one entry per tree level")
84
+ q_weight = kernel.q_to_odd.weight
85
+ leaf_factor = _single(kernel.leaf_factors, "leaf factor")
86
+ merge_factor = _single(kernel.merge_factors, "merge factor")
87
+ readout_factor = _single(kernel.readout_factors, "readout factor")
88
+ leaf_h = _single(tree.leaf_h, "leaf conditioner")
89
+ readout_h = _single(tree.readout_h, "readout conditioner")
90
+ odd_dtype = jnp.float32
91
+ q_compute = q_routed if q_routed.dtype == odd_dtype else q_routed.astype(odd_dtype)
92
+ q_weight = q_weight if q_weight.dtype == odd_dtype else q_weight.astype(odd_dtype)
93
+ z = q_compute @ q_weight
94
+ u_raw = _factorized_apply(leaf_factor, leaf_h, z)
95
+ u, log_rms = normalize_leaf_carriers(u_raw)
96
+ s = jnp.zeros(u.shape[:-1], dtype=odd_dtype)
97
+ s = s + log_rms.astype(s.dtype)
98
+ for h_level, opcodes in zip(tree.merge_h, tree.opcodes, strict=True):
99
+ if u.shape[-2] != 2 * h_level.shape[-2]:
100
+ raise ValueError("compiled merge level has incompatible shrinking shape")
101
+ u_left, u_right = u[..., 0::2, :], u[..., 1::2, :]
102
+ s_left, s_right = s[..., 0::2], s[..., 1::2]
103
+ raw = _compiled_quadrilinear_merge(kernel.merge_T, u_left, u_right)
104
+ out = raw + _factorized_apply(merge_factor, h_level, raw)
105
+ scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + kernel.merge_eps)
106
+ candidate_u = out / scale[..., None]
107
+ candidate_s = s_left + s_right + jnp.log(scale)
108
+ u = _gate_reference(candidate_u, u_left, u_right, opcodes, feature_axis=True)
109
+ s = _gate_reference(candidate_s, s_left, s_right, opcodes, feature_axis=False)
110
+ if u.shape[-2] != 1:
111
+ raise ValueError("compiled tree did not reduce to one root")
112
+ u_root = u[..., 0, :]
113
+ s_root = s[..., 0]
114
+ psi = _factorized_apply(readout_factor, readout_h, u_root)
115
+ psi_re, psi_im = psi[..., 0], psi[..., 1]
116
+ log_abs = 0.5 * jnp.log(psi_re * psi_re + psi_im * psi_im) + s_root
117
+ phase = jnp.arctan2(psi_im, psi_re)
118
+ return log_abs, phase
src/hamiltonzero/compiled/model.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ import equinox as eqx
9
+ import jax
10
+ import jax.numpy as jnp
11
+ from jaxtyping import Array
12
+
13
+ from hamiltonzero.model import (
14
+ normalize_leaf_carriers,
15
+ quadrilinear_merge,
16
+ tagged_dense_no_bias,
17
+ )
18
+ from hamiltonzero.optim import register_scale_and_shift
19
+
20
+ from .execute import (
21
+ _compiled_quadrilinear_merge,
22
+ _factorized_apply,
23
+ _gate_reference,
24
+ _single,
25
+ )
26
+ from .types import EMPTY, MERGE, OPCODE_DTYPE, CompiledTree, SharedKernel, level_widths
27
+
28
+
29
+ def _scale_tag(y_flat, x_flat, scale_param, *, tag_id: str):
30
+ return register_scale_and_shift(
31
+ y_flat,
32
+ x_flat,
33
+ scale=scale_param,
34
+ tag_id=tag_id,
35
+ )
36
+
37
+
38
+ class CompiledFinetuneWaveFunction(eqx.Module):
39
+ kernel: SharedKernel
40
+ leaf_h: Array
41
+ merge_h: Array
42
+ readout_h: Array
43
+ perm: Array
44
+ inv_perm: Array
45
+ leaf_real: Array
46
+ opcodes: Array
47
+ n_sites: int = eqx.field(static=True)
48
+ r_leaf: int = eqx.field(static=True)
49
+ r_merge: int = eqx.field(static=True)
50
+
51
+ def leaf_h_rows(self) -> Array:
52
+ return self.leaf_h.reshape(self.n_sites, self.r_leaf)
53
+
54
+ def merge_h_level(self, level: int, width: int) -> Array:
55
+ return self.merge_h[level, : width * self.r_merge].reshape(width, self.r_merge)
56
+
57
+ @classmethod
58
+ def from_compiled(cls, kernel: SharedKernel, tree: CompiledTree):
59
+ leaf_h = _single(tree.leaf_h, "leaf conditioner")
60
+ readout_h = _single(tree.readout_h, "readout conditioner")
61
+ n_sites = int(tree.perm.shape[-1])
62
+ widths = level_widths(n_sites)
63
+ w_max = widths[0]
64
+ r_leaf = int(leaf_h.shape[-1])
65
+ r_merge = int(tree.merge_h[0].shape[-1])
66
+ merge_rows = []
67
+ opcode_rows = []
68
+ for width, h_l, ops_l in zip(widths, tree.merge_h, tree.opcodes, strict=True):
69
+ if h_l.shape[-2] != width or ops_l.shape[-1] != width:
70
+ raise ValueError(
71
+ f"level width mismatch: expected {width}, got "
72
+ f"{h_l.shape[-2]}/{ops_l.shape[-1]}"
73
+ )
74
+ pad = w_max - width
75
+ merge_rows.append(
76
+ jnp.pad(h_l, ((0, pad), (0, 0))).reshape(-1) if pad else h_l.reshape(-1)
77
+ )
78
+ opcode_rows.append(
79
+ jnp.pad(ops_l, (0, pad), constant_values=EMPTY) if pad else ops_l
80
+ )
81
+ return cls(
82
+ kernel=kernel,
83
+ leaf_h=leaf_h.reshape(-1),
84
+ merge_h=jnp.stack(merge_rows),
85
+ readout_h=readout_h,
86
+ perm=tree.perm,
87
+ inv_perm=tree.inv_perm,
88
+ leaf_real=tree.leaf_real,
89
+ opcodes=jnp.stack(opcode_rows).astype(OPCODE_DTYPE),
90
+ n_sites=n_sites,
91
+ r_leaf=r_leaf,
92
+ r_merge=r_merge,
93
+ )
94
+
95
+ def as_compiled_tree(self) -> CompiledTree:
96
+ widths = level_widths(self.n_sites)
97
+ return CompiledTree(
98
+ perm=self.perm,
99
+ inv_perm=self.inv_perm,
100
+ leaf_real=self.leaf_real,
101
+ leaf_h=(self.leaf_h_rows(),),
102
+ leaf_combiner_h=(),
103
+ merge_h=tuple(
104
+ self.merge_h_level(i, width) for i, width in enumerate(widths)
105
+ ),
106
+ opcodes=tuple(self.opcodes[i, :width] for i, width in enumerate(widths)),
107
+ readout_h=(self.readout_h,),
108
+ readout_combiner_h=(),
109
+ )
110
+
111
+ def route_q(self, q: Array) -> Array:
112
+ return jnp.take(q, self.perm, axis=-2)
113
+
114
+ def param_counts(self) -> dict:
115
+ kernel_leaves = {
116
+ "q_to_odd": self.kernel.q_to_odd.weight.size,
117
+ "leaf_V": self.kernel.leaf_factors[0].V.size,
118
+ "leaf_U": self.kernel.leaf_factors[0].U.size,
119
+ "merge_T": self.kernel.merge_T.size,
120
+ "merge_V": self.kernel.merge_factors[0].V.size,
121
+ "merge_U": self.kernel.merge_factors[0].U.size,
122
+ "readout_V": self.kernel.readout_factors[0].V.size,
123
+ "readout_U": self.kernel.readout_factors[0].U.size,
124
+ }
125
+ tree_leaves = {
126
+ "leaf_h": self.leaf_h.size,
127
+ "merge_h": self.merge_h.size,
128
+ "readout_h": self.readout_h.size,
129
+ }
130
+ return {
131
+ "kernel": kernel_leaves,
132
+ "tree": tree_leaves,
133
+ "kernel_total": sum(kernel_leaves.values()),
134
+ "tree_total": sum(tree_leaves.values()),
135
+ "total": sum(kernel_leaves.values()) + sum(tree_leaves.values()),
136
+ }
137
+
138
+ def __call__(self, q: Array, ctx: Any = None, t: Any = 0.0):
139
+ del ctx, t
140
+ return self._forward_plain(q)
141
+
142
+ def _forward_plain(self, q: Array):
143
+ kernel = self.kernel
144
+ odd_dtype = jnp.float32
145
+ q_c = q if q.dtype == odd_dtype else q.astype(odd_dtype)
146
+ weight = kernel.q_to_odd.weight
147
+ weight = weight if weight.dtype == odd_dtype else weight.astype(odd_dtype)
148
+ z = q_c @ weight
149
+ u_raw = _factorized_apply(kernel.leaf_factors[0], self.leaf_h_rows(), z)
150
+ u, log_rms = normalize_leaf_carriers(u_raw)
151
+ s = jnp.zeros(u.shape[:-1], dtype=odd_dtype) + log_rms.astype(odd_dtype)
152
+ widths = level_widths(self.n_sites)
153
+ for level, width in enumerate(widths):
154
+ h_level = self.merge_h_level(level, width)
155
+ opcodes = self.opcodes[level, :width]
156
+ u_left, u_right = u[..., 0::2, :], u[..., 1::2, :]
157
+ s_left, s_right = s[..., 0::2], s[..., 1::2]
158
+ raw = _compiled_quadrilinear_merge(kernel.merge_T, u_left, u_right)
159
+ out = raw + _factorized_apply(kernel.merge_factors[0], h_level, raw)
160
+ scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + kernel.merge_eps)
161
+ candidate_u = out / scale[..., None]
162
+ candidate_s = s_left + s_right + jnp.log(scale)
163
+ u = _gate_reference(
164
+ candidate_u, u_left, u_right, opcodes, feature_axis=True
165
+ )
166
+ s = _gate_reference(
167
+ candidate_s, s_left, s_right, opcodes, feature_axis=False
168
+ )
169
+ return self._readout(
170
+ _factorized_apply(kernel.readout_factors[0], self.readout_h, u[..., 0, :]),
171
+ s[..., 0],
172
+ )
173
+
174
+ def _readout(self, psi: Array, s_root: Array):
175
+ psi_re, psi_im = psi[..., 0], psi[..., 1]
176
+ log_abs = 0.5 * jnp.log(psi_re * psi_re + psi_im * psi_im) + s_root
177
+ return log_abs, jnp.arctan2(psi_im, psi_re)
178
+
179
+ def call_tagged(self, q: Array, ctx: Any = None, t: Any = 0.0):
180
+ del ctx, t
181
+ if q.ndim != 2:
182
+ raise ValueError(
183
+ f"call_tagged is per-walker: expected q [P, 4], got {q.shape}"
184
+ )
185
+ kernel = self.kernel
186
+ odd_dtype = jnp.float32
187
+ q_c = q if q.dtype == odd_dtype else q.astype(odd_dtype)
188
+ real = self.leaf_real
189
+ z = tagged_dense_no_bias(
190
+ kernel.q_to_odd.weight,
191
+ q_c,
192
+ tag_id="compiled.q_to_odd",
193
+ pathway="odd",
194
+ kfac_structural_mask=real,
195
+ kfac_scan_shared=False,
196
+ kfac_repeat_ndim=1,
197
+ )
198
+ leaf = kernel.leaf_factors[0]
199
+ vz = tagged_dense_no_bias(
200
+ leaf.V,
201
+ z,
202
+ tag_id="compiled.leaf.V",
203
+ pathway="odd",
204
+ kfac_structural_mask=real,
205
+ kfac_scan_shared=False,
206
+ kfac_repeat_ndim=1,
207
+ )
208
+ vz_flat = vz.reshape(-1)
209
+ mixed = _scale_tag(
210
+ vz_flat * self.leaf_h,
211
+ vz_flat,
212
+ self.leaf_h,
213
+ tag_id="compiled.leaf.h",
214
+ ).reshape(vz.shape)
215
+ u_raw = tagged_dense_no_bias(
216
+ leaf.U,
217
+ mixed,
218
+ tag_id="compiled.leaf.U",
219
+ pathway="odd",
220
+ kfac_structural_mask=real,
221
+ kfac_scan_shared=False,
222
+ kfac_repeat_ndim=1,
223
+ )
224
+ u, log_rms = normalize_leaf_carriers(u_raw)
225
+ s = jnp.zeros(u.shape[:-1], dtype=odd_dtype) + log_rms.astype(odd_dtype)
226
+ merge = kernel.merge_factors[0]
227
+ merge_T = kernel.merge_T
228
+ merge_eps = kernel.merge_eps
229
+
230
+ def level_body(carry, xs):
231
+ u_buffer, s_buffer = carry
232
+ h_level, opcodes = xs
233
+ u_left, u_right = u_buffer[0::2], u_buffer[1::2]
234
+ s_left, s_right = s_buffer[0::2], s_buffer[1::2]
235
+ merge_rows = opcodes == MERGE
236
+ raw = quadrilinear_merge(
237
+ merge_T,
238
+ u_left,
239
+ u_right,
240
+ tag_id="compiled.merge.T",
241
+ pathway="odd",
242
+ kfac_structural_mask=merge_rows,
243
+ kfac_scan_shared=True,
244
+ kfac_repeat_ndim=1,
245
+ )
246
+ vx = tagged_dense_no_bias(
247
+ merge.V,
248
+ raw,
249
+ tag_id="compiled.merge.V",
250
+ pathway="odd",
251
+ kfac_structural_mask=merge_rows,
252
+ kfac_scan_shared=True,
253
+ kfac_repeat_ndim=1,
254
+ )
255
+ vx_flat = vx.reshape(-1)
256
+ mixed_level = _scale_tag(
257
+ vx_flat * h_level,
258
+ vx_flat,
259
+ h_level,
260
+ tag_id="compiled.merge.h",
261
+ ).reshape(vx.shape)
262
+ correction = tagged_dense_no_bias(
263
+ merge.U,
264
+ mixed_level,
265
+ tag_id="compiled.merge.U",
266
+ pathway="odd",
267
+ kfac_structural_mask=merge_rows,
268
+ kfac_scan_shared=True,
269
+ kfac_repeat_ndim=1,
270
+ )
271
+ out = raw + correction
272
+ scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + merge_eps)
273
+ candidate_u = out / scale[..., None]
274
+ candidate_s = s_left + s_right + jnp.log(scale)
275
+ u_next = _gate_reference(
276
+ candidate_u, u_left, u_right, opcodes, feature_axis=True
277
+ )
278
+ s_next = _gate_reference(
279
+ candidate_s, s_left, s_right, opcodes, feature_axis=False
280
+ )
281
+ return (
282
+ jnp.concatenate([u_next, jnp.zeros_like(u_next)], axis=0),
283
+ jnp.concatenate([s_next, jnp.zeros_like(s_next)], axis=0),
284
+ ), None
285
+
286
+ (u_buffer, s_buffer), _ = jax.lax.scan(
287
+ level_body,
288
+ (u, s),
289
+ (self.merge_h, self.opcodes),
290
+ )
291
+ readout = kernel.readout_factors[0]
292
+ vr = tagged_dense_no_bias(
293
+ readout.V,
294
+ u_buffer[0],
295
+ tag_id="compiled.readout.V",
296
+ pathway="odd",
297
+ )
298
+ mixed_readout = _scale_tag(
299
+ vr * self.readout_h,
300
+ vr,
301
+ self.readout_h,
302
+ tag_id="compiled.readout.h",
303
+ )
304
+ psi = tagged_dense_no_bias(
305
+ readout.U,
306
+ mixed_readout,
307
+ tag_id="compiled.readout.U",
308
+ pathway="odd",
309
+ )
310
+ return self._readout(psi, s_buffer[0])
311
+
312
+
313
+ def _expand_stage(V, U, h_2d, new_rank: int, key):
314
+ old_rank = V.shape[-1]
315
+ if new_rank < old_rank:
316
+ raise ValueError(f"cannot shrink rank {old_rank} -> {new_rank}")
317
+ if new_rank == old_rank:
318
+ return V, U, h_2d
319
+ extra = new_rank - old_rank
320
+ key_u, key_h = jax.random.split(key)
321
+ v_new = jnp.zeros((*V.shape[:-1], extra), dtype=V.dtype)
322
+ u_new = jnp.std(U) * jax.random.normal(key_u, (extra, *U.shape[1:]), dtype=U.dtype)
323
+ h_new = jnp.std(h_2d) * jax.random.normal(
324
+ key_h, (*h_2d.shape[:-1], extra), dtype=h_2d.dtype
325
+ )
326
+ return (
327
+ jnp.concatenate([V, v_new], axis=-1),
328
+ jnp.concatenate([U, u_new], axis=0),
329
+ jnp.concatenate([h_2d, h_new], axis=-1),
330
+ )
331
+
332
+
333
+ def expand_rank(
334
+ model: CompiledFinetuneWaveFunction,
335
+ *,
336
+ leaf_rank: int,
337
+ merge_rank: int,
338
+ key,
339
+ ) -> CompiledFinetuneWaveFunction:
340
+ key_leaf, key_merge = jax.random.split(jnp.asarray(key), 3)[:2]
341
+ kernel = model.kernel
342
+ leaf = kernel.leaf_factors[0]
343
+ merge = kernel.merge_factors[0]
344
+ n_sites = model.n_sites
345
+ max_width = n_sites // 2
346
+ n_levels = model.merge_h.shape[0]
347
+ leaf_h = model.leaf_h.reshape(n_sites, model.r_leaf)
348
+ merge_h = model.merge_h.reshape(n_levels, max_width, model.r_merge)
349
+ readout_h = model.readout_h
350
+ r_leaf, r_merge = model.r_leaf, model.r_merge
351
+ V, U, leaf_h = _expand_stage(leaf.V, leaf.U, leaf_h, int(leaf_rank), key_leaf)
352
+ leaf = eqx.tree_at(lambda factor: (factor.V, factor.U), leaf, (V, U))
353
+ r_leaf = int(leaf_rank)
354
+ V, U, merge_h = _expand_stage(merge.V, merge.U, merge_h, int(merge_rank), key_merge)
355
+ merge = eqx.tree_at(lambda factor: (factor.V, factor.U), merge, (V, U))
356
+ r_merge = int(merge_rank)
357
+ kernel = eqx.tree_at(
358
+ lambda value: (
359
+ value.leaf_factors,
360
+ value.merge_factors,
361
+ ),
362
+ kernel,
363
+ ((leaf,), (merge,)),
364
+ )
365
+ return CompiledFinetuneWaveFunction(
366
+ kernel=kernel,
367
+ leaf_h=leaf_h.reshape(-1),
368
+ merge_h=merge_h.reshape(n_levels, -1),
369
+ readout_h=readout_h,
370
+ perm=model.perm,
371
+ inv_perm=model.inv_perm,
372
+ leaf_real=model.leaf_real,
373
+ opcodes=model.opcodes,
374
+ n_sites=n_sites,
375
+ r_leaf=r_leaf,
376
+ r_merge=r_merge,
377
+ )
378
+
379
+
380
+ def compile_finetune_model(
381
+ eager_model,
382
+ ctx_row,
383
+ *,
384
+ leaf_rank: int,
385
+ merge_rank: int,
386
+ physical_perm,
387
+ key,
388
+ ) -> CompiledFinetuneWaveFunction:
389
+ from .tree import compile_physical_tree_reference
390
+ from .trunk import bind_shared_kernel, compile_shared_trunk
391
+
392
+ shared_trunk = compile_shared_trunk(eager_model, ctx_row)
393
+ n_sites = int(shared_trunk.real_mask.shape[-1])
394
+ identity = jnp.arange(n_sites, dtype=jnp.int32)
395
+ tree = compile_physical_tree_reference(eager_model, shared_trunk, identity)
396
+ model = CompiledFinetuneWaveFunction.from_compiled(
397
+ bind_shared_kernel(eager_model), tree
398
+ )
399
+ model = expand_rank(
400
+ model,
401
+ leaf_rank=leaf_rank,
402
+ merge_rank=merge_rank,
403
+ key=key,
404
+ )
405
+ physical_perm = jnp.asarray(physical_perm, dtype=jnp.int32)
406
+ if physical_perm.shape != (n_sites,):
407
+ raise ValueError(
408
+ f"physical_perm must have shape {(n_sites,)}, got {physical_perm.shape}"
409
+ )
410
+ model = eqx.tree_at(
411
+ lambda value: (value.perm, value.inv_perm),
412
+ model,
413
+ (
414
+ physical_perm,
415
+ jnp.argsort(physical_perm).astype(jnp.int32),
416
+ ),
417
+ )
418
+ return model
419
+
420
+
421
+ def build_finetune_template_model(
422
+ eager_model,
423
+ n_sites: int,
424
+ *,
425
+ leaf_rank: int,
426
+ merge_rank: int,
427
+ ) -> CompiledFinetuneWaveFunction:
428
+ from .trunk import bind_shared_kernel
429
+
430
+ kernel = bind_shared_kernel(eager_model)
431
+ widths = level_widths(int(n_sites))
432
+ r_leaf = int(kernel.leaf_factors[0].V.shape[-1])
433
+ r_merge = int(kernel.merge_factors[0].V.shape[-1])
434
+ r_readout = int(kernel.readout_factors[0].V.shape[-1])
435
+ tree = CompiledTree(
436
+ perm=jnp.arange(n_sites, dtype=jnp.int32),
437
+ inv_perm=jnp.arange(n_sites, dtype=jnp.int32),
438
+ leaf_real=jnp.ones((n_sites,), dtype=jnp.bool_),
439
+ leaf_h=(jnp.ones((n_sites, r_leaf), dtype=jnp.float32),),
440
+ leaf_combiner_h=(),
441
+ merge_h=tuple(
442
+ jnp.ones((width, r_merge), dtype=jnp.float32) for width in widths
443
+ ),
444
+ opcodes=tuple(
445
+ jnp.full((width,), MERGE, dtype=OPCODE_DTYPE) for width in widths
446
+ ),
447
+ readout_h=(jnp.ones((r_readout,), dtype=jnp.float32),),
448
+ readout_combiner_h=(),
449
+ )
450
+ model = CompiledFinetuneWaveFunction.from_compiled(kernel, tree)
451
+ return expand_rank(
452
+ model,
453
+ leaf_rank=leaf_rank,
454
+ merge_rank=merge_rank,
455
+ key=jax.random.PRNGKey(0),
456
+ )
src/hamiltonzero/compiled/tree.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ import equinox as eqx
9
+ import jax
10
+ import jax.numpy as jnp
11
+
12
+ from hamiltonzero.model import (
13
+ edge_merge_masked,
14
+ tagged_dense,
15
+ tagged_rms_eqx_style,
16
+ tree_active_clock_depth,
17
+ tree_depth_count_features,
18
+ tree_sphere,
19
+ )
20
+
21
+ from .types import (
22
+ CARRY_LEFT,
23
+ CARRY_RIGHT,
24
+ EMPTY,
25
+ MERGE,
26
+ CompiledTree,
27
+ LadderProjectionKernel,
28
+ PhysicalCompilerKernel,
29
+ )
30
+
31
+
32
+ def bind_physical_compiler_kernel(model: Any) -> PhysicalCompilerKernel:
33
+ leaf = eqx.tree_at(lambda x: (x.P_u.V, x.P_u.U), model.leaf, (None, None))
34
+ merge = eqx.tree_at(
35
+ lambda x: (x.T, x.output_hypernet.V, x.output_hypernet.U),
36
+ model.merge,
37
+ (None, None, None),
38
+ )
39
+ readout = eqx.tree_at(
40
+ lambda x: (x.output_hypernet.V, x.output_hypernet.U),
41
+ model.readout,
42
+ (None, None),
43
+ )
44
+ return PhysicalCompilerKernel(
45
+ contextualizer=model.readout_leaf_context,
46
+ global_fork=model.gladder_fork_phys,
47
+ leaf=leaf,
48
+ merge=merge,
49
+ readout=readout,
50
+ leaf_projection=LadderProjectionKernel(
51
+ model.gladder_to_gemb_w,
52
+ model.gladder_to_gemb_b,
53
+ model.gladder_gemb_ln_s,
54
+ ),
55
+ tree_pool=model.gladder_tree_pool,
56
+ tree_update=model.gladder_tree_update,
57
+ tree_projection_weight=model.gladder_tree_proj_w,
58
+ tree_projection_bias=model.gladder_tree_proj_b,
59
+ root_projection=LadderProjectionKernel(
60
+ model.gladder_root_proj_w,
61
+ model.gladder_root_proj_b,
62
+ model.gladder_root_ln_s,
63
+ ),
64
+ )
65
+
66
+
67
+ def _project_global(
68
+ projection: LadderProjectionKernel,
69
+ value,
70
+ *,
71
+ dense_tag: str,
72
+ norm_tag: str,
73
+ ):
74
+ structural_active = jnp.asarray(True)
75
+ out = tagged_dense(
76
+ projection.weight,
77
+ projection.bias,
78
+ value,
79
+ tag_id=dense_tag,
80
+ pathway="even",
81
+ kfac_structural_mask=structural_active,
82
+ kfac_scan_shared=False,
83
+ kfac_repeat_ndim=0,
84
+ )
85
+ return tagged_rms_eqx_style(
86
+ projection.norm_scale,
87
+ out,
88
+ tag_id=norm_tag,
89
+ pathway="even",
90
+ kfac_structural_mask=structural_active,
91
+ kfac_scan_shared=False,
92
+ kfac_repeat_ndim=0,
93
+ )
94
+
95
+
96
+ def compile_context_only_reduction(
97
+ *,
98
+ merge,
99
+ c_leaf,
100
+ leaf_real,
101
+ g_emb,
102
+ edges,
103
+ structural_mask,
104
+ gladder,
105
+ n_total=None,
106
+ clock_depth=None,
107
+ initial_counts=None,
108
+ level_offset: int = 0,
109
+ feature_n_levels=None,
110
+ ):
111
+ c = jnp.asarray(c_leaf)
112
+ m = jnp.asarray(leaf_real, dtype=c.dtype)
113
+ if c.shape[0] != m.shape[0]:
114
+ raise ValueError("c_leaf and leaf_real widths differ")
115
+ n = c.shape[0]
116
+ if n == 0 or n & (n - 1):
117
+ raise ValueError(f"context-only width must be a power of two, got {n}")
118
+ k = jnp.asarray(structural_mask, dtype=c.dtype)
119
+ if k.shape != m.shape:
120
+ raise ValueError("structural_mask and leaf_real widths differ")
121
+ n_total = jnp.sum(m) if n_total is None else n_total
122
+ feature_n_levels = (
123
+ tree_active_clock_depth(m) if feature_n_levels is None else feature_n_levels
124
+ )
125
+ clock_depth = tree_active_clock_depth(k) if clock_depth is None else clock_depth
126
+ counts = m if initial_counts is None else jnp.asarray(initial_counts, dtype=m.dtype)
127
+ if counts.shape != m.shape:
128
+ raise ValueError("initial_counts and leaf_real widths differ")
129
+ candidates = []
130
+ carried = []
131
+ depth_levels = []
132
+ opcode_levels = []
133
+ g_curr = g_emb
134
+ e_curr = edges
135
+ e_curr = tree_sphere(e_curr)
136
+ level = int(level_offset)
137
+ while c.shape[0] > 1:
138
+ c_a, c_b = c[0::2], c[1::2]
139
+ m_a, m_b = m[0::2], m[1::2]
140
+ k_a, k_b = k[0::2], k[1::2]
141
+ pair_count = c_a.shape[0]
142
+ pair_idx = jnp.arange(pair_count, dtype=jnp.int32)
143
+ both_struct = k_a * k_b
144
+ pair_base = jnp.maximum(
145
+ jnp.sum((k_a + k_b - k_a * k_b).astype(jnp.int32)),
146
+ jnp.asarray(2, dtype=jnp.int32),
147
+ )
148
+ cnt_a, cnt_b = counts[0::2], counts[1::2]
149
+ depth = tree_depth_count_features(
150
+ cnt_a, cnt_b, n_total, level, feature_n_levels, c.dtype
151
+ )
152
+ counts = cnt_a + cnt_b
153
+ depth_levels.append(depth)
154
+ d_edge = e_curr.shape[-1]
155
+ e_pairs = e_curr.reshape(pair_count, 2, pair_count, 2, d_edge)
156
+ sibling_lr = e_pairs[pair_idx, 0, pair_idx, 1]
157
+ sibling_rl = e_pairs[pair_idx, 1, pair_idx, 0]
158
+ level_active = jnp.any(both_struct.astype(bool))
159
+ g_level = g_curr
160
+ g_level = tagged_dense(
161
+ gladder[2],
162
+ gladder[3],
163
+ g_curr,
164
+ tag_id="gladder.tree.proj",
165
+ pathway="even",
166
+ kfac_structural_mask=level_active,
167
+ kfac_scan_shared=False,
168
+ kfac_repeat_ndim=0,
169
+ )
170
+
171
+ def candidate_one(ca, cb, elr, erl, dep, pidx, struct_active):
172
+ return merge.context_candidate(
173
+ ca,
174
+ cb,
175
+ g_level,
176
+ sibling_edge_lr=elr,
177
+ sibling_edge_rl=erl,
178
+ level_idx=jnp.int32(level),
179
+ pair_idx=pidx,
180
+ pair_base=pair_base,
181
+ clock_depth=clock_depth,
182
+ depth_feats=dep,
183
+ kfac_structural_mask=struct_active,
184
+ kfac_g_structural_mask=level_active,
185
+ kfac_scan_shared=False,
186
+ )
187
+
188
+ candidate = jax.vmap(candidate_one)(
189
+ c_a,
190
+ c_b,
191
+ sibling_lr,
192
+ sibling_rl,
193
+ depth,
194
+ pair_idx,
195
+ both_struct,
196
+ )
197
+ candidates.append(candidate)
198
+ gate_m_a, gate_m_b = k_a, k_b
199
+ gate_both = gate_m_a * gate_m_b
200
+ gate_a = gate_m_a * (1.0 - gate_m_b)
201
+ gate_b = (1.0 - gate_m_a) * gate_m_b
202
+ c = (
203
+ gate_both[:, None] * candidate
204
+ + gate_a[:, None] * c_a
205
+ + gate_b[:, None] * c_b
206
+ )
207
+ m = m_a + m_b - m_a * m_b
208
+ k = k_a + k_b - k_a * k_b
209
+ opcode_levels.append(
210
+ jnp.where(
211
+ m_a.astype(jnp.bool_),
212
+ jnp.where(m_b.astype(jnp.bool_), MERGE, CARRY_LEFT),
213
+ jnp.where(m_b.astype(jnp.bool_), CARRY_RIGHT, EMPTY),
214
+ ).astype(jnp.uint8)
215
+ )
216
+ attn_mask = both_struct
217
+ d_edge = e_curr.shape[-1]
218
+ e_blocks = e_curr.reshape(pair_count, 2, pair_count, 2, d_edge)
219
+ e00, e01 = e_blocks[:, 0, :, 0], e_blocks[:, 0, :, 1]
220
+ e10, e11 = e_blocks[:, 1, :, 0], e_blocks[:, 1, :, 1]
221
+
222
+ def edge_row(e0, e1, e2, e3, ma, mb, ka, kb, ca, cb):
223
+ return jax.vmap(
224
+ lambda x0, x1, x2, x3, mqa, mqb, kqa, kqb, cqa, cqb: edge_merge_masked(
225
+ x0,
226
+ x1,
227
+ x2,
228
+ x3,
229
+ ma,
230
+ mb,
231
+ mqa,
232
+ mqb,
233
+ ca,
234
+ cb,
235
+ cqa,
236
+ cqb,
237
+ merge.edge_merge,
238
+ k_2i=ka,
239
+ k_2i1=kb,
240
+ k_2j=kqa,
241
+ k_2j1=kqb,
242
+ kfac_scan_shared=False,
243
+ )[0]
244
+ )(e0, e1, e2, e3, m_a, m_b, k_a, k_b, c_a, c_b)
245
+
246
+ e_new = jax.vmap(edge_row)(e00, e01, e10, e11, m_a, m_b, k_a, k_b, c_a, c_b)
247
+ e_new = merge.tree_edge_fwl.apply_residual(
248
+ e_new, c, attn_mask, kfac_scan_shared=False
249
+ )
250
+ edge_keep = (both_struct[:, None] * both_struct[None, :]).astype(bool)
251
+ e_curr = jnp.where(edge_keep[..., None], e_new, e00)
252
+ e_curr = jnp.where(edge_keep[..., None], tree_sphere(e_curr), e00)
253
+ c_skip = c
254
+ c = merge.level_edge_attn(
255
+ c,
256
+ e_curr,
257
+ attn_mask,
258
+ level_idx=jnp.int32(level),
259
+ kfac_scan_shared=False,
260
+ )
261
+ c = jnp.where(attn_mask.astype(bool)[:, None], tree_sphere(c), c_skip)
262
+ carried.append(c)
263
+ level_mask = k
264
+ update_active = jnp.any(attn_mask.astype(bool))
265
+ pool_structural_mask = level_mask.astype(c.dtype) * update_active.astype(
266
+ c.dtype
267
+ )
268
+ pooled = gladder[0](
269
+ g_curr,
270
+ c,
271
+ level_mask.astype(c.dtype),
272
+ kfac_structural_mask=pool_structural_mask,
273
+ kfac_update_mask=update_active,
274
+ kfac_scan_shared=False,
275
+ kfac_repeat_ndim=1,
276
+ )
277
+ g_curr = gladder[1](
278
+ g_curr,
279
+ pooled,
280
+ update_mask=update_active,
281
+ kfac_structural_mask=update_active,
282
+ kfac_scan_shared=False,
283
+ )
284
+ level += 1
285
+ e_root = e_curr[0, 0]
286
+ merge_h = tuple(
287
+ compile_merge_h(
288
+ merge,
289
+ candidate,
290
+ depth_levels[i],
291
+ )
292
+ for i, candidate in enumerate(candidates)
293
+ )
294
+ return {
295
+ "c_candidate": tuple(candidates),
296
+ "c_carried": tuple(carried),
297
+ "depth_features": tuple(depth_levels),
298
+ "merge_h": merge_h,
299
+ "opcodes": tuple(opcode_levels),
300
+ "c_root": c[0],
301
+ "e_root": e_root,
302
+ "g_final": g_curr,
303
+ }
304
+
305
+
306
+ def project_conditioner(context, hypernet):
307
+ return jnp.matmul(context, hypernet.W_h)
308
+
309
+
310
+ def leaf_context(leaf_builder, e_leaf, g_emb):
311
+ g_broadcast = jnp.broadcast_to(g_emb, e_leaf.shape[:-1] + g_emb.shape)
312
+ return jnp.concatenate((e_leaf, g_broadcast), axis=-1)
313
+
314
+
315
+ def compile_target_leaf_h(leaf_builder, e_leaf, g_emb):
316
+ context = leaf_context(leaf_builder, e_leaf, g_emb)
317
+ return (project_conditioner(context, leaf_builder.P_u),)
318
+
319
+
320
+ def merge_context(merge, c_p_candidate, depth_features):
321
+ return jnp.concatenate(
322
+ (c_p_candidate, depth_features.astype(c_p_candidate.dtype)), axis=-1
323
+ )
324
+
325
+
326
+ def compile_merge_h(merge, c_p_candidate, depth_features):
327
+ return project_conditioner(
328
+ merge_context(merge, c_p_candidate, depth_features), merge.output_hypernet
329
+ )
330
+
331
+
332
+ def readout_context(readout, e_root, c_root, g_emb):
333
+ e_norm = readout.ln_e(e_root, pathway="even")
334
+ return jnp.concatenate(
335
+ (e_norm, c_root.astype(e_norm.dtype), g_emb.astype(e_norm.dtype)), axis=-1
336
+ )
337
+
338
+
339
+ def compile_target_readout_h(readout, e_root, c_root, g_emb):
340
+ context = readout_context(readout, e_root, c_root, g_emb)
341
+ return (project_conditioner(context, readout.output_hypernet),)
342
+
343
+
344
+ def classify_merge_opcodes(leaf_real):
345
+ active = jnp.asarray(leaf_real, dtype=jnp.bool_)
346
+ n = active.shape[0]
347
+ if n == 0 or n & (n - 1):
348
+ raise ValueError(f"leaf_real width must be a nonzero power of two, got {n}")
349
+ levels = []
350
+ while active.shape[0] > 1:
351
+ left = active[0::2]
352
+ right = active[1::2]
353
+ opcode = jnp.where(
354
+ left,
355
+ jnp.where(right, MERGE, CARRY_LEFT),
356
+ jnp.where(right, CARRY_RIGHT, EMPTY),
357
+ ).astype(jnp.uint8)
358
+ levels.append(opcode)
359
+ active = left | right
360
+ return tuple(levels)
361
+
362
+
363
+ def assemble_compiled_tree(*, perm, leaf_real, boundaries) -> CompiledTree:
364
+ perm = jnp.asarray(perm, dtype=jnp.int32)
365
+ if perm.ndim != 1:
366
+ raise ValueError(f"perm must be rank one, got shape {perm.shape}")
367
+ leaf_real = jnp.asarray(leaf_real, dtype=jnp.bool_)
368
+ if leaf_real.shape != perm.shape:
369
+ raise ValueError(
370
+ f"leaf_real shape {leaf_real.shape} must match perm {perm.shape}"
371
+ )
372
+ inv_perm = jnp.argsort(perm).astype(jnp.int32)
373
+ return CompiledTree(
374
+ perm=perm,
375
+ inv_perm=inv_perm,
376
+ leaf_real=leaf_real,
377
+ leaf_h=tuple(boundaries["leaf_h"]),
378
+ leaf_combiner_h=tuple(boundaries["leaf_combiner_h"]),
379
+ merge_h=tuple(boundaries["merge_h"]),
380
+ opcodes=tuple(boundaries["opcodes"]),
381
+ readout_h=tuple(boundaries["readout_h"]),
382
+ readout_combiner_h=tuple(boundaries["readout_combiner_h"]),
383
+ )
384
+
385
+
386
+ def compile_physical_tree_from_reduced_state(
387
+ kernel: PhysicalCompilerKernel,
388
+ *,
389
+ perm,
390
+ leaf_real,
391
+ leaf_h,
392
+ c_reduced,
393
+ edge_reduced,
394
+ real_reduced,
395
+ structural_reduced,
396
+ counts_reduced,
397
+ g_reduced,
398
+ early_merge_h=(),
399
+ early_opcodes=(),
400
+ full_structural_mask=None,
401
+ ) -> CompiledTree:
402
+ perm = jnp.asarray(perm, dtype=jnp.int32)
403
+ leaf_real = jnp.asarray(leaf_real)
404
+ if perm.ndim != 1 or leaf_real.shape != perm.shape:
405
+ raise ValueError("perm and leaf_real must be matching rank-one arrays")
406
+ early_merge_h = tuple(early_merge_h)
407
+ early_opcodes = tuple(early_opcodes)
408
+ if len(early_merge_h) != len(early_opcodes):
409
+ raise ValueError("early merge_h/opcode level counts differ")
410
+ if full_structural_mask is None:
411
+ full_structural_mask = leaf_real
412
+ level_offset = len(early_merge_h)
413
+ reduced = compile_context_only_reduction(
414
+ merge=kernel.merge,
415
+ c_leaf=c_reduced,
416
+ leaf_real=real_reduced,
417
+ g_emb=g_reduced,
418
+ edges=edge_reduced,
419
+ structural_mask=structural_reduced,
420
+ n_total=jnp.sum(leaf_real),
421
+ clock_depth=tree_active_clock_depth(jnp.asarray(full_structural_mask)),
422
+ gladder=(
423
+ kernel.tree_pool,
424
+ kernel.tree_update,
425
+ kernel.tree_projection_weight,
426
+ kernel.tree_projection_bias,
427
+ ),
428
+ initial_counts=counts_reduced,
429
+ level_offset=level_offset,
430
+ feature_n_levels=tree_active_clock_depth(leaf_real),
431
+ )
432
+ readout_g_emb = _project_global(
433
+ kernel.root_projection,
434
+ reduced["g_final"],
435
+ dense_tag="gladder.root_proj",
436
+ norm_tag="gladder.root_ln",
437
+ )
438
+ boundaries = {
439
+ "leaf_h": tuple(leaf_h),
440
+ "leaf_combiner_h": (),
441
+ "merge_h": early_merge_h + tuple(reduced["merge_h"]),
442
+ "opcodes": early_opcodes + tuple(reduced["opcodes"]),
443
+ "readout_h": compile_target_readout_h(
444
+ kernel.readout,
445
+ reduced["e_root"],
446
+ reduced["c_root"],
447
+ readout_g_emb,
448
+ ),
449
+ "readout_combiner_h": (),
450
+ }
451
+ return assemble_compiled_tree(
452
+ perm=perm,
453
+ leaf_real=leaf_real,
454
+ boundaries=boundaries,
455
+ )
456
+
457
+
458
+ def compile_physical_tree_from_shared_trunk(
459
+ kernel: PhysicalCompilerKernel,
460
+ shared_trunk,
461
+ perm,
462
+ ) -> CompiledTree:
463
+ perm = jnp.asarray(perm, dtype=jnp.int32)
464
+ if perm.ndim != 1 or perm.shape != shared_trunk.real_mask.shape:
465
+ raise ValueError("perm must be rank one and match the shared trunk site width")
466
+ node = shared_trunk.node_raw[perm]
467
+ edge = shared_trunk.edge_raw[perm][:, perm]
468
+ leaf_real = shared_trunk.real_mask[perm]
469
+ structural_mask = shared_trunk.balanced_mask
470
+ e_leaf, edge_leaf, g_stream = kernel.contextualizer.with_edge(
471
+ node,
472
+ edge,
473
+ leaf_real,
474
+ structural_mask,
475
+ g=shared_trunk.global_stream,
476
+ )
477
+ g_stream = kernel.global_fork(g_stream, edge_leaf, structural_mask)
478
+ leaf_g_emb = _project_global(
479
+ kernel.leaf_projection,
480
+ g_stream,
481
+ dense_tag="gladder.to_gemb",
482
+ norm_tag="gladder.gemb_ln",
483
+ )
484
+ c_leaf = tree_sphere(kernel.leaf.P_c(e_leaf, pathway="even"))
485
+ reduced = compile_context_only_reduction(
486
+ merge=kernel.merge,
487
+ c_leaf=c_leaf,
488
+ leaf_real=leaf_real,
489
+ g_emb=g_stream,
490
+ edges=edge_leaf,
491
+ structural_mask=structural_mask,
492
+ gladder=(
493
+ kernel.tree_pool,
494
+ kernel.tree_update,
495
+ kernel.tree_projection_weight,
496
+ kernel.tree_projection_bias,
497
+ ),
498
+ )
499
+ readout_g_emb = _project_global(
500
+ kernel.root_projection,
501
+ reduced["g_final"],
502
+ dense_tag="gladder.root_proj",
503
+ norm_tag="gladder.root_ln",
504
+ )
505
+ boundaries = {
506
+ "leaf_h": compile_target_leaf_h(
507
+ kernel.leaf,
508
+ e_leaf,
509
+ leaf_g_emb,
510
+ ),
511
+ "leaf_combiner_h": (),
512
+ "merge_h": reduced["merge_h"],
513
+ "opcodes": reduced["opcodes"],
514
+ "readout_h": compile_target_readout_h(
515
+ kernel.readout,
516
+ reduced["e_root"],
517
+ reduced["c_root"],
518
+ readout_g_emb,
519
+ ),
520
+ "readout_combiner_h": (),
521
+ }
522
+ return assemble_compiled_tree(
523
+ perm=perm,
524
+ leaf_real=leaf_real,
525
+ boundaries=boundaries,
526
+ )
527
+
528
+
529
+ def compile_physical_tree_reference(
530
+ model,
531
+ shared_trunk,
532
+ perm,
533
+ ) -> CompiledTree:
534
+ if model.gladder_post is None or model.gladder_fork_phys is None:
535
+ raise ValueError(
536
+ "reference physical compiler requires the target global ladder"
537
+ )
538
+ perm = jnp.asarray(perm, dtype=jnp.int32)
539
+ if perm.ndim != 1 or perm.shape != shared_trunk.real_mask.shape:
540
+ raise ValueError("perm must be rank one and match the shared trunk site width")
541
+ node = shared_trunk.node_raw[perm]
542
+ edge = shared_trunk.edge_raw[perm][:, perm]
543
+ leaf_real = shared_trunk.real_mask[perm]
544
+ structural_mask = shared_trunk.balanced_mask
545
+ e_leaf, edge_leaf, g_stream = model._contextualize_leaf_even_with_edge_g(
546
+ node,
547
+ edge,
548
+ leaf_real,
549
+ structural_mask,
550
+ shared_trunk.global_stream,
551
+ )
552
+ g_stream = model.gladder_fork_phys(g_stream, edge_leaf, structural_mask)
553
+ leaf_g_emb = model._gladder_project(g_stream)
554
+ c_leaf = tree_sphere(model.leaf.P_c(e_leaf, pathway="even"))
555
+ reduced = compile_context_only_reduction(
556
+ merge=model.merge,
557
+ c_leaf=c_leaf,
558
+ leaf_real=leaf_real,
559
+ g_emb=g_stream,
560
+ edges=edge_leaf,
561
+ structural_mask=structural_mask,
562
+ gladder=model._gladder_tree_refs(),
563
+ )
564
+ readout_g_emb = model._gladder_root_project(reduced["g_final"])
565
+ boundaries = {
566
+ "leaf_h": compile_target_leaf_h(model.leaf, e_leaf, leaf_g_emb),
567
+ "leaf_combiner_h": (),
568
+ "merge_h": reduced["merge_h"],
569
+ "opcodes": reduced["opcodes"],
570
+ "readout_h": compile_target_readout_h(
571
+ model.readout,
572
+ reduced["e_root"],
573
+ reduced["c_root"],
574
+ readout_g_emb,
575
+ ),
576
+ "readout_combiner_h": (),
577
+ }
578
+ return assemble_compiled_tree(
579
+ perm=perm,
580
+ leaf_real=leaf_real,
581
+ boundaries=boundaries,
582
+ )
src/hamiltonzero/compiled/trunk.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import equinox as eqx
7
+ import jax
8
+ import jax.numpy as jnp
9
+
10
+ from hamiltonzero.model import tree_sphere
11
+
12
+ from .types import (
13
+ CanonicalHamiltonian,
14
+ FactorizedQSide,
15
+ QSideLeafInput,
16
+ SharedKernel,
17
+ SharedTrunk,
18
+ TrunkCompilerKernel,
19
+ )
20
+
21
+
22
+ def _qside(hypernet) -> FactorizedQSide:
23
+ return FactorizedQSide(V=hypernet.V, U=hypernet.U)
24
+
25
+
26
+ def bind_shared_kernel(model) -> SharedKernel:
27
+ return SharedKernel(
28
+ q_to_odd=QSideLeafInput(weight=model.q_to_odd.weight),
29
+ leaf_factors=(_qside(model.leaf.P_u),),
30
+ leaf_combiner_factors=(),
31
+ merge_T=model.merge.T,
32
+ merge_factors=(_qside(model.merge.output_hypernet),),
33
+ readout_factors=(_qside(model.readout.output_hypernet),),
34
+ merge_eps=float(model.merge.eps),
35
+ )
36
+
37
+
38
+ def bind_trunk_compiler_kernel(model) -> TrunkCompilerKernel:
39
+ return TrunkCompilerKernel(
40
+ featurizer=model.featurizer,
41
+ trunk=model.trunk,
42
+ shared_global=model.gladder_post,
43
+ )
44
+
45
+
46
+ class _TrunkMaskContext(eqx.Module):
47
+ mask: jax.Array
48
+
49
+
50
+ def compile_canonical_shared_trunk(
51
+ kernel: TrunkCompilerKernel,
52
+ canonical: CanonicalHamiltonian,
53
+ ) -> SharedTrunk:
54
+ if canonical.node_mask.ndim != 2 or canonical.node_mask.shape[0] != 1:
55
+ raise ValueError("compiled shared trunk requires exact physical P=1")
56
+ if canonical.balanced_mask.shape != canonical.node_mask.shape:
57
+ raise ValueError("balanced_mask must match node_mask shape")
58
+ graph = canonical.graph_inputs
59
+ if graph.node.shape[:2] != canonical.node_mask.shape:
60
+ raise ValueError("canonical graph node width must match node_mask")
61
+ if graph.edge.shape[:3] != (
62
+ 1,
63
+ canonical.node_mask.shape[1],
64
+ canonical.node_mask.shape[1],
65
+ ):
66
+ raise ValueError("canonical graph edge width must match node_mask")
67
+
68
+ def one(edge_input, node_input, real_mask, balanced_mask):
69
+ edge_feat, local_feat, global_feat = kernel.featurizer(
70
+ edge_input,
71
+ real_mask,
72
+ node_input,
73
+ )
74
+ g_seed = tree_sphere(global_feat.astype(local_feat.dtype))
75
+ node_raw, edge_raw, g_seed = kernel.trunk(
76
+ _TrunkMaskContext(real_mask),
77
+ edge_feat,
78
+ local_feat,
79
+ g_seed,
80
+ )
81
+ global_stream = kernel.shared_global(
82
+ g_seed.astype(edge_raw.dtype), edge_raw, real_mask
83
+ )
84
+ return SharedTrunk(
85
+ node_raw=node_raw,
86
+ edge_raw=edge_raw,
87
+ global_raw=global_feat,
88
+ global_stream=global_stream,
89
+ real_mask=real_mask,
90
+ balanced_mask=balanced_mask,
91
+ )
92
+
93
+ return jax.vmap(one)(
94
+ graph.edge,
95
+ graph.node,
96
+ canonical.node_mask,
97
+ canonical.balanced_mask,
98
+ )
99
+
100
+
101
+ def select_single_physical_trunk(trunk: SharedTrunk) -> SharedTrunk:
102
+ leaves = jax.tree_util.tree_leaves(trunk)
103
+ if not leaves or any(x.ndim < 1 or x.shape[0] != 1 for x in leaves):
104
+ raise ValueError("production SharedTrunk must have exact leading P=1")
105
+ return jax.tree_util.tree_map(lambda x: x[0], trunk)
106
+
107
+
108
+ def compile_shared_trunk(model, ctx) -> SharedTrunk:
109
+ edge_feat, local_feat, global_feat = model.featurizer(
110
+ ctx.J_double_prime,
111
+ ctx.mask,
112
+ ctx.h_prime,
113
+ )
114
+ g_seed = tree_sphere(global_feat.astype(local_feat.dtype))
115
+ node_raw, edge_raw, g_seed = model.trunk(
116
+ ctx,
117
+ edge_feat,
118
+ local_feat,
119
+ g_seed,
120
+ )
121
+ global_stream = model._gladder_g_stream(edge_raw, ctx.mask, g_seed)
122
+ return SharedTrunk(
123
+ node_raw=node_raw,
124
+ edge_raw=edge_raw,
125
+ global_raw=global_feat,
126
+ global_stream=global_stream,
127
+ real_mask=ctx.mask,
128
+ balanced_mask=ctx.bmask,
129
+ )
src/hamiltonzero/compiled/types.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import IntEnum
7
+ from typing import Any
8
+
9
+ import equinox as eqx
10
+ import jax
11
+ import jax.numpy as jnp
12
+ from jaxtyping import Array
13
+
14
+
15
+ class MergeOpcode(IntEnum):
16
+ MERGE = 0
17
+ CARRY_LEFT = 1
18
+ CARRY_RIGHT = 2
19
+ EMPTY = 3
20
+
21
+
22
+ MERGE = int(MergeOpcode.MERGE)
23
+ CARRY_LEFT = int(MergeOpcode.CARRY_LEFT)
24
+ CARRY_RIGHT = int(MergeOpcode.CARRY_RIGHT)
25
+ EMPTY = int(MergeOpcode.EMPTY)
26
+ OPCODE_DTYPE = jnp.uint8
27
+
28
+
29
+ class QSideLeafInput(eqx.Module):
30
+ weight: Array
31
+
32
+
33
+ class FactorizedQSide(eqx.Module):
34
+ V: Array
35
+ U: Array
36
+
37
+
38
+ class SharedKernel(eqx.Module):
39
+ q_to_odd: QSideLeafInput
40
+ leaf_factors: tuple[FactorizedQSide, ...]
41
+ leaf_combiner_factors: tuple[FactorizedQSide, ...]
42
+ merge_T: Array
43
+ merge_factors: tuple[FactorizedQSide, ...]
44
+ readout_factors: tuple[FactorizedQSide, ...]
45
+ merge_eps: float = eqx.field(static=True)
46
+
47
+
48
+ class TrunkCompilerKernel(eqx.Module):
49
+ featurizer: Any
50
+ trunk: Any
51
+ shared_global: Any
52
+
53
+
54
+ class LadderProjectionKernel(eqx.Module):
55
+ weight: Array
56
+ bias: Array
57
+ norm_scale: Array
58
+
59
+
60
+ class PhysicalCompilerKernel(eqx.Module):
61
+ contextualizer: Any
62
+ global_fork: Any
63
+ leaf: Any
64
+ merge: Any
65
+ readout: Any
66
+ leaf_projection: LadderProjectionKernel
67
+ tree_pool: Any
68
+ tree_update: Any
69
+ tree_projection_weight: Array
70
+ tree_projection_bias: Array
71
+ root_projection: LadderProjectionKernel
72
+
73
+
74
+ class ModelHamiltonianArrays(eqx.Module):
75
+ coupling: Array
76
+ full_coupling: Array
77
+ field: Array
78
+
79
+
80
+ class GraphInputs(eqx.Module):
81
+ node: Array
82
+ edge: Array
83
+
84
+
85
+ class QuotientInputs(eqx.Module):
86
+ node_key: Array
87
+ edge_key: Array
88
+
89
+
90
+ class EnergyInputs(eqx.Module):
91
+ custom_lap_J_eff: Array
92
+ custom_lap_radial_const: Array
93
+ one_body_fields: tuple[Array, ...]
94
+
95
+
96
+ class EnergyMasks(eqx.Module):
97
+ real: Array
98
+ balanced: Array
99
+
100
+
101
+ class EnergyFrame(eqx.Module):
102
+ custom_lap_J_eff: Array
103
+ w_levels: tuple[Array, ...]
104
+ custom_lap_radial_const: Array
105
+ one_body_fields: tuple[Array, ...]
106
+ masks: EnergyMasks
107
+
108
+
109
+ EnergyFrameBatch = EnergyFrame
110
+
111
+
112
+ class CanonicalHamiltonian(eqx.Module):
113
+ model_coupling_fields: ModelHamiltonianArrays
114
+ node_mask: Array
115
+ balanced_mask: Array
116
+ graph_inputs: GraphInputs
117
+ quotient_inputs: QuotientInputs
118
+ energy_inputs: EnergyInputs
119
+ system_identity: Array
120
+
121
+
122
+ class SharedTrunk(eqx.Module):
123
+ node_raw: Array
124
+ edge_raw: Array
125
+ global_raw: Array
126
+ global_stream: Array
127
+ real_mask: Array
128
+ balanced_mask: Array
129
+
130
+
131
+ class CompiledTree(eqx.Module):
132
+ perm: Array
133
+ inv_perm: Array
134
+ leaf_real: Array
135
+ leaf_h: tuple[Array, ...]
136
+ leaf_combiner_h: tuple[Array, ...]
137
+ merge_h: tuple[Array, ...]
138
+ opcodes: tuple[Array, ...]
139
+ readout_h: tuple[Array, ...]
140
+ readout_combiner_h: tuple[Array, ...]
141
+
142
+
143
+ CompiledTreeBatch = CompiledTree
144
+
145
+
146
+ class CompiledWaveFunction(eqx.Module):
147
+ kernel: SharedKernel
148
+ tree: CompiledTree
149
+
150
+ def __call__(self, q_routed, _ctx=None, _t=0.0):
151
+ from .execute import execute_wavefunction
152
+
153
+ return execute_wavefunction(self.kernel, self.tree, q_routed)
154
+
155
+
156
+ class CompiledWaveFunctions(eqx.Module):
157
+ kernel: SharedKernel
158
+ trees: CompiledTreeBatch
159
+
160
+ def __call__(self, q_routed, _ctx=None, _t=0.0):
161
+ from .execute import execute_wavefunction
162
+
163
+ return execute_wavefunction(self.kernel, self.trees, q_routed)
164
+
165
+
166
+ def level_widths(n_sites: int) -> tuple[int, ...]:
167
+ if n_sites <= 0 or n_sites & (n_sites - 1):
168
+ raise ValueError(f"n_sites must be a positive power of two, got {n_sites}")
169
+ return tuple(n_sites >> level for level in range(1, n_sites.bit_length()))
src/hamiltonzero/config.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import dataclasses
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any, Literal, TypeVar
11
+
12
+
13
+ AttentionImplementation = Literal["tuned", "einsum"]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ModelConfig:
18
+ d_e: int = 512
19
+ d_o: int = 32
20
+ d_c: int = 512
21
+ d_r: int = 32
22
+ n_heads: int = 16
23
+ n_layers: int = 8
24
+ rank: int = 32
25
+ edge_channels: int = 96
26
+ attention_qk_dim: int = 256
27
+ attention_v_dim: int = 256
28
+ merge_dim: int = 1536
29
+ trunk_edge_node_context_dim: int = 256
30
+ trunk_edge_hidden_dim: int = 128
31
+ trunk_attention_bias_hidden_dim: int = 64
32
+ trunk_ffn_hidden_dim: int = 2048
33
+ trunk_two_hop_hidden_dim: int = 128
34
+ tree_edge_node_context_dim: int = 256
35
+ global_dim: int = 256
36
+ merge_hypernet_rank: int = 256
37
+ featurizer_bond_dim: int = 128
38
+ featurizer_heads: int = 8
39
+ featurizer_head_dim: int = 64
40
+ featurizer_global_queries: int = 4
41
+ featurizer_edge_hidden_dim: int = 192
42
+ featurizer_zeeman_hidden_dim: int = 2048
43
+ featurizer_global_hidden_dim: int = 16384
44
+ featurizer_combine_hidden_dim: int = 12288
45
+ featurizer_token_initial_scale: float = 0.02
46
+ polar_group_norm_tau: float = 0.001
47
+ polar_bond_hidden_dim: int = 256
48
+ polar_bond_groups: int = 16
49
+ polar_bond_group_dim: int = 16
50
+ polar_zeeman_groups: int = 16
51
+ polar_zeeman_group_dim: int = 16
52
+ router_max_n: int = 128
53
+ router_model_dim: int = 512
54
+ router_heads: int = 16
55
+ router_attention_dim: int = 128
56
+ router_score_dim: int = 512
57
+ router_candidate_dim: int = 1024
58
+ router_summary_dim: int = 1024
59
+ router_ffn_dim: int = 1024
60
+ router_score_initial_scale: float = 0.0
61
+ router_rope_base: float = 10000.0
62
+ router_rope_scaling: float = 1.0
63
+ router_tree_prefix_layers: int = 4
64
+ router_tree_candidate_layers: int = 2
65
+ router_tree_merge_dim: int = 1024
66
+ router_tree_post_layers: int = 4
67
+ router_context_layers: int = 2
68
+ router_context_heads: int = 4
69
+ router_context_attention_dim: int = 256
70
+ router_context_edge_node_dim: int = 256
71
+ level_edge_heads: int = 8
72
+ level_edge_mlp_dim: int = 384
73
+ level_edge_mlp_blocks: int = 3
74
+ level_edge_ffn_dim: int = 1024
75
+ level_edge_rope_base: float = 10000.0
76
+ level_edge_rope_scaling: float = 1.0
77
+ root_readout_edge_rank: int = 128
78
+ ngpt_alpha_initial: float = 0.25
79
+ ngpt_alpha_initial_fraction: float = 0.25
80
+ ngpt_alpha_maximum: float = 0.8
81
+ global_ladder_tap_dim: int = 256
82
+ level_edge_bias_mlp_dim: int = 128
83
+ level_edge_bias_mlp_blocks: int = 1
84
+ merge_context_mlp_dim: int = 1024
85
+ readout_context_layers: int = 2
86
+ readout_context_heads: int = 8
87
+ readout_context_attention_dim: int = 128
88
+ readout_context_edge_node_dim: int = 256
89
+ readout_context_summary_dim: int = 1024
90
+ readout_context_mlp_dim: int = 2048
91
+ readout_context_bias_dim: int = 32
92
+ readout_context_edge_ffn_dim: int = 384
93
+ readout_context_rope_base: float = 10000.0
94
+ readout_context_rope_scaling: float = 1.0
95
+ two_hop_channels: int = 64
96
+ tree_fwl_channels: int = 128
97
+ attention: AttentionImplementation = "tuned"
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class MCMCConfig:
102
+ batch_size: int = 512
103
+ replicas: int = 8
104
+ steps: int = 32
105
+ burn_in: int = 256
106
+ burn_in_replica_steps: int = 2
107
+ walker_chunk_size: int | None = None
108
+ initial_sigma: float = 0.3
109
+ initial_haar_sites: int = 1
110
+ sigma_scale: float = 1.1
111
+ langevin_target_acceptance: float = 0.574
112
+ haar_target_acceptance: float = 0.234
113
+ beta_history_weight: float = 0.9
114
+ adapt_every: int = 1
115
+ reuse_mcmc: Path | None = None
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class KFACConfig:
120
+ learning_rate_numerator: float = 0.05
121
+ learning_rate_offset: float = 5.0
122
+ learning_rate_decay_steps: float = 5000.0
123
+ curvature_ema: float = 0.995
124
+ curvature_update_period: int = 2
125
+ inverse_update_period: int = 2
126
+ damping: float = 0.001
127
+ minimum_damping: float = 0.0001
128
+ norm_constraint: float = 0.001
129
+ mad_clip_width: float = 5.0
130
+ momentum: float = 0.0
131
+ l2_regularization: float = 0.0
132
+
133
+
134
+ @dataclass(frozen=True, slots=True)
135
+ class RouterConfig:
136
+ temperature: float = 1.0
137
+ loss_weight: float = 1.0
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class EnergyConfig:
142
+ mu: float | None = None
143
+ eps: float = 0.1
144
+ chunk_size: int = 512
145
+
146
+
147
+ @dataclass(frozen=True, slots=True)
148
+ class TrainConfig:
149
+ systems: Path
150
+ output: Path
151
+ steps: int
152
+ seed: int = 777
153
+ n_max: int = 64
154
+ model: ModelConfig = field(default_factory=ModelConfig)
155
+ router: RouterConfig = field(default_factory=RouterConfig)
156
+ mcmc: MCMCConfig = field(default_factory=MCMCConfig)
157
+ kfac: KFACConfig = field(default_factory=KFACConfig)
158
+ energy: EnergyConfig = field(default_factory=EnergyConfig)
159
+
160
+
161
+ def _finetune_mcmc() -> MCMCConfig:
162
+ return MCMCConfig(batch_size=256, replicas=8, steps=2, burn_in=256)
163
+
164
+
165
+ def _finetune_kfac() -> KFACConfig:
166
+ return KFACConfig(
167
+ learning_rate_numerator=0.002,
168
+ learning_rate_offset=1.0,
169
+ learning_rate_decay_steps=10000.0,
170
+ curvature_ema=0.99,
171
+ curvature_update_period=2,
172
+ inverse_update_period=4,
173
+ damping=0.001,
174
+ )
175
+
176
+
177
+ def _finetune_energy() -> EnergyConfig:
178
+ return EnergyConfig(mu=2.86)
179
+
180
+
181
+ @dataclass(frozen=True, slots=True)
182
+ class FineTuneConfig:
183
+ system: Path
184
+ checkpoint: Path
185
+ output: Path
186
+ steps: int = 10000
187
+ seed: int = 777
188
+ leaf_rank: int = 1536
189
+ merge_rank: int = 1024
190
+ route_temperature: float = 1.0
191
+ model: ModelConfig = field(default_factory=lambda: ModelConfig(attention="einsum"))
192
+ mcmc: MCMCConfig = field(default_factory=_finetune_mcmc)
193
+ kfac: KFACConfig = field(default_factory=_finetune_kfac)
194
+ energy: EnergyConfig = field(default_factory=_finetune_energy)
195
+
196
+
197
+ def _eval_mcmc() -> EvalMCMCConfig:
198
+ return EvalMCMCConfig(
199
+ batch_size=256,
200
+ replicas=8,
201
+ steps=24,
202
+ burn_in=1024,
203
+ walker_chunk_size=16,
204
+ )
205
+
206
+
207
+ @dataclass(frozen=True, slots=True)
208
+ class EvalMCMCConfig:
209
+ batch_size: int = 256
210
+ replicas: int = 8
211
+ steps: int = 24
212
+ burn_in: int = 1024
213
+ burn_in_replica_steps: int = 2
214
+ walker_chunk_size: int = 16
215
+ initial_sigma: float = 0.3
216
+ initial_haar_sites: int = 1
217
+ sigma_scale: float = 1.1
218
+ langevin_target_acceptance: float = 0.574
219
+ haar_target_acceptance: float = 0.234
220
+ beta_history_weight: float = 0.9
221
+
222
+
223
+ @dataclass(frozen=True, slots=True)
224
+ class EvalConfig:
225
+ system: Path
226
+ checkpoint: Path
227
+ output: Path
228
+ seed: int = 777
229
+ contest: bool = False
230
+ large_n: bool = False
231
+ measurements: int = 256
232
+ contest_candidates: int = 8
233
+ contest_beam_width: int = 8
234
+ contest_preburn: int = 128
235
+ contest_measurements: int = 128
236
+ contest_se_multiplier: float = 2.0
237
+ route_temperature: float = 4.0
238
+ large_n_sequence_shards: int = 0
239
+ large_n_pair_tile_size: int = 128
240
+ contextualizer_attention: AttentionImplementation | None = None
241
+ model: ModelConfig = field(default_factory=ModelConfig)
242
+ mcmc: EvalMCMCConfig = field(default_factory=_eval_mcmc)
243
+ energy: EnergyConfig = field(default_factory=EnergyConfig)
244
+
245
+ def __post_init__(self) -> None:
246
+ if self.contest and self.large_n:
247
+ raise ValueError("contest and large_n are mutually exclusive")
248
+
249
+
250
+ Config = TrainConfig | FineTuneConfig | EvalConfig
251
+ T = TypeVar("T")
252
+
253
+
254
+ def _coerce(cls: type[T], values: dict[str, Any]) -> T:
255
+ nested = {
256
+ "model": ModelConfig,
257
+ "router": RouterConfig,
258
+ "mcmc": MCMCConfig,
259
+ "kfac": KFACConfig,
260
+ "energy": EnergyConfig,
261
+ }
262
+ if cls is EvalConfig:
263
+ nested["mcmc"] = EvalMCMCConfig
264
+ data = dict(values)
265
+ fields_by_name = {item.name: item for item in dataclasses.fields(cls)}
266
+ for name, nested_cls in nested.items():
267
+ if name in data and isinstance(data[name], dict):
268
+ item = fields_by_name.get(name)
269
+ defaults: dict[str, Any] = {}
270
+ if item is not None and item.default_factory is not dataclasses.MISSING:
271
+ defaults = dataclasses.asdict(item.default_factory())
272
+ nested_values = {**defaults, **data[name]}
273
+ if name == "mcmc" and nested_values.get("reuse_mcmc") is not None:
274
+ nested_values["reuse_mcmc"] = Path(nested_values["reuse_mcmc"])
275
+ data[name] = nested_cls(**nested_values)
276
+ path_fields = {"systems", "system", "checkpoint", "output"}
277
+ for item in dataclasses.fields(cls):
278
+ if item.name in path_fields and item.name in data:
279
+ data[item.name] = Path(data[item.name])
280
+ return cls(**data)
281
+
282
+
283
+ def load_config(path: str | Path, mode: Literal["train", "finetune", "eval"]) -> Config:
284
+ values = json.loads(Path(path).read_text())
285
+ cls = {"train": TrainConfig, "finetune": FineTuneConfig, "eval": EvalConfig}[mode]
286
+ return _coerce(cls, values)
287
+
288
+
289
+ __all__ = [
290
+ "AttentionImplementation",
291
+ "EnergyConfig",
292
+ "EvalConfig",
293
+ "EvalMCMCConfig",
294
+ "FineTuneConfig",
295
+ "KFACConfig",
296
+ "MCMCConfig",
297
+ "ModelConfig",
298
+ "RouterConfig",
299
+ "TrainConfig",
300
+ "load_config",
301
+ ]
src/hamiltonzero/data/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .systems import (
5
+ build_context,
6
+ build_context_and_energy,
7
+ build_multi_context,
8
+ load_system,
9
+ load_systems,
10
+ padded_model_arrays,
11
+ save_system,
12
+ )
13
+
14
+ __all__ = [
15
+ "build_context",
16
+ "build_context_and_energy",
17
+ "build_multi_context",
18
+ "load_system",
19
+ "load_systems",
20
+ "padded_model_arrays",
21
+ "save_system",
22
+ ]
src/hamiltonzero/data/systems.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from dataclasses import replace
8
+ from pathlib import Path
9
+ from typing import Any, Iterable
10
+
11
+ import numpy as np
12
+
13
+ from hamiltonzero.hamiltonian import SpinHamiltonian, _exchange_matrix
14
+
15
+
16
+ def _load_payload(path: str | Path) -> dict[str, Any]:
17
+ source = Path(path)
18
+ if source.suffix == ".jsonl":
19
+ records = []
20
+ with source.open(encoding="utf-8") as stream:
21
+ for line_number, line in enumerate(stream, 1):
22
+ if not line.strip():
23
+ continue
24
+ record = json.loads(line)
25
+ if not isinstance(record, dict):
26
+ raise ValueError(f"JSONL record {line_number} must be an object")
27
+ records.append(record)
28
+ return {"systems": records}
29
+ payload = json.loads(source.read_text())
30
+ if not isinstance(payload, dict):
31
+ raise ValueError("Hamiltonian dataset must be a JSON object")
32
+ return payload
33
+
34
+
35
+ def _from_sparse(spec: dict[str, Any]) -> SpinHamiltonian:
36
+ n_sites = int(spec.get("n_sites", spec.get("n_spins", 0)))
37
+ if n_sites <= 0:
38
+ raise ValueError("sparse Hamiltonian requires a positive n_sites")
39
+ exchange = np.zeros((n_sites, n_sites, 3, 3), dtype=np.float32)
40
+ coupling = np.zeros((n_sites, n_sites), dtype=np.float32)
41
+ seen: set[tuple[int, int]] = set()
42
+ for offset, term in enumerate(spec["exchange"]):
43
+ if not isinstance(term, list) or len(term) != 3:
44
+ raise ValueError(f"exchange term {offset} must be [i,j,J]")
45
+ left, right, value = term
46
+ if (
47
+ not isinstance(left, int)
48
+ or isinstance(left, bool)
49
+ or not isinstance(right, int)
50
+ or isinstance(right, bool)
51
+ or not 0 <= left < right < n_sites
52
+ ):
53
+ raise ValueError(
54
+ f"exchange term {offset} must satisfy 0 <= i < j < n_sites"
55
+ )
56
+ pair = (left, right)
57
+ if pair in seen:
58
+ raise ValueError(f"duplicate exchange term for sites {pair}")
59
+ seen.add(pair)
60
+ matrix = _exchange_matrix(value)
61
+ exchange[left, right] = matrix
62
+ exchange[right, left] = matrix.T
63
+ coupling[left, right] = coupling[right, left] = 1.0
64
+ field = spec.get("field", spec.get("h", spec.get("h_field", 0.0)))
65
+ return SpinHamiltonian.from_arrays(
66
+ exchange,
67
+ field,
68
+ coupling=coupling,
69
+ nodes=spec.get("nodes"),
70
+ mu=spec.get("mu"),
71
+ )
72
+
73
+
74
+ def _next_power_of_two(value: int) -> int:
75
+ return 1 if value <= 1 else 1 << (value - 1).bit_length()
76
+
77
+
78
+ def _from_record(
79
+ record: dict[str, Any],
80
+ *,
81
+ needs_fwl2: bool | None = None,
82
+ ) -> SpinHamiltonian:
83
+ outer = record
84
+ spec = record.get("spec", record)
85
+ convention = spec.get("convention", "textbook")
86
+ if convention != "textbook":
87
+ raise ValueError("public Hamiltonian JSON must use convention='textbook'")
88
+ if "exchange" in spec:
89
+ system = _from_sparse(spec)
90
+ else:
91
+ system = SpinHamiltonian.from_arrays(
92
+ spec["J"],
93
+ spec.get("h", spec.get("h_field", 0.0)),
94
+ coupling=spec.get("coupling"),
95
+ nodes=spec.get("nodes"),
96
+ mu=spec.get("mu"),
97
+ )
98
+ if needs_fwl2 is None:
99
+ needs_fwl2 = outer.get("needs_fwl2", spec.get("needs_fwl2"))
100
+ metadata = {
101
+ name: outer.get(name, spec.get(name))
102
+ for name in ("category", "tag", "topology_class", "j_class")
103
+ }
104
+ return replace(
105
+ system,
106
+ _needs_fwl2=needs_fwl2,
107
+ _category=metadata["category"],
108
+ _tag=metadata["tag"],
109
+ _topology_class=metadata["topology_class"],
110
+ _j_class=metadata["j_class"],
111
+ )
112
+
113
+
114
+ def load_system(path: str | Path) -> SpinHamiltonian:
115
+ payload = _load_payload(path)
116
+ if "systems" in payload:
117
+ systems = payload["systems"]
118
+ if len(systems) != 1:
119
+ raise ValueError("load_system requires exactly one system")
120
+ dispatch = payload.get("needs_fwl2", payload.get("dataset_needs_fwl2"))
121
+ if dispatch is not None:
122
+ if not isinstance(dispatch, list) or len(dispatch) != 1:
123
+ raise ValueError("needs_fwl2 sidecar must align with systems")
124
+ return _from_record(systems[0], needs_fwl2=bool(dispatch[0]))
125
+ return _from_record(systems[0])
126
+ return _from_record(payload)
127
+
128
+
129
+ def load_systems(path: str | Path) -> list[SpinHamiltonian]:
130
+ payload = _load_payload(path)
131
+ records = payload.get("systems", [payload])
132
+ dispatch = (
133
+ payload.get("needs_fwl2", payload.get("dataset_needs_fwl2"))
134
+ if "systems" in payload
135
+ else None
136
+ )
137
+ if dispatch is None and isinstance(payload.get("per_system"), list):
138
+ derived = payload["per_system"]
139
+ if len(derived) == len(records) and all(
140
+ isinstance(value, dict) and "needs_fwl2" in value for value in derived
141
+ ):
142
+ dispatch = [value["needs_fwl2"] for value in derived]
143
+ if dispatch is not None:
144
+ if not isinstance(dispatch, list) or len(dispatch) != len(records):
145
+ raise ValueError("needs_fwl2 sidecar must align with systems")
146
+ return [
147
+ _from_record(record, needs_fwl2=bool(value))
148
+ for record, value in zip(records, dispatch, strict=True)
149
+ ]
150
+ return [_from_record(record) for record in records]
151
+
152
+
153
+ def save_system(path: str | Path, system: SpinHamiltonian) -> None:
154
+ destination = Path(path)
155
+ destination.parent.mkdir(parents=True, exist_ok=True)
156
+ destination.write_text(json.dumps(system.to_dict(), indent=2) + "\n")
157
+
158
+
159
+ def padded_model_arrays(
160
+ system: SpinHamiltonian,
161
+ n_max: int | None = None,
162
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
163
+ width = _next_power_of_two(system.n_spins) if n_max is None else int(n_max)
164
+ if width < system.n_spins:
165
+ raise ValueError("n_max cannot be smaller than the system")
166
+ if width <= 0 or width & (width - 1):
167
+ raise ValueError("n_max must be a positive power of two")
168
+ coupling, exchange, field = system.model_arrays()
169
+ padding = width - system.n_spins
170
+ coupling = np.pad(coupling, ((0, padding), (0, padding)))
171
+ exchange = np.pad(exchange, ((0, padding), (0, padding), (0, 0), (0, 0)))
172
+ field = np.pad(field, ((0, padding), (0, 0)))
173
+ mask = np.zeros((width,), dtype=np.int32)
174
+ mask[: system.n_spins] = 1
175
+ return coupling, exchange, field, mask
176
+
177
+
178
+ def _context_arrays(system: SpinHamiltonian, n_max: int | None):
179
+ import jax.numpy as jnp
180
+
181
+ from hamiltonzero.model.route_quotient import system_needs_fwl2
182
+
183
+ _coupling, exchange, field, mask = padded_model_arrays(system, n_max)
184
+ needs_fwl2 = system._needs_fwl2
185
+ if needs_fwl2 is None:
186
+ _, physical_exchange, physical_field = system.model_arrays()
187
+ needs_fwl2 = system_needs_fwl2(
188
+ physical_exchange,
189
+ physical_field,
190
+ system.n_spins,
191
+ category=system._category,
192
+ tag=system._tag,
193
+ topology_class=system._topology_class,
194
+ j_class=system._j_class,
195
+ )
196
+ return (
197
+ jnp.asarray(exchange),
198
+ jnp.asarray(field),
199
+ jnp.asarray(mask),
200
+ needs_fwl2,
201
+ )
202
+
203
+
204
+ def build_context(
205
+ system: SpinHamiltonian,
206
+ n_max: int | None = None,
207
+ ):
208
+ from hamiltonzero.model import SpinContext
209
+
210
+ exchange, field, mask, needs_fwl2 = _context_arrays(system, n_max)
211
+ return SpinContext(
212
+ J_full=exchange,
213
+ h=field,
214
+ mask=mask,
215
+ needs_fwl2=needs_fwl2,
216
+ )
217
+
218
+
219
+ def build_context_and_energy(
220
+ system: SpinHamiltonian,
221
+ n_max: int | None = None,
222
+ mu: float | None = None,
223
+ eps: float = 0.1,
224
+ ):
225
+ from hamiltonzero.energy.frame import build_energy_inputs
226
+ from hamiltonzero.model import SpinContext
227
+
228
+ exchange, field, mask, needs_fwl2 = _context_arrays(system, n_max)
229
+ context = SpinContext(
230
+ J_full=exchange,
231
+ h=field,
232
+ mask=mask,
233
+ needs_fwl2=needs_fwl2,
234
+ )
235
+ energy_inputs = build_energy_inputs(
236
+ exchange,
237
+ field,
238
+ mask,
239
+ system.mu if mu is None else mu,
240
+ eps,
241
+ )
242
+ return context, energy_inputs
243
+
244
+
245
+ def build_multi_context(
246
+ systems: Iterable[SpinHamiltonian],
247
+ n_max: int,
248
+ ):
249
+ from hamiltonzero.model import MultiSystemContext
250
+
251
+ system_list = list(systems)
252
+ contexts = [build_context(system, n_max=n_max) for system in system_list]
253
+ return MultiSystemContext.stack(contexts)
254
+
255
+
256
+ __all__ = [
257
+ "build_context",
258
+ "build_context_and_energy",
259
+ "build_multi_context",
260
+ "load_system",
261
+ "load_systems",
262
+ "padded_model_arrays",
263
+ "save_system",
264
+ ]
src/hamiltonzero/energy/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from hamiltonzero.model._custom_lap_primitives import (
7
+ custom_lap_active,
8
+ quadrilinear_merge_p,
9
+ )
10
+
11
+ from .compiled import (
12
+ vmc_energy_custom_lap_compiled,
13
+ vmc_energy_custom_lap_finetune,
14
+ )
15
+
16
+
17
+ __all__ = [
18
+ "custom_lap_active",
19
+ "quadrilinear_merge_p",
20
+ "vmc_energy_custom_lap_compiled",
21
+ "vmc_energy_custom_lap_finetune",
22
+ ]
src/hamiltonzero/energy/compiled.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ import jax
10
+
11
+ from hamiltonzero.energy.kernel import (
12
+ _vmc_energy_custom_lap_finetune,
13
+ _vmc_energy_custom_lap_prebuilt,
14
+ )
15
+
16
+
17
+ def vmc_energy_custom_lap_compiled(
18
+ kernel: Any,
19
+ tree: Any,
20
+ energy_frame: Any,
21
+ q_routed: jax.Array,
22
+ *,
23
+ chunk_size: int | None = 512,
24
+ ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
25
+ return _vmc_energy_custom_lap_prebuilt(
26
+ kernel,
27
+ tree,
28
+ energy_frame,
29
+ q_routed,
30
+ chunk_size=chunk_size,
31
+ )
32
+
33
+
34
+ def vmc_energy_custom_lap_finetune(
35
+ model: Any,
36
+ energy_frame: Any,
37
+ q_routed: jax.Array,
38
+ *,
39
+ chunk_size: int | None = 512,
40
+ ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
41
+ from hamiltonzero.compiled.model import CompiledFinetuneWaveFunction
42
+
43
+ if not isinstance(model, CompiledFinetuneWaveFunction):
44
+ raise TypeError("fine-tune energy requires CompiledFinetuneWaveFunction")
45
+ return _vmc_energy_custom_lap_finetune(
46
+ model,
47
+ energy_frame,
48
+ q_routed,
49
+ 0.0,
50
+ chunk_size=chunk_size,
51
+ )
52
+
53
+
54
+ __all__ = [
55
+ "vmc_energy_custom_lap_compiled",
56
+ "vmc_energy_custom_lap_finetune",
57
+ ]
src/hamiltonzero/energy/custom_lap.py ADDED
@@ -0,0 +1,1491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Callable, NamedTuple
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ from jax import lax
12
+ from jax.extend import core
13
+ from jax.extend.core import Literal
14
+
15
+
16
+ class JLP(NamedTuple):
17
+ value: Any
18
+ jac: Any
19
+ lap: Any
20
+ level: int
21
+ has_chunk_axis: bool
22
+ chunk_idx: int = -1
23
+
24
+
25
+ def _is_jlp(x) -> bool:
26
+ return isinstance(x, JLP)
27
+
28
+
29
+ from hamiltonzero.model._custom_lap_primitives import (
30
+ custom_lap_active,
31
+ enter_custom_lap,
32
+ restore_custom_lap,
33
+ quadrilinear_merge_p,
34
+ )
35
+
36
+
37
+ class use_custom_lap:
38
+ def __enter__(self):
39
+ self._cla_token = enter_custom_lap()
40
+ return self
41
+
42
+ def __exit__(self, *exc):
43
+ restore_custom_lap(self._cla_token)
44
+
45
+
46
+ def build_W_levels(W_full, N: int) -> list:
47
+
48
+ assert W_full.shape == (3 * N, 3 * N), (
49
+ f"W_full must be [3N, 3N]; got {W_full.shape}"
50
+ )
51
+ assert (N & (N - 1)) == 0, f"N must be a power of 2; got {N}"
52
+ levels = []
53
+ k = 1
54
+ while k <= N:
55
+ n_chunks = N // k
56
+ W_reshaped = W_full.reshape(n_chunks, 3 * k, n_chunks, 3 * k)
57
+ idx = jnp.arange(n_chunks)
58
+ W_k = W_reshaped[idx, :, idx, :]
59
+ levels.append(W_k)
60
+ k *= 2
61
+ return levels
62
+
63
+
64
+ def _level_idx(k: int) -> int:
65
+
66
+ assert k > 0 and (k & (k - 1)) == 0, f"k must be a power of 2; got {k}"
67
+ return k.bit_length() - 1
68
+
69
+
70
+ def _W_at_level(W_levels, k: int):
71
+ return W_levels[_level_idx(k)]
72
+
73
+
74
+ _RULE_REGISTRY: dict[core.Primitive, Callable] = {}
75
+
76
+
77
+ def _params_except(params, *drop, **defaults):
78
+
79
+ out = {k: params[k] for k in params if k not in drop}
80
+ for k, v in defaults.items():
81
+ out.setdefault(k, v)
82
+ return out
83
+
84
+
85
+ def _shape_bind_params(params, *drop):
86
+
87
+ out = {k: params[k] for k in params if k not in drop and k != "out_sharding"}
88
+ out.setdefault("sharding", params.get("out_sharding", None))
89
+ return out
90
+
91
+
92
+ def _select_W_for_jlp(
93
+ level: int, chunk_idx: int, has_chunk_axis: bool, W_levels, M_jac: int
94
+ ):
95
+
96
+ W_k = _W_at_level(W_levels, level)
97
+ if has_chunk_axis:
98
+ assert W_k.shape[0] == M_jac, (
99
+ f"has_chunk_axis: W_k chunks {W_k.shape[0]} must equal jac M {M_jac}"
100
+ )
101
+ return W_k
102
+ if chunk_idx >= 0:
103
+ assert M_jac == 1
104
+ return W_k[chunk_idx : chunk_idx + 1]
105
+
106
+ assert W_k.shape[0] == M_jac, (
107
+ f"multi-chunk: W_k chunks {W_k.shape[0]} must equal jac M {M_jac}"
108
+ )
109
+ return W_k
110
+
111
+
112
+ def _jac_self_quad_form(
113
+ jac, level: int, chunk_idx: int, has_chunk_axis: bool, W_levels
114
+ ):
115
+
116
+ M = jac.shape[1]
117
+ W_used = _select_W_for_jlp(level, chunk_idx, has_chunk_axis, W_levels, M)
118
+ n_trailing = jac.ndim - 2
119
+ if n_trailing == 0:
120
+ out = jnp.einsum("mc,cmn,nc->c", jac, W_used, jac)
121
+ elif n_trailing == 1:
122
+ out = jnp.einsum("mca,cmn,nca->ca", jac, W_used, jac)
123
+ elif n_trailing == 2:
124
+ out = jnp.einsum("mcab,cmn,ncab->cab", jac, W_used, jac)
125
+ elif n_trailing == 3:
126
+ out = jnp.einsum("mcabd,cmn,ncabd->cabd", jac, W_used, jac)
127
+ else:
128
+ raise NotImplementedError(
129
+ f"_jac_self_quad_form: trailing rank {n_trailing} not supported"
130
+ )
131
+ if not has_chunk_axis:
132
+ out = out.sum(axis=0)
133
+ return out
134
+
135
+
136
+ def _make_unary_rule(prim, f_prime_fn, f_dprime_fn):
137
+
138
+ def rule(invals, params, W_levels):
139
+ [x] = invals
140
+ assert _is_jlp(x)
141
+ v_out = prim.bind(x.value, **params)
142
+ fp = f_prime_fn(x.value)
143
+ jac_out = fp * x.jac
144
+ fpp = f_dprime_fn(x.value)
145
+ cross = _jac_self_quad_form(
146
+ x.jac, x.level, x.chunk_idx, x.has_chunk_axis, W_levels
147
+ )
148
+ lap_out = fp * x.lap + fpp * cross
149
+ return JLP(
150
+ value=v_out,
151
+ jac=jac_out,
152
+ lap=lap_out,
153
+ level=x.level,
154
+ has_chunk_axis=x.has_chunk_axis,
155
+ chunk_idx=x.chunk_idx,
156
+ )
157
+
158
+ _RULE_REGISTRY[prim] = rule
159
+
160
+
161
+ _make_unary_rule(lax.sin_p, jnp.cos, lambda x: -jnp.sin(x))
162
+ _make_unary_rule(lax.cos_p, lambda x: -jnp.sin(x), lambda x: -jnp.cos(x))
163
+ _make_unary_rule(
164
+ lax.tanh_p,
165
+ lambda x: 1.0 - jnp.tanh(x) ** 2,
166
+ lambda x: -2.0 * jnp.tanh(x) * (1.0 - jnp.tanh(x) ** 2),
167
+ )
168
+ _make_unary_rule(lax.exp_p, jnp.exp, jnp.exp)
169
+ _make_unary_rule(lax.log_p, lambda x: 1.0 / x, lambda x: -1.0 / (x * x))
170
+ _make_unary_rule(lax.neg_p, lambda x: -jnp.ones_like(x), lambda x: jnp.zeros_like(x))
171
+ _make_unary_rule(lax.abs_p, lambda x: jnp.sign(x), lambda x: jnp.zeros_like(x))
172
+ _make_unary_rule(
173
+ lax.sqrt_p, lambda x: 0.5 / jnp.sqrt(x), lambda x: -0.25 / (x * jnp.sqrt(x))
174
+ )
175
+ _make_unary_rule(
176
+ lax.rsqrt_p,
177
+ lambda x: -0.5 / (x * jnp.sqrt(x)),
178
+ lambda x: 0.75 / (x * x * jnp.sqrt(x)),
179
+ )
180
+
181
+
182
+ def _logistic_prime(x):
183
+ s = jax.nn.sigmoid(x)
184
+ return s * (1.0 - s)
185
+
186
+
187
+ def _logistic_dprime(x):
188
+ s = jax.nn.sigmoid(x)
189
+ return s * (1.0 - s) * (1.0 - 2.0 * s)
190
+
191
+
192
+ _logistic_p = lax.logistic_p
193
+ _make_unary_rule(_logistic_p, _logistic_prime, _logistic_dprime)
194
+
195
+
196
+ def _integer_pow_rule(invals, params, W_levels):
197
+ [x] = invals
198
+ y = params["y"]
199
+ assert _is_jlp(x)
200
+ v_out = lax.integer_pow_p.bind(x.value, **params)
201
+ if y == 0:
202
+ return JLP(
203
+ value=jnp.ones_like(x.value),
204
+ jac=jnp.zeros_like(x.jac),
205
+ lap=jnp.zeros_like(x.lap),
206
+ level=x.level,
207
+ has_chunk_axis=x.has_chunk_axis,
208
+ chunk_idx=x.chunk_idx,
209
+ )
210
+ if y == 1:
211
+ return x
212
+ fp = float(y) * lax.integer_pow_p.bind(x.value, y=y - 1)
213
+ jac_out = fp * x.jac
214
+ fpp = float(y * (y - 1)) * lax.integer_pow_p.bind(x.value, y=max(y - 2, 0))
215
+ cross = _jac_self_quad_form(x.jac, x.level, x.chunk_idx, x.has_chunk_axis, W_levels)
216
+ lap_out = fp * x.lap + fpp * cross
217
+ return JLP(
218
+ value=v_out,
219
+ jac=jac_out,
220
+ lap=lap_out,
221
+ level=x.level,
222
+ has_chunk_axis=x.has_chunk_axis,
223
+ chunk_idx=x.chunk_idx,
224
+ )
225
+
226
+
227
+ _RULE_REGISTRY[lax.integer_pow_p] = _integer_pow_rule
228
+
229
+
230
+ def _convert_rule(invals, params, W_levels):
231
+ [x] = invals
232
+ assert _is_jlp(x)
233
+ new_dtype = params["new_dtype"]
234
+ return JLP(
235
+ value=x.value.astype(new_dtype),
236
+ jac=x.jac.astype(new_dtype),
237
+ lap=x.lap.astype(new_dtype),
238
+ level=x.level,
239
+ has_chunk_axis=x.has_chunk_axis,
240
+ chunk_idx=x.chunk_idx,
241
+ )
242
+
243
+
244
+ _RULE_REGISTRY[lax.convert_element_type_p] = _convert_rule
245
+
246
+
247
+ def _broadcast_jac_lap_for_op(x, target_shape):
248
+
249
+ lap_out = jnp.broadcast_to(x.lap, target_shape)
250
+ jac_out = _broadcast_jac_to_value_shape(x.jac, target_shape, x.has_chunk_axis)
251
+ return jac_out, lap_out
252
+
253
+
254
+ def _broadcast_jac_to_value_shape(jac, target_value_shape, has_chunk_axis: bool):
255
+
256
+ if has_chunk_axis:
257
+ new_shape = (jac.shape[0], target_value_shape[0]) + tuple(
258
+ target_value_shape[1:]
259
+ )
260
+ else:
261
+ new_shape = (jac.shape[0], jac.shape[1]) + tuple(target_value_shape)
262
+ while jac.ndim < len(new_shape):
263
+ jac = jnp.expand_dims(jac, axis=jac.ndim)
264
+ return jnp.broadcast_to(jac, new_shape)
265
+
266
+
267
+ def _promote_jlp_one_level(x: JLP) -> JLP:
268
+
269
+ k = x.level
270
+ new_k = 2 * k
271
+ if x.has_chunk_axis:
272
+ assert x.chunk_idx in (0, 1), (
273
+ f"_promote_jlp_one_level (chunked): chunk_idx must be 0 or 1; got {x}"
274
+ )
275
+ is_left = x.chunk_idx == 0
276
+ zero_shape = (3 * k,) + x.jac.shape[1:]
277
+ zeros = jnp.zeros(zero_shape, dtype=x.jac.dtype)
278
+ if is_left:
279
+ new_jac = jnp.concatenate([x.jac, zeros], axis=0)
280
+ else:
281
+ new_jac = jnp.concatenate([zeros, x.jac], axis=0)
282
+ return JLP(
283
+ value=x.value,
284
+ jac=new_jac,
285
+ lap=x.lap,
286
+ level=new_k,
287
+ has_chunk_axis=True,
288
+ chunk_idx=-1,
289
+ )
290
+
291
+ assert x.chunk_idx >= 0, (
292
+ f"_promote_jlp_one_level: per-node requires chunk_idx>=0; got {x}"
293
+ )
294
+ is_left = x.chunk_idx % 2 == 0
295
+ new_chunk_idx = x.chunk_idx // 2
296
+ zero_shape = (3 * k,) + x.jac.shape[1:]
297
+ zeros = jnp.zeros(zero_shape, dtype=x.jac.dtype)
298
+ if is_left:
299
+ new_jac = jnp.concatenate([x.jac, zeros], axis=0)
300
+ else:
301
+ new_jac = jnp.concatenate([zeros, x.jac], axis=0)
302
+ return JLP(
303
+ value=x.value,
304
+ jac=new_jac,
305
+ lap=x.lap,
306
+ level=new_k,
307
+ has_chunk_axis=False,
308
+ chunk_idx=new_chunk_idx,
309
+ )
310
+
311
+
312
+ def _align_jlp_levels(a: JLP, b: JLP):
313
+
314
+ while a.level < b.level:
315
+ a = _promote_jlp_one_level(a)
316
+ while b.level < a.level:
317
+ b = _promote_jlp_one_level(b)
318
+ assert a.has_chunk_axis == b.has_chunk_axis, (
319
+ "_align_jlp_levels: chunk-axis mismatch"
320
+ )
321
+ if a.has_chunk_axis:
322
+ if (
323
+ a.chunk_idx in (0, 1)
324
+ and b.chunk_idx in (0, 1)
325
+ and a.chunk_idx != b.chunk_idx
326
+ ):
327
+ a = _promote_jlp_one_level(a)
328
+ b = _promote_jlp_one_level(b)
329
+ return a, b
330
+
331
+ while a.chunk_idx != b.chunk_idx:
332
+ a = _promote_jlp_one_level(a)
333
+ b = _promote_jlp_one_level(b)
334
+ return a, b
335
+
336
+
337
+ def _add_or_sub_rule(sign: float):
338
+ def rule(invals, params, W_levels):
339
+ a, b = invals
340
+ if _is_jlp(a) and _is_jlp(b):
341
+ need_promote = (
342
+ (a.level != b.level)
343
+ or (
344
+ not a.has_chunk_axis
345
+ and not b.has_chunk_axis
346
+ and a.chunk_idx != b.chunk_idx
347
+ )
348
+ or (
349
+ a.has_chunk_axis
350
+ and b.has_chunk_axis
351
+ and a.chunk_idx in (0, 1)
352
+ and b.chunk_idx in (0, 1)
353
+ and a.chunk_idx != b.chunk_idx
354
+ )
355
+ )
356
+ if need_promote:
357
+ a, b = _align_jlp_levels(a, b)
358
+ assert a.has_chunk_axis == b.has_chunk_axis, "add/sub: chunk-axis mismatch"
359
+ v_out = a.value + sign * b.value
360
+ a_jac_b = _broadcast_jac_to_value_shape(
361
+ a.jac, v_out.shape, a.has_chunk_axis
362
+ )
363
+ b_jac_b = _broadcast_jac_to_value_shape(
364
+ b.jac, v_out.shape, b.has_chunk_axis
365
+ )
366
+ jac_out = a_jac_b + sign * b_jac_b
367
+ a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
368
+ b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
369
+ lap_out = a_lap_b + sign * b_lap_b
370
+ return JLP(
371
+ value=v_out,
372
+ jac=jac_out,
373
+ lap=lap_out,
374
+ level=a.level,
375
+ has_chunk_axis=a.has_chunk_axis,
376
+ chunk_idx=a.chunk_idx,
377
+ )
378
+ if _is_jlp(a):
379
+ v_out = a.value + sign * b
380
+ jac_out, lap_out = _broadcast_jac_lap_for_op(a, v_out.shape)
381
+ return JLP(
382
+ value=v_out,
383
+ jac=jac_out,
384
+ lap=lap_out,
385
+ level=a.level,
386
+ has_chunk_axis=a.has_chunk_axis,
387
+ chunk_idx=a.chunk_idx,
388
+ )
389
+
390
+ v_out = a + sign * b.value
391
+ jac_out, lap_out = _broadcast_jac_lap_for_op(b, v_out.shape)
392
+ return JLP(
393
+ value=v_out,
394
+ jac=sign * jac_out,
395
+ lap=sign * lap_out,
396
+ level=b.level,
397
+ has_chunk_axis=b.has_chunk_axis,
398
+ chunk_idx=b.chunk_idx,
399
+ )
400
+
401
+ return rule
402
+
403
+
404
+ _RULE_REGISTRY[lax.add_p] = _add_or_sub_rule(+1.0)
405
+ _RULE_REGISTRY[lax.sub_p] = _add_or_sub_rule(-1.0)
406
+
407
+
408
+ def _mul_rule(invals, params, W_levels):
409
+ a, b = invals
410
+ if _is_jlp(a) and _is_jlp(b):
411
+ need_promote = (a.level != b.level) or (
412
+ not a.has_chunk_axis and not b.has_chunk_axis and a.chunk_idx != b.chunk_idx
413
+ )
414
+ if need_promote:
415
+ a, b = _align_jlp_levels(a, b)
416
+ assert a.has_chunk_axis == b.has_chunk_axis
417
+
418
+ v_out = a.value * b.value
419
+ a_jac_b = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
420
+ b_jac_b = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
421
+ a_val_b = jnp.broadcast_to(a.value, v_out.shape)
422
+ b_val_b = jnp.broadcast_to(b.value, v_out.shape)
423
+ jac_out = a_val_b * b_jac_b + b_val_b * a_jac_b
424
+ a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
425
+ b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
426
+ cross = _jac_cross_quad_form(
427
+ a_jac_b, b_jac_b, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
428
+ )
429
+ lap_out = a_val_b * b_lap_b + b_val_b * a_lap_b + 2.0 * cross
430
+ return JLP(
431
+ value=v_out,
432
+ jac=jac_out,
433
+ lap=lap_out,
434
+ level=a.level,
435
+ has_chunk_axis=a.has_chunk_axis,
436
+ chunk_idx=a.chunk_idx,
437
+ )
438
+ if _is_jlp(a):
439
+ v_out = a.value * b
440
+ jac_b, lap_b = _broadcast_jac_lap_for_op(a, v_out.shape)
441
+ return JLP(
442
+ value=v_out,
443
+ jac=b * jac_b,
444
+ lap=b * lap_b,
445
+ level=a.level,
446
+ has_chunk_axis=a.has_chunk_axis,
447
+ chunk_idx=a.chunk_idx,
448
+ )
449
+
450
+ v_out = a * b.value
451
+ jac_b, lap_b = _broadcast_jac_lap_for_op(b, v_out.shape)
452
+ return JLP(
453
+ value=v_out,
454
+ jac=a * jac_b,
455
+ lap=a * lap_b,
456
+ level=b.level,
457
+ has_chunk_axis=b.has_chunk_axis,
458
+ chunk_idx=b.chunk_idx,
459
+ )
460
+
461
+
462
+ def _jac_cross_quad_form(
463
+ jac_a, jac_b, level: int, chunk_idx: int, has_chunk_axis: bool, W_levels
464
+ ):
465
+
466
+ M = jac_a.shape[1]
467
+ W_used = _select_W_for_jlp(level, chunk_idx, has_chunk_axis, W_levels, M)
468
+ n_trailing = jac_a.ndim - 2
469
+ if n_trailing == 0:
470
+ out = jnp.einsum("mc,cmn,nc->c", jac_a, W_used, jac_b)
471
+ elif n_trailing == 1:
472
+ out = jnp.einsum("mca,cmn,nca->ca", jac_a, W_used, jac_b)
473
+ elif n_trailing == 2:
474
+ out = jnp.einsum("mcab,cmn,ncab->cab", jac_a, W_used, jac_b)
475
+ elif n_trailing == 3:
476
+ out = jnp.einsum("mcabd,cmn,ncabd->cabd", jac_a, W_used, jac_b)
477
+ else:
478
+ raise NotImplementedError(
479
+ f"_jac_cross_quad_form: trailing rank {n_trailing} not supported"
480
+ )
481
+ if not has_chunk_axis:
482
+ out = out.sum(axis=0)
483
+ return out
484
+
485
+
486
+ _RULE_REGISTRY[lax.mul_p] = _mul_rule
487
+
488
+
489
+ def _div_rule(invals, params, W_levels):
490
+ a, b = invals
491
+ if _is_jlp(a) and _is_jlp(b):
492
+ need_promote = (a.level != b.level) or (
493
+ not a.has_chunk_axis and not b.has_chunk_axis and a.chunk_idx != b.chunk_idx
494
+ )
495
+ if need_promote:
496
+ a, b = _align_jlp_levels(a, b)
497
+ assert a.has_chunk_axis == b.has_chunk_axis
498
+ v_out = a.value / b.value
499
+ a_val_b = jnp.broadcast_to(a.value, v_out.shape)
500
+ b_val_b = jnp.broadcast_to(b.value, v_out.shape)
501
+ a_jac_b = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
502
+ b_jac_b = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
503
+ inv_b = 1.0 / b_val_b
504
+ jac_out = inv_b * a_jac_b - (a_val_b * inv_b * inv_b) * b_jac_b
505
+ a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
506
+ b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
507
+ lap_first = inv_b * a_lap_b - (a_val_b * inv_b * inv_b) * b_lap_b
508
+ cross_bb = _jac_self_quad_form(
509
+ b_jac_b, b.level, b.chunk_idx, b.has_chunk_axis, W_levels
510
+ )
511
+ cross_ab = _jac_cross_quad_form(
512
+ a_jac_b, b_jac_b, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
513
+ )
514
+ lap_second = (2.0 * a_val_b * inv_b**3) * cross_bb + (
515
+ -2.0 * inv_b * inv_b
516
+ ) * cross_ab
517
+ lap_out = lap_first + lap_second
518
+ return JLP(
519
+ value=v_out,
520
+ jac=jac_out,
521
+ lap=lap_out,
522
+ level=a.level,
523
+ has_chunk_axis=a.has_chunk_axis,
524
+ chunk_idx=a.chunk_idx,
525
+ )
526
+ if _is_jlp(a):
527
+ inv_b = 1.0 / b
528
+ v_out = a.value * inv_b
529
+ jac_b, lap_b = _broadcast_jac_lap_for_op(a, v_out.shape)
530
+ return JLP(
531
+ value=v_out,
532
+ jac=inv_b * jac_b,
533
+ lap=inv_b * lap_b,
534
+ level=a.level,
535
+ has_chunk_axis=a.has_chunk_axis,
536
+ chunk_idx=a.chunk_idx,
537
+ )
538
+
539
+ v_out = a / b.value
540
+ inv_b = 1.0 / b.value
541
+ fp = -a * inv_b * inv_b
542
+ jac_out = fp * b.jac
543
+ fpp = 2.0 * a * inv_b**3
544
+ cross = _jac_self_quad_form(b.jac, b.level, b.chunk_idx, b.has_chunk_axis, W_levels)
545
+ lap_out = fp * b.lap + fpp * cross
546
+ return JLP(
547
+ value=v_out,
548
+ jac=jac_out,
549
+ lap=lap_out,
550
+ level=b.level,
551
+ has_chunk_axis=b.has_chunk_axis,
552
+ chunk_idx=b.chunk_idx,
553
+ )
554
+
555
+
556
+ _RULE_REGISTRY[lax.div_p] = _div_rule
557
+
558
+
559
+ def _atan2_rule(invals, params, W_levels):
560
+ a_arg, b_arg = invals
561
+ if _is_jlp(a_arg) and _is_jlp(b_arg):
562
+ need_promote = (a_arg.level != b_arg.level) or (
563
+ not a_arg.has_chunk_axis
564
+ and not b_arg.has_chunk_axis
565
+ and a_arg.chunk_idx != b_arg.chunk_idx
566
+ )
567
+ if need_promote:
568
+ a_arg, b_arg = _align_jlp_levels(a_arg, b_arg)
569
+ assert a_arg.has_chunk_axis == b_arg.has_chunk_axis
570
+ a, b = a_arg, b_arg
571
+ elif _is_jlp(a_arg):
572
+ a = a_arg
573
+ b = _trivial_jlp_like(b_arg, a_arg)
574
+ else:
575
+ b = b_arg
576
+ a = _trivial_jlp_like(a_arg, b_arg)
577
+ v_out = jnp.arctan2(a.value, b.value)
578
+ r_sq = a.value**2 + b.value**2
579
+ inv_r2 = 1.0 / r_sq
580
+ fa = b.value * inv_r2
581
+ fb = -a.value * inv_r2
582
+ aj = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
583
+ bj = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
584
+ jac_out = fa * aj + fb * bj
585
+ faa = -2.0 * a.value * b.value * inv_r2 * inv_r2
586
+ fbb = 2.0 * a.value * b.value * inv_r2 * inv_r2
587
+ fab = (a.value**2 - b.value**2) * inv_r2 * inv_r2
588
+ al = jnp.broadcast_to(a.lap, v_out.shape)
589
+ bl = jnp.broadcast_to(b.lap, v_out.shape)
590
+ cross_aa = _jac_self_quad_form(aj, a.level, a.chunk_idx, a.has_chunk_axis, W_levels)
591
+ cross_bb = _jac_self_quad_form(bj, b.level, b.chunk_idx, b.has_chunk_axis, W_levels)
592
+ cross_ab = _jac_cross_quad_form(
593
+ aj, bj, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
594
+ )
595
+ lap_out = fa * al + fb * bl + faa * cross_aa + fbb * cross_bb + 2.0 * fab * cross_ab
596
+ return JLP(
597
+ value=v_out,
598
+ jac=jac_out,
599
+ lap=lap_out,
600
+ level=a.level,
601
+ has_chunk_axis=a.has_chunk_axis,
602
+ chunk_idx=a.chunk_idx,
603
+ )
604
+
605
+
606
+ def _trivial_jlp_like(plain_val, template_jlp: JLP) -> JLP:
607
+
608
+ val = jnp.asarray(plain_val)
609
+ if template_jlp.has_chunk_axis:
610
+ jac_shape = (template_jlp.jac.shape[0], template_jlp.jac.shape[1]) + tuple(
611
+ val.shape[1:]
612
+ )
613
+ else:
614
+ jac_shape = (template_jlp.jac.shape[0], template_jlp.jac.shape[1]) + tuple(
615
+ val.shape
616
+ )
617
+ return JLP(
618
+ value=val,
619
+ jac=jnp.zeros(jac_shape, dtype=val.dtype),
620
+ lap=jnp.zeros_like(val),
621
+ level=template_jlp.level,
622
+ has_chunk_axis=template_jlp.has_chunk_axis,
623
+ chunk_idx=template_jlp.chunk_idx,
624
+ )
625
+
626
+
627
+ _RULE_REGISTRY[lax.atan2_p] = _atan2_rule
628
+
629
+
630
+ def _dot_general_rule(invals, params, W_levels):
631
+ lhs, rhs = invals
632
+ dimension_numbers = params["dimension_numbers"]
633
+ (lhs_contract, rhs_contract), (lhs_batch, rhs_batch) = dimension_numbers
634
+
635
+ if _is_jlp(lhs) and _is_jlp(rhs):
636
+ raise NotImplementedError(
637
+ "dot_general: JLP × JLP outside quadrilinear_merge_p is not supported. "
638
+ "If a model op needs same-level bilinear, route it through the merge "
639
+ "primitive or rewrite as elementwise mul + reduce_sum."
640
+ )
641
+ if _is_jlp(lhs):
642
+ return _dot_general_jlp_plain(
643
+ lhs, rhs, dimension_numbers, params, jlp_is_lhs=True
644
+ )
645
+ return _dot_general_jlp_plain(rhs, lhs, dimension_numbers, params, jlp_is_lhs=False)
646
+
647
+
648
+ def _dot_general_jlp_plain(
649
+ jlp_arg, plain_arg, dimension_numbers, params, jlp_is_lhs: bool
650
+ ):
651
+
652
+ (lhs_contract, rhs_contract), (lhs_batch, rhs_batch) = dimension_numbers
653
+ dot_kw = {
654
+ "precision": params.get("precision", None),
655
+ "preferred_element_type": params.get("preferred_element_type", None),
656
+ "out_sharding": params.get("out_sharding", None),
657
+ }
658
+
659
+ if jlp_is_lhs:
660
+ v_out = lax.dot_general(jlp_arg.value, plain_arg, dimension_numbers, **dot_kw)
661
+ lap_out = lax.dot_general(jlp_arg.lap, plain_arg, dimension_numbers, **dot_kw)
662
+ else:
663
+ v_out = lax.dot_general(plain_arg, jlp_arg.value, dimension_numbers, **dot_kw)
664
+ lap_out = lax.dot_general(plain_arg, jlp_arg.lap, dimension_numbers, **dot_kw)
665
+
666
+ jac = jlp_arg.jac
667
+ leading_3k = jac.shape[0]
668
+
669
+ if jlp_arg.has_chunk_axis:
670
+ jac_for_dot = jac
671
+ shift = 1
672
+ else:
673
+ jac_for_dot = jac.reshape((leading_3k,) + tuple(jlp_arg.value.shape))
674
+ shift = 1
675
+
676
+ if jlp_is_lhs:
677
+ new_lhs_contract = tuple(a + shift for a in lhs_contract)
678
+ new_rhs_contract = tuple(rhs_contract)
679
+ new_lhs_batch = tuple(a + shift for a in lhs_batch)
680
+ new_rhs_batch = tuple(rhs_batch)
681
+ new_dim_nums = (
682
+ (new_lhs_contract, new_rhs_contract),
683
+ (new_lhs_batch, new_rhs_batch),
684
+ )
685
+ jac_out_raw = lax.dot_general(jac_for_dot, plain_arg, new_dim_nums, **dot_kw)
686
+
687
+ n_batch = len(new_lhs_batch)
688
+ pos_3k = n_batch
689
+ else:
690
+ new_lhs_contract = tuple(lhs_contract)
691
+ new_rhs_contract = tuple(a + shift for a in rhs_contract)
692
+ new_lhs_batch = tuple(lhs_batch)
693
+ new_rhs_batch = tuple(a + shift for a in rhs_batch)
694
+ new_dim_nums = (
695
+ (new_lhs_contract, new_rhs_contract),
696
+ (new_lhs_batch, new_rhs_batch),
697
+ )
698
+ jac_out_raw = lax.dot_general(plain_arg, jac_for_dot, new_dim_nums, **dot_kw)
699
+
700
+ n_batch = len(new_lhs_batch)
701
+ lhs_ndim = jnp.asarray(plain_arg).ndim
702
+ n_lhs_nonbatch = lhs_ndim - n_batch - len(new_lhs_contract)
703
+ pos_3k = n_batch + n_lhs_nonbatch
704
+
705
+ if pos_3k != 0:
706
+ jac_out = jnp.moveaxis(jac_out_raw, pos_3k, 0)
707
+ else:
708
+ jac_out = jac_out_raw
709
+
710
+ if jlp_arg.has_chunk_axis:
711
+ new_has_chunk_axis = True
712
+ new_chunk_idx = -1
713
+ else:
714
+ jac_out = jac_out[:, None]
715
+ new_has_chunk_axis = False
716
+ new_chunk_idx = jlp_arg.chunk_idx
717
+
718
+ return JLP(
719
+ value=v_out,
720
+ jac=jac_out,
721
+ lap=lap_out,
722
+ level=jlp_arg.level,
723
+ has_chunk_axis=new_has_chunk_axis,
724
+ chunk_idx=new_chunk_idx,
725
+ )
726
+
727
+
728
+ _RULE_REGISTRY[lax.dot_general_p] = _dot_general_rule
729
+
730
+
731
+ def _broadcast_in_dim_rule(invals, params, W_levels):
732
+ [x] = invals
733
+ assert _is_jlp(x)
734
+ shape = params["shape"]
735
+ broadcast_dimensions = params["broadcast_dimensions"]
736
+ extra = _shape_bind_params(params, "shape", "broadcast_dimensions")
737
+
738
+ v_out = lax.broadcast_in_dim_p.bind(
739
+ x.value, shape=shape, broadcast_dimensions=broadcast_dimensions, **extra
740
+ )
741
+ lap_out = lax.broadcast_in_dim_p.bind(
742
+ x.lap, shape=shape, broadcast_dimensions=broadcast_dimensions, **extra
743
+ )
744
+
745
+ if x.has_chunk_axis:
746
+ new_shape = (x.jac.shape[0],) + tuple(shape)
747
+ new_bd = (0,) + tuple(d + 1 for d in broadcast_dimensions)
748
+ jac_out = lax.broadcast_in_dim_p.bind(
749
+ x.jac.reshape((x.jac.shape[0],) + x.value.shape),
750
+ shape=new_shape,
751
+ broadcast_dimensions=new_bd,
752
+ **extra,
753
+ )
754
+
755
+ new_has_chunk_axis = True
756
+ new_chunk_idx = -1
757
+ else:
758
+ new_shape = (x.jac.shape[0], 1) + tuple(shape)
759
+ new_bd = (0, 1) + tuple(d + 2 for d in broadcast_dimensions)
760
+ jac_out = lax.broadcast_in_dim_p.bind(
761
+ x.jac,
762
+ shape=new_shape,
763
+ broadcast_dimensions=new_bd,
764
+ **extra,
765
+ )
766
+ new_has_chunk_axis = False
767
+ new_chunk_idx = x.chunk_idx
768
+
769
+ return JLP(
770
+ value=v_out,
771
+ jac=jac_out,
772
+ lap=lap_out,
773
+ level=x.level,
774
+ has_chunk_axis=new_has_chunk_axis,
775
+ chunk_idx=new_chunk_idx,
776
+ )
777
+
778
+
779
+ _RULE_REGISTRY[lax.broadcast_in_dim_p] = _broadcast_in_dim_rule
780
+
781
+
782
+ def _reduce_sum_rule(invals, params, W_levels):
783
+ [x] = invals
784
+ assert _is_jlp(x)
785
+ axes = tuple(params["axes"])
786
+ extra = _params_except(params, "axes", out_sharding=None)
787
+ v_out = lax.reduce_sum_p.bind(x.value, axes=axes, **extra)
788
+ lap_out = lax.reduce_sum_p.bind(x.lap, axes=axes, **extra)
789
+ if x.has_chunk_axis:
790
+ if 0 in axes:
791
+ non_chunk_axes = tuple(a for a in axes if a != 0)
792
+ jac_reduce_axes = tuple(a + 1 for a in non_chunk_axes)
793
+ if jac_reduce_axes:
794
+ jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
795
+ else:
796
+ jac_out = x.jac
797
+ new_has_chunk_axis = False
798
+ new_chunk_idx = -1
799
+ else:
800
+ jac_reduce_axes = tuple(a + 1 for a in axes)
801
+ jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
802
+ new_has_chunk_axis = True
803
+ new_chunk_idx = -1
804
+ else:
805
+ jac_reduce_axes = tuple(a + 2 for a in axes)
806
+ jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
807
+ new_has_chunk_axis = False
808
+ new_chunk_idx = x.chunk_idx
809
+ return JLP(
810
+ value=v_out,
811
+ jac=jac_out,
812
+ lap=lap_out,
813
+ level=x.level,
814
+ has_chunk_axis=new_has_chunk_axis,
815
+ chunk_idx=new_chunk_idx,
816
+ )
817
+
818
+
819
+ _RULE_REGISTRY[lax.reduce_sum_p] = _reduce_sum_rule
820
+
821
+
822
+ def _reduce_max_rule(invals, params, W_levels):
823
+ [x] = invals
824
+ assert _is_jlp(x)
825
+ axes = tuple(params["axes"])
826
+ extra = _params_except(params, "axes", out_sharding=None)
827
+ v_out = lax.reduce_max_p.bind(x.value, axes=axes, **extra)
828
+
829
+ keep_shape = list(x.value.shape)
830
+ for a in axes:
831
+ keep_shape[a] = 1
832
+ v_max_kept = lax.reduce_max_p.bind(x.value, axes=axes, **extra).reshape(
833
+ tuple(keep_shape)
834
+ )
835
+ mask = (x.value == v_max_kept).astype(x.value.dtype)
836
+
837
+ mask_sum_axes = lax.reduce_sum_p.bind(
838
+ mask,
839
+ axes=axes,
840
+ **extra,
841
+ ).reshape(tuple(keep_shape))
842
+ mask = mask / (mask_sum_axes + 1e-30)
843
+
844
+ def _gated_reduce(jac_arr, jac_axes):
845
+
846
+ n_lead = jac_arr.ndim - x.value.ndim
847
+ mask_b = mask.reshape((1,) * n_lead + mask.shape)
848
+
849
+ return lax.reduce_sum_p.bind(
850
+ jac_arr * mask_b,
851
+ axes=jac_axes,
852
+ **extra,
853
+ )
854
+
855
+ if x.has_chunk_axis:
856
+ if 0 in axes:
857
+ non_chunk_axes = tuple(a for a in axes if a != 0)
858
+ jac_reduce_axes = tuple(a + 1 for a in non_chunk_axes) + (1,)
859
+ jac_out = _gated_reduce(x.jac, jac_reduce_axes)
860
+ new_has_chunk_axis = False
861
+ new_chunk_idx = -1
862
+ else:
863
+ jac_reduce_axes = tuple(a + 1 for a in axes)
864
+ jac_out = _gated_reduce(x.jac, jac_reduce_axes)
865
+ new_has_chunk_axis = True
866
+ new_chunk_idx = -1
867
+ else:
868
+ jac_reduce_axes = tuple(a + 2 for a in axes)
869
+ jac_out = _gated_reduce(x.jac, jac_reduce_axes)
870
+ new_has_chunk_axis = False
871
+ new_chunk_idx = x.chunk_idx
872
+ lap_out = jnp.zeros_like(v_out)
873
+ return JLP(
874
+ value=v_out,
875
+ jac=jac_out,
876
+ lap=lap_out,
877
+ level=x.level,
878
+ has_chunk_axis=new_has_chunk_axis,
879
+ chunk_idx=new_chunk_idx,
880
+ )
881
+
882
+
883
+ _RULE_REGISTRY[lax.reduce_max_p] = _reduce_max_rule
884
+
885
+
886
+ def _make_minmax_rule(sign_for_first):
887
+
888
+ def rule(invals, params, W_levels):
889
+ a, b = invals
890
+ if _is_jlp(a) and _is_jlp(b):
891
+ need_promote = (
892
+ (a.level != b.level)
893
+ or (
894
+ not a.has_chunk_axis
895
+ and not b.has_chunk_axis
896
+ and a.chunk_idx != b.chunk_idx
897
+ )
898
+ or (
899
+ a.has_chunk_axis
900
+ and b.has_chunk_axis
901
+ and a.chunk_idx in (0, 1)
902
+ and b.chunk_idx in (0, 1)
903
+ and a.chunk_idx != b.chunk_idx
904
+ )
905
+ )
906
+ if need_promote:
907
+ a, b = _align_jlp_levels(a, b)
908
+ cmp = (a.value - b.value) * sign_for_first
909
+ mask_a = (cmp > 0).astype(a.value.dtype)
910
+ mask_b = 1.0 - mask_a
911
+ v_out = mask_a * a.value + mask_b * b.value
912
+
913
+ jac_a_b = _broadcast_jac_to_value_shape(
914
+ a.jac, v_out.shape, a.has_chunk_axis
915
+ )
916
+ jac_b_b = _broadcast_jac_to_value_shape(
917
+ b.jac, v_out.shape, b.has_chunk_axis
918
+ )
919
+ n_lead_a = jac_a_b.ndim - mask_a.ndim
920
+ n_lead_b = jac_b_b.ndim - mask_b.ndim
921
+ mask_a_lead = mask_a.reshape((1,) * n_lead_a + mask_a.shape)
922
+ mask_b_lead = mask_b.reshape((1,) * n_lead_b + mask_b.shape)
923
+ jac_out = mask_a_lead * jac_a_b + mask_b_lead * jac_b_b
924
+ lap_out = mask_a * jnp.broadcast_to(
925
+ a.lap, v_out.shape
926
+ ) + mask_b * jnp.broadcast_to(b.lap, v_out.shape)
927
+ return JLP(
928
+ value=v_out,
929
+ jac=jac_out,
930
+ lap=lap_out,
931
+ level=a.level,
932
+ has_chunk_axis=a.has_chunk_axis,
933
+ chunk_idx=a.chunk_idx,
934
+ )
935
+ if _is_jlp(a):
936
+ cmp = (a.value - b) * sign_for_first
937
+ mask_a = (cmp > 0).astype(a.value.dtype)
938
+ v_out = mask_a * a.value + (1 - mask_a) * b
939
+ n_lead = a.jac.ndim - mask_a.ndim
940
+ mask_a_lead = mask_a.reshape((1,) * n_lead + mask_a.shape)
941
+ jac_out = mask_a_lead * a.jac
942
+ lap_out = mask_a * a.lap
943
+ return JLP(
944
+ value=v_out,
945
+ jac=jac_out,
946
+ lap=lap_out,
947
+ level=a.level,
948
+ has_chunk_axis=a.has_chunk_axis,
949
+ chunk_idx=a.chunk_idx,
950
+ )
951
+
952
+ cmp = (a - b.value) * sign_for_first
953
+ mask_a = (cmp > 0).astype(b.value.dtype)
954
+ v_out = mask_a * a + (1 - mask_a) * b.value
955
+ mask_b = 1 - mask_a
956
+ n_lead = b.jac.ndim - mask_b.ndim
957
+ mask_b_lead = mask_b.reshape((1,) * n_lead + mask_b.shape)
958
+ jac_out = mask_b_lead * b.jac
959
+ lap_out = mask_b * b.lap
960
+ return JLP(
961
+ value=v_out,
962
+ jac=jac_out,
963
+ lap=lap_out,
964
+ level=b.level,
965
+ has_chunk_axis=b.has_chunk_axis,
966
+ chunk_idx=b.chunk_idx,
967
+ )
968
+
969
+ return rule
970
+
971
+
972
+ _RULE_REGISTRY[lax.max_p] = _make_minmax_rule(+1.0)
973
+ _RULE_REGISTRY[lax.min_p] = _make_minmax_rule(-1.0)
974
+
975
+
976
+ def _reshape_jac_for_value(x: JLP, new_value_shape: tuple, new_dimensions):
977
+
978
+ leading_3k = x.jac.shape[0]
979
+ if x.has_chunk_axis:
980
+ M = x.value.shape[0]
981
+ old_trailing = x.value.shape[1:]
982
+
983
+ if len(new_value_shape) >= 1 and new_value_shape[0] == M:
984
+ jac_axes_perm = None
985
+ if new_dimensions is not None:
986
+ jac_axes_perm = (0,) + tuple(d + 1 for d in new_dimensions)
987
+ jac_pre = jnp.transpose(x.jac, jac_axes_perm)
988
+ else:
989
+ jac_pre = x.jac
990
+ new_jac_shape = (leading_3k,) + tuple(new_value_shape)
991
+ jac_new = jnp.reshape(jac_pre, new_jac_shape)
992
+
993
+ return jac_new, True, -1
994
+
995
+ if M == 1:
996
+ assert new_dimensions is None, (
997
+ "reshape with M=1-squeeze + transpose not yet supported"
998
+ )
999
+
1000
+ new_jac_shape = (leading_3k, 1) + tuple(new_value_shape)
1001
+
1002
+ jac_new = jnp.reshape(x.jac, new_jac_shape)
1003
+ return jac_new, False, 0
1004
+ raise NotImplementedError(
1005
+ f"reshape: cannot reshape JLP value {x.value.shape} (has_chunk_axis, M={M}) "
1006
+ f"to {new_value_shape} — chunk axis would be fused/lost."
1007
+ )
1008
+
1009
+ assert new_dimensions is None or all(d >= 0 for d in new_dimensions)
1010
+ jac_pre = x.jac
1011
+ if new_dimensions is not None:
1012
+ jac_axes_perm = (0, 1) + tuple(d + 2 for d in new_dimensions)
1013
+ jac_pre = jnp.transpose(jac_pre, jac_axes_perm)
1014
+ new_jac_shape = (leading_3k, 1) + tuple(new_value_shape)
1015
+ jac_new = jnp.reshape(jac_pre, new_jac_shape)
1016
+ return jac_new, False, x.chunk_idx
1017
+
1018
+
1019
+ def _reshape_rule(invals, params, W_levels):
1020
+ [x] = invals
1021
+ assert _is_jlp(x)
1022
+ new_sizes = tuple(params["new_sizes"])
1023
+ dimensions = params.get("dimensions")
1024
+
1025
+ extra = _shape_bind_params(params, "new_sizes", "dimensions")
1026
+ v_out = lax.reshape_p.bind(
1027
+ x.value, new_sizes=new_sizes, dimensions=dimensions, **extra
1028
+ )
1029
+ lap_out = lax.reshape_p.bind(
1030
+ x.lap, new_sizes=new_sizes, dimensions=dimensions, **extra
1031
+ )
1032
+ jac_new, new_has_chunk, new_idx = _reshape_jac_for_value(x, new_sizes, dimensions)
1033
+ return JLP(
1034
+ value=v_out,
1035
+ jac=jac_new,
1036
+ lap=lap_out,
1037
+ level=x.level,
1038
+ has_chunk_axis=new_has_chunk,
1039
+ chunk_idx=new_idx,
1040
+ )
1041
+
1042
+
1043
+ _RULE_REGISTRY[lax.reshape_p] = _reshape_rule
1044
+
1045
+
1046
+ def _transpose_rule(invals, params, W_levels):
1047
+ [x] = invals
1048
+ assert _is_jlp(x)
1049
+ perm = tuple(params["permutation"])
1050
+ extra = {k: params[k] for k in params if k != "permutation"}
1051
+ v_out = lax.transpose_p.bind(x.value, permutation=perm, **extra)
1052
+ lap_out = lax.transpose_p.bind(x.lap, permutation=perm, **extra)
1053
+
1054
+ if x.has_chunk_axis:
1055
+ if perm[0] != 0:
1056
+ raise NotImplementedError(
1057
+ "transpose: chunk axis (value axis 0) must remain at position 0; "
1058
+ f"got permutation {perm}."
1059
+ )
1060
+
1061
+ jac_perm = (0, 1) + tuple(p + 1 for p in perm[1:])
1062
+ else:
1063
+ jac_perm = (0, 1) + tuple(p + 2 for p in perm)
1064
+ jac_new = lax.transpose_p.bind(x.jac, permutation=jac_perm, **extra)
1065
+ return JLP(
1066
+ value=v_out,
1067
+ jac=jac_new,
1068
+ lap=lap_out,
1069
+ level=x.level,
1070
+ has_chunk_axis=x.has_chunk_axis,
1071
+ chunk_idx=x.chunk_idx,
1072
+ )
1073
+
1074
+
1075
+ _RULE_REGISTRY[lax.transpose_p] = _transpose_rule
1076
+
1077
+
1078
+ def _slice_rule(invals, params, W_levels):
1079
+ [x] = invals
1080
+ assert _is_jlp(x)
1081
+ start = tuple(params["start_indices"])
1082
+ limit = tuple(params["limit_indices"])
1083
+ strides = params.get("strides")
1084
+ extra = {
1085
+ k: params[k]
1086
+ for k in params
1087
+ if k not in ("start_indices", "limit_indices", "strides")
1088
+ }
1089
+ v_out = lax.slice_p.bind(
1090
+ x.value, start_indices=start, limit_indices=limit, strides=strides, **extra
1091
+ )
1092
+ lap_out = lax.slice_p.bind(
1093
+ x.lap, start_indices=start, limit_indices=limit, strides=strides, **extra
1094
+ )
1095
+
1096
+ if x.has_chunk_axis:
1097
+ new_chunk_idx = x.chunk_idx
1098
+ old_M = x.value.shape[0]
1099
+ stride0 = strides[0] if strides is not None else 1
1100
+ v_out_M = v_out.shape[0]
1101
+ chunk_axis_touched = start[0] != 0 or limit[0] != old_M or stride0 != 1
1102
+ if chunk_axis_touched:
1103
+ if v_out_M == 1:
1104
+ new_chunk_idx = start[0]
1105
+ elif stride0 > 1 and v_out_M * stride0 == old_M and start[0] in (0, 1):
1106
+ new_chunk_idx = start[0]
1107
+ elif v_out_M != old_M:
1108
+ raise NotImplementedError(
1109
+ f"slice on chunk axis: unsupported sub-range "
1110
+ f"start={start[0]}, limit={limit[0]}, stride={stride0}, "
1111
+ f"old_M={old_M}, v_out_M={v_out_M}"
1112
+ )
1113
+ jac_start = (0, start[0]) + tuple(start[1:])
1114
+ jac_limit = (x.jac.shape[0], limit[0]) + tuple(limit[1:])
1115
+ jac_strides = None if strides is None else (1, strides[0]) + tuple(strides[1:])
1116
+ jac_out = lax.slice_p.bind(
1117
+ x.jac,
1118
+ start_indices=jac_start,
1119
+ limit_indices=jac_limit,
1120
+ strides=jac_strides,
1121
+ **extra,
1122
+ )
1123
+ return JLP(
1124
+ value=v_out,
1125
+ jac=jac_out,
1126
+ lap=lap_out,
1127
+ level=x.level,
1128
+ has_chunk_axis=True,
1129
+ chunk_idx=new_chunk_idx,
1130
+ )
1131
+
1132
+ jac_start = (0, 0) + tuple(start)
1133
+ jac_limit = (x.jac.shape[0], x.jac.shape[1]) + tuple(limit)
1134
+ jac_strides = None if strides is None else (1, 1) + tuple(strides)
1135
+ jac_out = lax.slice_p.bind(
1136
+ x.jac,
1137
+ start_indices=jac_start,
1138
+ limit_indices=jac_limit,
1139
+ strides=jac_strides,
1140
+ **extra,
1141
+ )
1142
+ return JLP(
1143
+ value=v_out,
1144
+ jac=jac_out,
1145
+ lap=lap_out,
1146
+ level=x.level,
1147
+ has_chunk_axis=False,
1148
+ chunk_idx=x.chunk_idx,
1149
+ )
1150
+
1151
+
1152
+ _RULE_REGISTRY[lax.slice_p] = _slice_rule
1153
+
1154
+
1155
+ def _squeeze_rule(invals, params, W_levels):
1156
+ [x] = invals
1157
+ assert _is_jlp(x)
1158
+ dims = tuple(params["dimensions"])
1159
+ extra = {k: params[k] for k in params if k != "dimensions"}
1160
+ v_out = lax.squeeze_p.bind(x.value, dimensions=dims, **extra)
1161
+ lap_out = lax.squeeze_p.bind(x.lap, dimensions=dims, **extra)
1162
+ if x.has_chunk_axis and 0 in dims:
1163
+ non_chunk_dims = tuple(d for d in dims if d != 0)
1164
+ jac_dims = tuple(d + 1 for d in non_chunk_dims)
1165
+
1166
+ if jac_dims:
1167
+ jac_out = lax.squeeze_p.bind(x.jac, dimensions=jac_dims, **extra)
1168
+ else:
1169
+ jac_out = x.jac
1170
+ return JLP(
1171
+ value=v_out,
1172
+ jac=jac_out,
1173
+ lap=lap_out,
1174
+ level=x.level,
1175
+ has_chunk_axis=False,
1176
+ chunk_idx=x.chunk_idx,
1177
+ )
1178
+ if x.has_chunk_axis:
1179
+ jac_dims = tuple(d + 1 for d in dims)
1180
+ else:
1181
+ jac_dims = tuple(d + 2 for d in dims)
1182
+ if jac_dims:
1183
+ jac_out = lax.squeeze_p.bind(x.jac, dimensions=jac_dims, **extra)
1184
+ else:
1185
+ jac_out = x.jac
1186
+ return JLP(
1187
+ value=v_out,
1188
+ jac=jac_out,
1189
+ lap=lap_out,
1190
+ level=x.level,
1191
+ has_chunk_axis=x.has_chunk_axis,
1192
+ chunk_idx=x.chunk_idx,
1193
+ )
1194
+
1195
+
1196
+ _RULE_REGISTRY[lax.squeeze_p] = _squeeze_rule
1197
+
1198
+
1199
+ _stack_p = lax.stack_p
1200
+
1201
+
1202
+ def _stack_rule(invals, params, W_levels):
1203
+ del W_levels
1204
+ axis = int(params["axis"])
1205
+ extra = {k: params[k] for k in params if k != "axis"}
1206
+
1207
+ jlp_inputs = [v for v in invals if _is_jlp(v)]
1208
+ levels = {v.level for v in jlp_inputs}
1209
+ assert len(levels) == 1, f"stack: mixed JLP levels {levels}"
1210
+ level = next(iter(levels))
1211
+ has_chunk = jlp_inputs[0].has_chunk_axis
1212
+ chunk_idx = jlp_inputs[0].chunk_idx
1213
+ for v in jlp_inputs[1:]:
1214
+ assert v.has_chunk_axis == has_chunk and v.chunk_idx == chunk_idx, (
1215
+ "stack: inconsistent chunk metadata"
1216
+ )
1217
+
1218
+ if has_chunk and axis == 0:
1219
+ raise NotImplementedError(
1220
+ "stack: inserting an axis before the JLP chunk axis is not supported"
1221
+ )
1222
+
1223
+ values = [v.value if _is_jlp(v) else v for v in invals]
1224
+ value_out = _stack_p.bind(*values, axis=axis, **extra)
1225
+
1226
+ laps = [v.lap if _is_jlp(v) else jnp.zeros_like(v) for v in invals]
1227
+ lap_out = _stack_p.bind(*laps, axis=axis, **extra)
1228
+
1229
+ jac_axis = axis + (1 if has_chunk else 2)
1230
+ ref_jac = jlp_inputs[0].jac
1231
+ jacs = [v.jac if _is_jlp(v) else jnp.zeros_like(ref_jac) for v in invals]
1232
+ jac_out = _stack_p.bind(*jacs, axis=jac_axis, **extra)
1233
+ return JLP(
1234
+ value=value_out,
1235
+ jac=jac_out,
1236
+ lap=lap_out,
1237
+ level=level,
1238
+ has_chunk_axis=has_chunk,
1239
+ chunk_idx=chunk_idx,
1240
+ )
1241
+
1242
+
1243
+ _RULE_REGISTRY[_stack_p] = _stack_rule
1244
+
1245
+
1246
+ def _concatenate_rule(invals, params, W_levels):
1247
+ dim = params["dimension"]
1248
+ extra = {k: params[k] for k in params if k != "dimension"}
1249
+
1250
+ jlp_inputs = [v for v in invals if _is_jlp(v)]
1251
+ levels = set(v.level for v in jlp_inputs)
1252
+ assert len(levels) == 1, f"concatenate: mixed JLP levels {levels}"
1253
+ level = next(iter(levels))
1254
+ has_chunk = jlp_inputs[0].has_chunk_axis
1255
+ chunk_idx = jlp_inputs[0].chunk_idx
1256
+ for v in jlp_inputs[1:]:
1257
+ assert v.has_chunk_axis == has_chunk and v.chunk_idx == chunk_idx, (
1258
+ "concatenate: inconsistent chunk metadata"
1259
+ )
1260
+ if has_chunk and dim == 0:
1261
+ raise NotImplementedError("concatenate on chunk axis not supported")
1262
+
1263
+ values = [v.value if _is_jlp(v) else v for v in invals]
1264
+ v_out = lax.concatenate_p.bind(*values, dimension=dim, **extra)
1265
+ laps = [v.lap if _is_jlp(v) else jnp.zeros_like(v) for v in invals]
1266
+ lap_out = lax.concatenate_p.bind(*laps, dimension=dim, **extra)
1267
+
1268
+ jac_dim = dim + 2 if not has_chunk else dim + 1
1269
+ jacs = []
1270
+ for v in invals:
1271
+ if _is_jlp(v):
1272
+ jacs.append(v.jac)
1273
+ else:
1274
+ shape_v = v.shape if hasattr(v, "shape") else jnp.asarray(v).shape
1275
+ if has_chunk:
1276
+ M = jlp_inputs[0].jac.shape[1]
1277
+ jac_shape = (jlp_inputs[0].jac.shape[0], M) + tuple(shape_v[1:])
1278
+ else:
1279
+ M = jlp_inputs[0].jac.shape[1]
1280
+ jac_shape = (jlp_inputs[0].jac.shape[0], M) + tuple(shape_v)
1281
+ jacs.append(jnp.zeros(jac_shape, dtype=jlp_inputs[0].jac.dtype))
1282
+ jac_out = lax.concatenate_p.bind(*jacs, dimension=jac_dim, **extra)
1283
+ return JLP(
1284
+ value=v_out,
1285
+ jac=jac_out,
1286
+ lap=lap_out,
1287
+ level=level,
1288
+ has_chunk_axis=has_chunk,
1289
+ chunk_idx=chunk_idx,
1290
+ )
1291
+
1292
+
1293
+ _RULE_REGISTRY[lax.concatenate_p] = _concatenate_rule
1294
+
1295
+
1296
+ def _jit_p_rule(invals, params, W_levels):
1297
+
1298
+ inner_jaxpr = params["jaxpr"]
1299
+ j = inner_jaxpr.jaxpr
1300
+ consts = inner_jaxpr.consts
1301
+ env: dict = {}
1302
+ for cv, c in zip(j.constvars, consts):
1303
+ env[cv] = c
1304
+ for iv, x in zip(j.invars, invals):
1305
+ env[iv] = x
1306
+ for eqn in j.eqns:
1307
+ outvals = _eval_eqn(eqn, env, W_levels)
1308
+ for ov, ov_val in zip(eqn.outvars, outvals):
1309
+ env[ov] = ov_val
1310
+ return [env[ov] for ov in j.outvars]
1311
+
1312
+
1313
+ from jax._src import pjit as _pjit_module
1314
+
1315
+ _RULE_REGISTRY[_pjit_module.jit_p] = _jit_p_rule
1316
+
1317
+
1318
+ def _quadrilinear_merge_rule(invals, params, W_levels):
1319
+
1320
+ T, u_a, u_b = invals
1321
+ assert not _is_jlp(T), "quadrilinear_merge: T must be plain"
1322
+ assert _is_jlp(u_a) and _is_jlp(u_b), "quadrilinear_merge: u_a, u_b must be JLPs"
1323
+ assert u_a.level == u_b.level, (
1324
+ f"quadrilinear_merge: leg levels differ {u_a.level} vs {u_b.level}"
1325
+ )
1326
+ assert u_a.has_chunk_axis and u_b.has_chunk_axis, (
1327
+ "quadrilinear_merge requires the compiled chunk axis"
1328
+ )
1329
+
1330
+ k = u_a.level
1331
+ new_k = 2 * k
1332
+ G, d_r, _, _ = T.shape
1333
+ d_m_eff = G * d_r
1334
+ M = u_a.value.shape[0]
1335
+ assert u_b.value.shape[0] == M, "quadrilinear_merge: chunked legs must agree on M"
1336
+ u_a_2d = u_a.value.reshape(M, G, d_r)
1337
+ u_b_2d = u_b.value.reshape(M, G, d_r)
1338
+ raw_value_2d = jnp.einsum("ijkl,mik,mil->mij", T, u_a_2d, u_b_2d)
1339
+ raw_value = raw_value_2d.reshape(M, d_m_eff)
1340
+ ua_jac_2d = u_a.jac.reshape(u_a.jac.shape[0], M, G, d_r)
1341
+ ub_jac_2d = u_b.jac.reshape(u_b.jac.shape[0], M, G, d_r)
1342
+ jac_upper_2d = jnp.einsum(
1343
+ "ijkl,Amik,mil->Amij",
1344
+ T,
1345
+ ua_jac_2d,
1346
+ u_b_2d,
1347
+ )
1348
+ jac_lower_2d = jnp.einsum(
1349
+ "ijkl,mik,Bmil->Bmij",
1350
+ T,
1351
+ u_a_2d,
1352
+ ub_jac_2d,
1353
+ )
1354
+ jac_upper = jac_upper_2d.reshape(jac_upper_2d.shape[0], M, d_m_eff)
1355
+ jac_lower = jac_lower_2d.reshape(jac_lower_2d.shape[0], M, d_m_eff)
1356
+ jac_out = jnp.concatenate([jac_upper, jac_lower], axis=0)
1357
+ ua_lap_2d = u_a.lap.reshape(M, G, d_r)
1358
+ ub_lap_2d = u_b.lap.reshape(M, G, d_r)
1359
+ term1_2d = jnp.einsum(
1360
+ "ijkl,mik,mil->mij",
1361
+ T,
1362
+ ua_lap_2d,
1363
+ u_b_2d,
1364
+ )
1365
+ term2_2d = jnp.einsum(
1366
+ "ijkl,mik,mil->mij",
1367
+ T,
1368
+ u_a_2d,
1369
+ ub_lap_2d,
1370
+ )
1371
+ W_2k = _W_at_level(W_levels, new_k)
1372
+ W_off = W_2k[:, : 3 * k, 3 * k :]
1373
+ cross_2d = jnp.einsum(
1374
+ "ijkl,Amik,Bmil,mAB->mij",
1375
+ T,
1376
+ ua_jac_2d,
1377
+ ub_jac_2d,
1378
+ W_off,
1379
+ )
1380
+ lap_out_2d = term1_2d + term2_2d + 2.0 * cross_2d
1381
+ lap_out = lap_out_2d.reshape(M, d_m_eff)
1382
+ return JLP(
1383
+ value=raw_value,
1384
+ jac=jac_out,
1385
+ lap=lap_out,
1386
+ level=new_k,
1387
+ has_chunk_axis=True,
1388
+ chunk_idx=-1,
1389
+ )
1390
+
1391
+
1392
+ _RULE_REGISTRY[quadrilinear_merge_p] = _quadrilinear_merge_rule
1393
+
1394
+
1395
+ def _eval_eqn(eqn, env, W_levels):
1396
+ invals = []
1397
+ for v in eqn.invars:
1398
+ if isinstance(v, Literal):
1399
+ invals.append(v.val)
1400
+ else:
1401
+ invals.append(env[v])
1402
+ has_jlp = any(_is_jlp(x) for x in invals)
1403
+ if not has_jlp:
1404
+ bind_params = eqn.primitive.get_bind_params(eqn.params)
1405
+ outvals = eqn.primitive.bind(*invals, **bind_params)
1406
+ if not eqn.primitive.multiple_results:
1407
+ outvals = [outvals]
1408
+ return outvals
1409
+ rule = _RULE_REGISTRY.get(eqn.primitive)
1410
+ if rule is None:
1411
+ raise NotImplementedError(
1412
+ f"custom_lap: no rule for primitive {eqn.primitive.name!r}. "
1413
+ f"eqn = {eqn}. Register a rule in energy/custom_lap.py."
1414
+ )
1415
+ outvals = rule(invals, eqn.params, W_levels)
1416
+ if not eqn.primitive.multiple_results:
1417
+ outvals = [outvals]
1418
+ return outvals
1419
+
1420
+
1421
+ def _trace_z(fn, z, N, W_levels):
1422
+
1423
+ z = jnp.asarray(z)
1424
+ assert z.shape == (N, 3), f"z must be [N={N}, 3]; got {z.shape}"
1425
+ eye3 = jnp.eye(3, dtype=z.dtype)
1426
+ z_jac = jnp.broadcast_to(eye3[:, None, :], (3, N, 3))
1427
+ z_lap = jnp.zeros((N, 3), dtype=z.dtype)
1428
+ z_jlp = JLP(
1429
+ value=z,
1430
+ jac=z_jac,
1431
+ lap=z_lap,
1432
+ level=1,
1433
+ has_chunk_axis=True,
1434
+ chunk_idx=-1,
1435
+ )
1436
+ closed = jax.make_jaxpr(fn)(z)
1437
+ jaxpr = closed.jaxpr
1438
+ consts = closed.consts
1439
+ env: dict = {}
1440
+ for cv, c in zip(jaxpr.constvars, consts):
1441
+ env[cv] = c
1442
+ env[jaxpr.invars[0]] = z_jlp
1443
+ for eqn in jaxpr.eqns:
1444
+ outvals = _eval_eqn(eqn, env, W_levels)
1445
+ for ov, ov_val in zip(eqn.outvars, outvals):
1446
+ env[ov] = ov_val
1447
+ return [env[ov] for ov in jaxpr.outvars]
1448
+
1449
+
1450
+ def _canonical_jac(jlp: JLP):
1451
+
1452
+ if jlp.jac.shape[1] != 1:
1453
+ jac = jnp.swapaxes(jlp.jac, 0, 1)
1454
+ return jac.reshape((jlp.jac.shape[0] * jlp.jac.shape[1],) + jlp.jac.shape[2:])
1455
+ return jlp.jac[:, 0]
1456
+
1457
+
1458
+ def custom_forward_laplacian_with_jac(fn: Callable, W_levels: list, N: int) -> Callable:
1459
+
1460
+ def lap_jac_fn(z):
1461
+ outs = _trace_z(fn, z, N, W_levels)
1462
+ if len(outs) == 1:
1463
+ out = outs[0]
1464
+ if _is_jlp(out):
1465
+ return out.value, _canonical_jac(out), out.lap
1466
+ value = out
1467
+ return (
1468
+ value,
1469
+ jnp.zeros((3 * N,) + value.shape, dtype=value.dtype),
1470
+ jnp.zeros_like(value),
1471
+ )
1472
+ values = tuple(o.value if _is_jlp(o) else o for o in outs)
1473
+ jacs = tuple(
1474
+ _canonical_jac(o)
1475
+ if _is_jlp(o)
1476
+ else jnp.zeros((3 * N,) + o.shape, dtype=o.dtype)
1477
+ for o in outs
1478
+ )
1479
+ laps = tuple(o.lap if _is_jlp(o) else jnp.zeros_like(o) for o in outs)
1480
+ return values, jacs, laps
1481
+
1482
+ return lap_jac_fn
1483
+
1484
+
1485
+ __all__ = [
1486
+ "JLP",
1487
+ "build_W_levels",
1488
+ "custom_forward_laplacian_with_jac",
1489
+ "custom_lap_active",
1490
+ "use_custom_lap",
1491
+ ]
src/hamiltonzero/energy/frame.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Sequence
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ import numpy as np
12
+ from jaxtyping import Array
13
+
14
+ from hamiltonzero.compiled.types import EnergyFrame, EnergyInputs, EnergyMasks
15
+ from hamiltonzero.energy.custom_lap import build_W_levels
16
+
17
+
18
+ def _real_dtype(dtype):
19
+ return jnp.real(jnp.zeros((), dtype)).dtype
20
+
21
+
22
+ def _host_eigvalsh(x):
23
+ out_dtype = _real_dtype(x.dtype)
24
+ out_shape = jax.ShapeDtypeStruct(x.shape[:-1], out_dtype)
25
+
26
+ def callback(a):
27
+ values = np.linalg.eigvalsh(np.asarray(a))
28
+ return values.astype(np.dtype(out_dtype))
29
+
30
+ return jax.pure_callback(
31
+ callback,
32
+ out_shape,
33
+ x,
34
+ vmap_method="sequential",
35
+ )
36
+
37
+
38
+ def _compute_custom_lap_views_batched(J_full, mask, mu, eps):
39
+ n = J_full.shape[1]
40
+ n_systems = J_full.shape[0]
41
+ dtype = J_full.dtype
42
+ J_matrix = jnp.transpose(J_full, (0, 1, 3, 2, 4)).reshape(n_systems, 3 * n, 3 * n)
43
+ J_matrix_symmetric = 0.5 * (J_matrix + jnp.swapaxes(J_matrix, -1, -2))
44
+ mask_3 = jnp.repeat(mask.astype(dtype), 3, axis=-1)
45
+ eye_3n = jnp.eye(3 * n, dtype=dtype)
46
+ masked_identity = eye_3n[None] * mask_3[:, None, :]
47
+ J_eff = (J_matrix_symmetric - (mu + eps)[:, None, None] * masked_identity) / 4.0
48
+ J_eff = 0.5 * (J_eff + jnp.swapaxes(J_eff, -1, -2))
49
+
50
+ eigh_epsilon = jnp.asarray(1e-6, dtype=_real_dtype(dtype))
51
+ eigenvalues = _host_eigvalsh(J_eff + eigh_epsilon * eye_3n[None]) - eigh_epsilon
52
+ lam_max = jnp.max(eigenvalues, axis=-1)
53
+ delta_mu = jax.nn.relu(4.0 * (lam_max + eps))
54
+ shift = delta_mu / 4.0
55
+ J_eff = J_eff - shift[:, None, None] * eye_3n[None]
56
+ mu_eff = mu + delta_mu
57
+ casimir_per_site = (mu_eff + eps) * jnp.asarray(0.75, dtype=dtype)
58
+ radial_const = -casimir_per_site * mask.astype(dtype).sum(axis=-1)
59
+ return J_eff, radial_const
60
+
61
+
62
+ def _compute_custom_lap_views(J_full, mask, mu, eps):
63
+ values = _compute_custom_lap_views_batched(
64
+ J_full[None],
65
+ mask[None],
66
+ jnp.asarray(mu)[None],
67
+ jnp.asarray(eps)[None],
68
+ )
69
+ return tuple(value[0] for value in values)
70
+
71
+
72
+ def build_energy_inputs(J_full, h, mask, mu, eps) -> EnergyInputs:
73
+ dtype = J_full.dtype
74
+ mu_array = jnp.asarray(mu, dtype=dtype)
75
+ eps_array = jnp.asarray(eps, dtype=dtype)
76
+ J_eff, radial_const = _compute_custom_lap_views(
77
+ J_full,
78
+ mask,
79
+ mu_array,
80
+ eps_array,
81
+ )
82
+ return EnergyInputs(
83
+ custom_lap_J_eff=J_eff,
84
+ custom_lap_radial_const=radial_const,
85
+ one_body_fields=(h,),
86
+ )
87
+
88
+
89
+ def block_permutation3(perm: Array) -> Array:
90
+
91
+ components = jnp.arange(3, dtype=perm.dtype)
92
+ return (perm[:, None] * 3 + components[None, :]).reshape(-1)
93
+
94
+
95
+ def route_one_body_field(field: Array, perm: Array) -> Array:
96
+
97
+ return jnp.take(field, perm, axis=0)
98
+
99
+
100
+ def route_one_body_fields(
101
+ fields: Sequence[Array],
102
+ perm: Array,
103
+ ) -> tuple[Array, ...]:
104
+
105
+ return tuple(route_one_body_field(field, perm) for field in fields)
106
+
107
+
108
+ def route_J_eff(J_eff: Array, perm: Array) -> Array:
109
+
110
+ block_perm = block_permutation3(perm)
111
+ return jnp.take(jnp.take(J_eff, block_perm, axis=0), block_perm, axis=1)
112
+
113
+
114
+ def route_energy_inputs(energy_inputs: EnergyInputs, perm: Array) -> EnergyInputs:
115
+ return EnergyInputs(
116
+ custom_lap_J_eff=route_J_eff(energy_inputs.custom_lap_J_eff, perm),
117
+ custom_lap_radial_const=energy_inputs.custom_lap_radial_const,
118
+ one_body_fields=route_one_body_fields(energy_inputs.one_body_fields, perm),
119
+ )
120
+
121
+
122
+ def compile_energy_frame(
123
+ energy_inputs: EnergyInputs,
124
+ real_mask: Array,
125
+ balanced_mask: Array,
126
+ perm: Array,
127
+ ) -> EnergyFrame:
128
+
129
+ J_eff = route_J_eff(energy_inputs.custom_lap_J_eff, perm)
130
+ n_sites = int(perm.shape[0])
131
+ frame = EnergyFrame(
132
+ custom_lap_J_eff=J_eff,
133
+ w_levels=tuple(build_W_levels(J_eff, n_sites)),
134
+ custom_lap_radial_const=energy_inputs.custom_lap_radial_const,
135
+ one_body_fields=route_one_body_fields(energy_inputs.one_body_fields, perm),
136
+ masks=EnergyMasks(
137
+ real=route_one_body_field(real_mask, perm),
138
+ balanced=balanced_mask,
139
+ ),
140
+ )
141
+ return frame
142
+
143
+
144
+ __all__ = [
145
+ "block_permutation3",
146
+ "build_energy_inputs",
147
+ "compile_energy_frame",
148
+ "route_energy_inputs",
149
+ "route_J_eff",
150
+ "route_one_body_field",
151
+ "route_one_body_fields",
152
+ ]
src/hamiltonzero/energy/kernel.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Callable
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ from jaxtyping import Array, Complex, Float
12
+
13
+ from hamiltonzero.compiled.execute import execute_wavefunction
14
+ from hamiltonzero.energy.custom_lap import (
15
+ custom_forward_laplacian_with_jac,
16
+ use_custom_lap,
17
+ )
18
+
19
+
20
+ def _right_su2_chart_jet(
21
+ q: Float[Array, "N 4"],
22
+ z: Float[Array, "N 3"],
23
+ ) -> Float[Array, "N 4"]:
24
+
25
+ q0, q1, q2, q3 = q[:, 0], q[:, 1], q[:, 2], q[:, 3]
26
+ zx, zy, zz = z[:, 0], z[:, 1], z[:, 2]
27
+ u0 = 1.0 - 0.5 * (zx * zx + zy * zy + zz * zz)
28
+ u1, u2, u3 = zz, zy, zx
29
+ return jnp.stack(
30
+ [
31
+ q0 * u0 - q1 * u1 - q2 * u2 - q3 * u3,
32
+ q0 * u1 + q1 * u0 + q2 * u3 - q3 * u2,
33
+ q0 * u2 - q1 * u3 + q2 * u0 + q3 * u1,
34
+ q0 * u3 + q1 * u2 - q2 * u1 + q3 * u0,
35
+ ],
36
+ axis=-1,
37
+ )
38
+
39
+
40
+ def _custom_lap_single_finetune(
41
+ model: Callable,
42
+ q: Float[Array, "N 4"],
43
+ t,
44
+ energy_frame: Any,
45
+ ):
46
+ if len(energy_frame.one_body_fields) != 1:
47
+ raise ValueError("custom-Laplacian energy requires exactly one one-body field")
48
+ return _custom_lap_single_from_frame(
49
+ model,
50
+ None,
51
+ q,
52
+ t,
53
+ J_eff=energy_frame.custom_lap_J_eff,
54
+ W_levels=energy_frame.w_levels,
55
+ field_xyz=energy_frame.one_body_fields[0],
56
+ radial_const=energy_frame.custom_lap_radial_const,
57
+ )
58
+
59
+
60
+ def _custom_lap_single_prebuilt(
61
+ model: Callable,
62
+ q: Float[Array, "N 4"],
63
+ t,
64
+ frame: Any,
65
+ ):
66
+ if len(frame.one_body_fields) != 1:
67
+ raise ValueError("custom-Laplacian energy requires exactly one one-body field")
68
+ return _custom_lap_single_from_frame(
69
+ model,
70
+ None,
71
+ q,
72
+ t,
73
+ J_eff=frame.custom_lap_J_eff,
74
+ W_levels=frame.w_levels,
75
+ field_xyz=frame.one_body_fields[0],
76
+ radial_const=frame.custom_lap_radial_const,
77
+ )
78
+
79
+
80
+ def _custom_lap_single_from_frame(
81
+ model: Callable,
82
+ ctx: Any,
83
+ q: Float[Array, "N 4"],
84
+ t,
85
+ *,
86
+ J_eff,
87
+ W_levels,
88
+ field_xyz,
89
+ radial_const,
90
+ ):
91
+ N = q.shape[0]
92
+
93
+ def f_entry(z):
94
+ q_pert = _right_su2_chart_jet(q, z)
95
+ re, im = model(q_pert, ctx, t)
96
+ return jnp.stack([re, im])
97
+
98
+ with use_custom_lap():
99
+ _value, jac_pair, lap_pair = custom_forward_laplacian_with_jac(
100
+ f_entry,
101
+ W_levels,
102
+ N,
103
+ )(jnp.zeros((N, 3), dtype=q.dtype))
104
+
105
+ tr_total = lap_pair[0] + 1j * lap_pair[1]
106
+
107
+ g_lie = jac_pair[:, 0] + 1j * jac_pair[:, 1]
108
+ quad_total = jnp.einsum("a,ab,b->", g_lie, J_eff.astype(g_lie.dtype), g_lie)
109
+
110
+ la_xyz = 0.5 * g_lie.reshape(N, 3)
111
+ field = (1j * jnp.einsum("ic,ic->", field_xyz.astype(g_lie.dtype), la_xyz)).astype(
112
+ g_lie.dtype
113
+ )
114
+
115
+ total = tr_total + quad_total + radial_const.astype(g_lie.dtype) + field
116
+
117
+ zero = jnp.zeros_like(total)
118
+ exchange = total - field
119
+ return total, exchange, zero, field
120
+
121
+
122
+ def _vmc_energy_custom_lap_finetune(
123
+ model: Callable,
124
+ energy_frame: Any,
125
+ q: Float[Array, "... N 4"],
126
+ t=0.0,
127
+ *,
128
+ chunk_size: int | None = None,
129
+ ) -> tuple[
130
+ Complex[Array, "..."],
131
+ Complex[Array, "..."],
132
+ Complex[Array, "..."],
133
+ Complex[Array, "..."],
134
+ ]:
135
+
136
+ def single(qq, tt):
137
+ return _custom_lap_single_finetune(model, qq, tt, energy_frame)
138
+
139
+ return _run_custom_lap_batch(single, q, t, chunk_size)
140
+
141
+
142
+ def _vmc_energy_custom_lap_prebuilt(
143
+ kernel: Any,
144
+ tree: Any,
145
+ energy_frame: Any,
146
+ q: Float[Array, "... N 4"],
147
+ *,
148
+ chunk_size: int | None = None,
149
+ ) -> tuple[
150
+ Complex[Array, "..."],
151
+ Complex[Array, "..."],
152
+ Complex[Array, "..."],
153
+ Complex[Array, "..."],
154
+ ]:
155
+ def model(q_pert, _ctx, _t):
156
+ return execute_wavefunction(kernel, tree, q_pert)
157
+
158
+ def single(qq, tt):
159
+ return _custom_lap_single_prebuilt(model, qq, tt, energy_frame)
160
+
161
+ return _run_custom_lap_batch(single, q, 0.0, chunk_size)
162
+
163
+
164
+ def _run_custom_lap_batch(single, q, t, chunk_size):
165
+ n_sites, n_dims = q.shape[-2], q.shape[-1]
166
+ assert n_dims == 4, f"expected quaternion last dim 4, got {n_dims}"
167
+ lead = q.shape[:-2]
168
+ n_items = 1
169
+ for d in lead:
170
+ n_items *= d
171
+
172
+ q_flat = q.reshape(n_items, n_sites, 4)
173
+ t_arr = jnp.asarray(t, dtype=q.dtype)
174
+ t_bcast = jnp.broadcast_to(t_arr, lead if lead else ())
175
+ t_flat = t_bcast.reshape(n_items) if lead else jnp.broadcast_to(t_arr, (n_items,))
176
+
177
+ with jax.default_matmul_precision("highest"):
178
+ if chunk_size is None or chunk_size >= n_items:
179
+ total, exchange, casimir, field = jax.vmap(single)(q_flat, t_flat)
180
+ else:
181
+ total, exchange, casimir, field = jax.lax.map(
182
+ lambda x: single(x[0], x[1]),
183
+ (q_flat, t_flat),
184
+ batch_size=chunk_size,
185
+ )
186
+
187
+ out_shape = lead if lead else ()
188
+ return (
189
+ total.reshape(out_shape),
190
+ exchange.reshape(out_shape),
191
+ casimir.reshape(out_shape),
192
+ field.reshape(out_shape),
193
+ )
194
+
195
+
196
+ __all__ = []
src/hamiltonzero/evaluation/__init__.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .backend import (
5
+ BeamCandidates,
6
+ CanonicalContext,
7
+ EvalBackend,
8
+ LargeNCompilation,
9
+ MCMCPopulation,
10
+ )
11
+ from .runner import evaluate
12
+ from .runtime import DefaultEvalBackend, build_eval_backend
13
+ from .statistics import (
14
+ ChannelMetrics,
15
+ EnergyWindow,
16
+ P01_TWO_SIDED,
17
+ select_winner_per_physical,
18
+ standard_error_from_tailstd,
19
+ welch_band_walkermean,
20
+ )
21
+ from .types import ContestCandidate, ContestResult, EvalMetric, EvalResult
22
+
23
+ __all__ = [
24
+ "BeamCandidates",
25
+ "CanonicalContext",
26
+ "ChannelMetrics",
27
+ "ContestCandidate",
28
+ "ContestResult",
29
+ "DefaultEvalBackend",
30
+ "EnergyWindow",
31
+ "EvalBackend",
32
+ "EvalMetric",
33
+ "EvalResult",
34
+ "LargeNCompilation",
35
+ "MCMCPopulation",
36
+ "P01_TWO_SIDED",
37
+ "evaluate",
38
+ "build_eval_backend",
39
+ "select_winner_per_physical",
40
+ "standard_error_from_tailstd",
41
+ "welch_band_walkermean",
42
+ ]
src/hamiltonzero/evaluation/backend.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Protocol
9
+
10
+ from hamiltonzero.config import EnergyConfig, EvalMCMCConfig, ModelConfig
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class CanonicalContext:
15
+ context: Any
16
+ old_inverse: Any
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class BeamCandidates:
21
+ permutations: Any
22
+ log_probabilities: Any
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class LargeNCompilation:
27
+ wavefunction: Any
28
+ permutation: Any
29
+ log_probability: Any
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class MCMCPopulation:
34
+ q: Any
35
+ sigma: Any
36
+ beta: Any
37
+
38
+
39
+ class EvalBackend(Protocol):
40
+ def load_system(self, path: Path, energy: EnergyConfig) -> Any: ...
41
+
42
+ def load_model(
43
+ self,
44
+ checkpoint: Path,
45
+ config: ModelConfig,
46
+ key: Any,
47
+ context: Any,
48
+ *,
49
+ contextualizer_attention: str | None,
50
+ ) -> Any: ...
51
+
52
+ def canonicalize_context(self, context: Any) -> CanonicalContext: ...
53
+
54
+ def embedded_route(self, model: Any) -> Any | None: ...
55
+
56
+ def route_context(
57
+ self,
58
+ context: Any,
59
+ permutation: Any,
60
+ *,
61
+ compact_custom_lap: bool,
62
+ ) -> Any: ...
63
+
64
+ def release_context(self, context: Any) -> None: ...
65
+
66
+ def virtual_context(self, context: Any, permutations: Any) -> Any: ...
67
+
68
+ def beam_candidates(
69
+ self,
70
+ model: Any,
71
+ context: Any,
72
+ *,
73
+ beam_width: int,
74
+ top_k: int,
75
+ temperature: float,
76
+ ) -> BeamCandidates: ...
77
+
78
+ def compile_single(self, model: Any, routed_context: Any) -> Any: ...
79
+
80
+ def compile_embedded(self, model: Any) -> Any: ...
81
+
82
+ def compile_candidates(
83
+ self,
84
+ model: Any,
85
+ canonical_context: Any,
86
+ permutations: Any,
87
+ ) -> Any: ...
88
+
89
+ def select_candidate(self, wavefunctions: Any, winner: int) -> Any: ...
90
+
91
+ def compile_large_n(
92
+ self,
93
+ model: Any,
94
+ canonical_context: Any,
95
+ *,
96
+ sequence_shards: int,
97
+ pair_tile_size: int,
98
+ temperature: float,
99
+ ) -> LargeNCompilation: ...
100
+
101
+ def prepare_singular(
102
+ self,
103
+ model: Any,
104
+ context: Any,
105
+ state: Any,
106
+ ) -> tuple[Any, Any, Any]: ...
107
+
108
+ def initialize_mcmc(
109
+ self,
110
+ key: Any,
111
+ model: Any,
112
+ context: Any,
113
+ config: EvalMCMCConfig,
114
+ ) -> Any: ...
115
+
116
+ def step_mcmc(
117
+ self,
118
+ state: Any,
119
+ model: Any,
120
+ context: Any,
121
+ *,
122
+ replica_steps: int,
123
+ walker_chunk_size: int,
124
+ ) -> Any: ...
125
+
126
+ def adapt_mcmc(self, state: Any, config: EvalMCMCConfig) -> Any: ...
127
+
128
+ def route_mcmc(self, state: Any, permutation: Any) -> Any: ...
129
+
130
+ def mcmc_population(self, state: Any) -> MCMCPopulation: ...
131
+
132
+ def replace_mcmc_population(
133
+ self,
134
+ state: Any,
135
+ population: MCMCPopulation,
136
+ ) -> Any: ...
137
+
138
+ def cold_walkers(self, state: Any) -> Any: ...
139
+
140
+ def custom_lap_energy(
141
+ self,
142
+ model: Any,
143
+ context: Any,
144
+ q: Any,
145
+ config: EnergyConfig,
146
+ ) -> tuple[Any, Any, Any, Any]: ...
147
+
148
+ def block_until_ready(self, value: Any) -> None: ...
src/hamiltonzero/evaluation/greedy_router.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable
8
+ from functools import partial
9
+
10
+ import jax
11
+ from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
12
+
13
+ from hamiltonzero.model.route_pointer import TreePrefixPointerMHSEA
14
+
15
+
16
+ def _ring_permute_rows_local(values, route_ids, *, axis_size: int):
17
+
18
+ local_rows = values.shape[0]
19
+ lane = jax.lax.axis_index("seq").astype(jax.numpy.int32)
20
+ local_ids = jax.lax.dynamic_slice_in_dim(
21
+ route_ids,
22
+ lane * local_rows,
23
+ local_rows,
24
+ axis=0,
25
+ )
26
+ owner = local_ids // local_rows
27
+ within_owner = local_ids % local_rows
28
+ output0 = jax.numpy.zeros_like(values)
29
+
30
+ def take_from(panel, origin, output):
31
+ selected = panel[within_owner]
32
+ mask = owner == origin
33
+ while mask.ndim < selected.ndim:
34
+ mask = mask[..., None]
35
+ return jax.numpy.where(mask, selected, output)
36
+
37
+ origin0 = lane
38
+ output0 = take_from(values, origin0, output0)
39
+ permutation = [(i, (i + 1) % axis_size) for i in range(axis_size)]
40
+
41
+ def step(carry, _):
42
+ panel, origin, output = carry
43
+ panel = jax.lax.ppermute(panel, "seq", permutation)
44
+ origin = (origin - jax.numpy.asarray(1, jax.numpy.int32)) % axis_size
45
+ output = take_from(panel, origin, output)
46
+ return (panel, origin, output), None
47
+
48
+ (_, _, output), _ = jax.lax.scan(
49
+ step,
50
+ (values, origin0, output0),
51
+ xs=None,
52
+ length=axis_size - 1,
53
+ )
54
+ return output
55
+
56
+
57
+ def _make_row_permute(mesh: Mesh) -> Callable:
58
+ axis_size = int(mesh.shape["seq"])
59
+ spec = P("seq", None, None)
60
+ if axis_size == 1:
61
+ return lambda values, route_ids: values[route_ids]
62
+ return jax.shard_map(
63
+ partial(_ring_permute_rows_local, axis_size=axis_size),
64
+ mesh=mesh,
65
+ in_specs=(spec, P()),
66
+ out_specs=spec,
67
+ check_vma=False,
68
+ )
69
+
70
+
71
+ def build_compact_greedy_router(
72
+ *,
73
+ mesh: Mesh,
74
+ decoder_template: TreePrefixPointerMHSEA,
75
+ pair_tile_size: int = 128,
76
+ ) -> Callable:
77
+
78
+ if tuple(mesh.axis_names) != ("seq",):
79
+ raise ValueError("compact greedy router requires a one-dimensional 'seq' mesh")
80
+ if int(mesh.shape["seq"]) < 1:
81
+ raise ValueError("compact greedy router requires at least one seq lane")
82
+ if not isinstance(decoder_template, TreePrefixPointerMHSEA):
83
+ raise TypeError("decoder_template must be TreePrefixPointerMHSEA")
84
+ if isinstance(pair_tile_size, bool) or int(pair_tile_size) < 1:
85
+ raise ValueError("pair_tile_size must be a positive integer")
86
+
87
+ rep = NamedSharding(mesh, P())
88
+ seq_vec = NamedSharding(mesh, P("seq", None))
89
+ seq_edge = NamedSharding(mesh, P("seq", None, None))
90
+ decoder_rep = jax.tree_util.tree_map(lambda _leaf: rep, decoder_template)
91
+ row_permute = _make_row_permute(mesh)
92
+
93
+ def decode(decoder, h, edge, mask, global_feat, tau, real_mask):
94
+
95
+ h = jax.lax.with_sharding_constraint(h, seq_vec)
96
+ edge = jax.lax.with_sharding_constraint(edge, seq_edge)
97
+ perm, logp = decoder._decode_greedy_compact(
98
+ h,
99
+ edge,
100
+ mask,
101
+ global_feat=global_feat,
102
+ tau=tau,
103
+ real_mask=real_mask,
104
+ sequence_mesh=mesh,
105
+ pair_tile_size=int(pair_tile_size),
106
+ row_permute_fn=row_permute,
107
+ )
108
+ return perm, logp
109
+
110
+ return jax.jit(
111
+ decode,
112
+ in_shardings=(
113
+ decoder_rep,
114
+ seq_vec,
115
+ seq_edge,
116
+ rep,
117
+ rep,
118
+ rep,
119
+ rep,
120
+ ),
121
+ out_shardings=(rep, rep),
122
+ )
123
+
124
+
125
+ __all__ = ["build_compact_greedy_router"]
src/hamiltonzero/evaluation/large_n.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ import gc
8
+ from typing import NamedTuple
9
+
10
+ import jax
11
+ import jax.numpy as jnp
12
+ import numpy as np
13
+ from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
14
+
15
+ from hamiltonzero.compiled.tree import bind_physical_compiler_kernel
16
+ from hamiltonzero.compiled.trunk import (
17
+ bind_shared_kernel,
18
+ bind_trunk_compiler_kernel,
19
+ )
20
+ from hamiltonzero.compiled.types import CompiledWaveFunction
21
+ from hamiltonzero.model.route_pointer import TreePrefixPointerMHSEA
22
+ from .sequence_trunk import (
23
+ build_sequence_pair_permute,
24
+ build_sequence_parallel_contextualizer,
25
+ build_sequence_parallel_edge_global_update,
26
+ build_sequence_parallel_physical_leaf,
27
+ build_sequence_parallel_physical_reducer,
28
+ build_sequence_parallel_shared_trunk,
29
+ )
30
+ from .greedy_router import build_compact_greedy_router
31
+
32
+
33
+ class LargeNCompiledEvalResult(NamedTuple):
34
+ wavefunction: CompiledWaveFunction
35
+ perm: jax.Array
36
+ logp: jax.Array
37
+
38
+
39
+ def _sequence_mesh(n: int, requested_shards: int) -> Mesh:
40
+ devices = tuple(jax.devices())
41
+ shards = len(devices) if requested_shards == 0 else requested_shards
42
+ if shards < 1 or shards > len(devices):
43
+ raise ValueError(
44
+ f"compiled eval requested {shards} seq shards, but JAX exposes "
45
+ f"{len(devices)} devices"
46
+ )
47
+ if n % shards:
48
+ raise ValueError(f"N={n} must be divisible by seq_shards={shards}")
49
+ local_rows = n // shards
50
+ if n & (n - 1) or local_rows & (local_rows - 1):
51
+ raise ValueError(
52
+ "large-N physical compilation requires a power-of-two padded "
53
+ f"width and power-of-two rows per lane; got N={n}, "
54
+ f"local_rows={local_rows}"
55
+ )
56
+ return Mesh(np.asarray(devices[:shards], dtype=object), ("seq",))
57
+
58
+
59
+ def _replicate(tree, sharding: NamedSharding):
60
+ return jax.device_put(tree, jax.tree_util.tree_map(lambda _leaf: sharding, tree))
61
+
62
+
63
+ class _LargeNXlaTreePrefixPointer(TreePrefixPointerMHSEA):
64
+ def _resolve_heavy_attn_impl(self, n: int):
65
+ del n
66
+ return None
67
+
68
+ def _resolve_tree_attn_impl(self):
69
+ return None
70
+
71
+ def _route_attention(
72
+ self,
73
+ q,
74
+ k,
75
+ v,
76
+ edge_bias,
77
+ key_mask,
78
+ *,
79
+ impl,
80
+ key_mask_only=False,
81
+ attention_mask=None,
82
+ sequence_axis_name=None,
83
+ sequence_mesh=None,
84
+ ):
85
+ del impl, key_mask_only
86
+ dtype = q.dtype
87
+ valid = key_mask.astype(bool)[:, None, :]
88
+ if attention_mask is not None:
89
+ valid = valid & attention_mask.astype(bool)
90
+ has_key = jnp.any(valid, axis=-1)
91
+ q_c = q.astype(jnp.float32)
92
+ k_c = k.astype(jnp.float32)
93
+ v_c = v.astype(jnp.float32)
94
+ bias_c = edge_bias.astype(jnp.float32)
95
+ if sequence_axis_name is not None:
96
+
97
+ def sharding(*axes):
98
+ spec = P(*axes)
99
+ return (
100
+ NamedSharding(sequence_mesh, spec)
101
+ if sequence_mesh is not None
102
+ else spec
103
+ )
104
+
105
+ q_c = jax.lax.with_sharding_constraint(
106
+ q_c, sharding(None, sequence_axis_name, None, None)
107
+ )
108
+ k_c = jax.lax.with_sharding_constraint(
109
+ k_c, sharding(None, None, None, None)
110
+ )
111
+ v_c = jax.lax.with_sharding_constraint(
112
+ v_c, sharding(None, None, None, None)
113
+ )
114
+ bias_c = jax.lax.with_sharding_constraint(
115
+ bias_c, sharding(None, sequence_axis_name, None, None)
116
+ )
117
+ logits = jnp.einsum("bihd,bjhd->bhij", q_c, k_c)
118
+ logits = logits / jnp.sqrt(jnp.asarray(self.d_head, dtype=jnp.float32))
119
+ logits = logits + jnp.transpose(bias_c, (0, 3, 1, 2))
120
+ if sequence_axis_name is not None:
121
+ logits = jax.lax.with_sharding_constraint(
122
+ logits, sharding(None, None, sequence_axis_name, None)
123
+ )
124
+ logits = jnp.where(
125
+ valid[:, None, :, :],
126
+ logits,
127
+ jnp.asarray(-1.0e30, dtype=jnp.float32),
128
+ )
129
+ if sequence_axis_name is not None:
130
+ logits = jax.lax.with_sharding_constraint(
131
+ logits, sharding(None, None, sequence_axis_name, None)
132
+ )
133
+ alpha = jax.nn.softmax(logits, axis=-1)
134
+ if sequence_axis_name is not None:
135
+ alpha = jax.lax.with_sharding_constraint(
136
+ alpha, sharding(None, None, sequence_axis_name, None)
137
+ )
138
+ out = jnp.einsum("bhij,bjhd->bihd", alpha, v_c)
139
+ if sequence_axis_name is not None:
140
+ out = jax.lax.with_sharding_constraint(
141
+ out, sharding(None, sequence_axis_name, None, None)
142
+ )
143
+ out = out.astype(dtype)
144
+ return jnp.where(has_key[..., None, None], out, jnp.zeros_like(out))
145
+
146
+
147
+ def _large_n_xla_decoder_view(decoder: TreePrefixPointerMHSEA):
148
+ compiled = object.__new__(_LargeNXlaTreePrefixPointer)
149
+ compiled.__dict__.update(decoder.__dict__)
150
+ return compiled
151
+
152
+
153
+ def _validate_large_n_model(model) -> None:
154
+ decoder = getattr(model, "route_decoder", None)
155
+ failures: list[str] = []
156
+ if not isinstance(decoder, TreePrefixPointerMHSEA):
157
+ failures.append("route decoder must be TreePrefixPointerMHSEA")
158
+ if getattr(model, "route_contextualizer", None) is None:
159
+ failures.append("route contextualizer must be enabled")
160
+ if getattr(model, "gladder_fork_route", None) is None:
161
+ failures.append("route global fork must be enabled")
162
+ if getattr(model, "readout_leaf_context", None) is None:
163
+ failures.append("physical contextualizer must be enabled")
164
+ if failures:
165
+ raise ValueError(
166
+ "unsupported large-N eval compiler model: " + "; ".join(failures)
167
+ )
168
+
169
+
170
+ def _validate_context(ctx) -> int:
171
+ jdp = jnp.asarray(ctx.J_double_prime)
172
+ mask = jnp.asarray(ctx.mask)
173
+ bmask = jnp.asarray(ctx.bmask)
174
+ h_prime = jnp.asarray(ctx.h_prime)
175
+ if jdp.ndim != 3 or jdp.shape[-1] != 10:
176
+ raise ValueError("ctx.J_double_prime must have shape [N,N,10]")
177
+ n = int(jdp.shape[0])
178
+ if jdp.shape[1] != n:
179
+ raise ValueError("ctx.J_double_prime pair axes must be square")
180
+ if mask.shape != (n,) or bmask.shape != (n,):
181
+ raise ValueError("ctx.mask and ctx.bmask must both have shape [N]")
182
+ if h_prime.shape != (n, 3):
183
+ raise ValueError("ctx.h_prime must have shape [N,3]")
184
+ if jdp.dtype != jnp.float32 or h_prime.dtype != jnp.float32:
185
+ raise TypeError(
186
+ "large-N eval keeps streamed pair/frontier arithmetic in fp32; "
187
+ f"got J={jdp.dtype}, h={h_prime.dtype}"
188
+ )
189
+ return n
190
+
191
+
192
+ def compile_large_n_eval_wavefunction(
193
+ model,
194
+ ctx,
195
+ *,
196
+ seq_shards: int = 0,
197
+ pair_tile_size: int = 128,
198
+ tau: float = 1.0,
199
+ ) -> LargeNCompiledEvalResult:
200
+
201
+ _validate_large_n_model(model)
202
+ n = _validate_context(ctx)
203
+ if pair_tile_size < 1:
204
+ raise ValueError("pair_tile_size must be positive")
205
+ mesh = _sequence_mesh(n, int(seq_shards))
206
+ rep = NamedSharding(mesh, P())
207
+ seq_edge = NamedSharding(mesh, P("seq", None, None))
208
+
209
+ trunk_kernel = _replicate(bind_trunk_compiler_kernel(model), rep)
210
+ route_contextualizer = _replicate(model.route_contextualizer, rep)
211
+ route_global_fork = _replicate(model.gladder_fork_route, rep)
212
+ decoder = _replicate(_large_n_xla_decoder_view(model.route_decoder), rep)
213
+ physical_kernel = _replicate(bind_physical_compiler_kernel(model), rep)
214
+
215
+ jdp = jax.device_put(jnp.asarray(ctx.J_double_prime), seq_edge)
216
+ h_prime, real_mask, structural_mask = jax.device_put(
217
+ (
218
+ jnp.asarray(ctx.h_prime),
219
+ jnp.asarray(ctx.mask),
220
+ jnp.asarray(ctx.bmask),
221
+ ),
222
+ rep,
223
+ )
224
+
225
+ trunk_entry = build_sequence_parallel_shared_trunk(
226
+ mesh=mesh,
227
+ kernel_template=trunk_kernel,
228
+ featurizer_tile_size=int(pair_tile_size),
229
+ )
230
+ trunk = trunk_entry(trunk_kernel, jdp, h_prime, real_mask, structural_mask)
231
+
232
+ route_context_entry = build_sequence_parallel_contextualizer(
233
+ mesh=mesh,
234
+ contextualizer_template=route_contextualizer,
235
+ g_template=trunk.global_stream,
236
+ tile_size=int(pair_tile_size),
237
+ )
238
+ route_node, route_edge, route_g = route_context_entry(
239
+ route_contextualizer,
240
+ trunk.node_raw,
241
+ trunk.edge_raw,
242
+ real_mask,
243
+ structural_mask,
244
+ trunk.global_stream,
245
+ )
246
+ route_global_entry = build_sequence_parallel_edge_global_update(
247
+ mesh=mesh,
248
+ module_template=route_global_fork,
249
+ tile_size=int(pair_tile_size),
250
+ )
251
+ route_global = route_global_entry(
252
+ route_global_fork, route_g, route_edge, structural_mask
253
+ )
254
+
255
+ route_entry = build_compact_greedy_router(
256
+ mesh=mesh,
257
+ decoder_template=decoder,
258
+ pair_tile_size=int(pair_tile_size),
259
+ )
260
+ perm, logp = route_entry(
261
+ decoder,
262
+ route_node,
263
+ route_edge,
264
+ structural_mask,
265
+ route_global,
266
+ jnp.asarray(tau, dtype=jnp.float32),
267
+ real_mask,
268
+ )
269
+
270
+ jax.block_until_ready((perm, logp))
271
+ del route_node, route_edge, route_g, route_global
272
+
273
+ pair_permute = build_sequence_pair_permute(mesh=mesh)
274
+ routed_node, routed_edge = pair_permute(trunk.node_raw, trunk.edge_raw, perm)
275
+ leaf_real = real_mask[perm]
276
+ global_stream = trunk.global_stream
277
+
278
+ jax.block_until_ready((routed_node, routed_edge, leaf_real, global_stream))
279
+ del (
280
+ trunk,
281
+ trunk_kernel,
282
+ route_contextualizer,
283
+ route_global_fork,
284
+ decoder,
285
+ jdp,
286
+ h_prime,
287
+ real_mask,
288
+ trunk_entry,
289
+ route_context_entry,
290
+ route_global_entry,
291
+ route_entry,
292
+ pair_permute,
293
+ )
294
+ gc.collect()
295
+
296
+ physical_context_entry = build_sequence_parallel_contextualizer(
297
+ mesh=mesh,
298
+ contextualizer_template=physical_kernel.contextualizer,
299
+ g_template=global_stream,
300
+ tile_size=int(pair_tile_size),
301
+ )
302
+ physical_node, physical_edge, physical_context_g = physical_context_entry(
303
+ physical_kernel.contextualizer,
304
+ routed_node,
305
+ routed_edge,
306
+ leaf_real,
307
+ structural_mask,
308
+ global_stream,
309
+ )
310
+ jax.block_until_ready((physical_node, physical_edge, physical_context_g))
311
+ del routed_node, routed_edge, global_stream, physical_context_entry
312
+ gc.collect()
313
+
314
+ physical_global_entry = build_sequence_parallel_edge_global_update(
315
+ mesh=mesh,
316
+ module_template=physical_kernel.global_fork,
317
+ tile_size=int(pair_tile_size),
318
+ )
319
+ physical_global = physical_global_entry(
320
+ physical_kernel.global_fork,
321
+ physical_context_g,
322
+ physical_edge,
323
+ structural_mask,
324
+ )
325
+ jax.block_until_ready(physical_global)
326
+ del physical_context_g, physical_global_entry
327
+ gc.collect()
328
+
329
+ physical_leaf_entry = build_sequence_parallel_physical_leaf(
330
+ mesh=mesh,
331
+ kernel_template=physical_kernel,
332
+ )
333
+ leaf_h, c_rows = physical_leaf_entry(
334
+ physical_kernel,
335
+ physical_node,
336
+ physical_global,
337
+ )
338
+ jax.block_until_ready((leaf_h, c_rows))
339
+ del physical_node, physical_leaf_entry
340
+ gc.collect()
341
+
342
+ physical_reducer_entry = build_sequence_parallel_physical_reducer(
343
+ mesh=mesh,
344
+ kernel_template=physical_kernel,
345
+ edge_template=physical_edge,
346
+ replicate_threshold=min(512, n),
347
+ contextualizer_tile_size=int(pair_tile_size),
348
+ )
349
+ tree = physical_reducer_entry(
350
+ physical_kernel,
351
+ physical_edge,
352
+ leaf_h,
353
+ c_rows,
354
+ leaf_real,
355
+ structural_mask,
356
+ physical_global,
357
+ perm,
358
+ )
359
+ jax.block_until_ready(tree)
360
+
361
+ return LargeNCompiledEvalResult(
362
+ wavefunction=CompiledWaveFunction(bind_shared_kernel(model), tree),
363
+ perm=perm,
364
+ logp=logp,
365
+ )
366
+
367
+
368
+ __all__ = [
369
+ "LargeNCompiledEvalResult",
370
+ "compile_large_n_eval_wavefunction",
371
+ ]
src/hamiltonzero/evaluation/pallas_mha.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ # Copyright 2023 The JAX Authors.
4
+ # Modifications copyright (c) 2026 Simulacra Research Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+
19
+ from __future__ import annotations
20
+
21
+ import dataclasses
22
+ import functools
23
+ import math
24
+ from typing import Any
25
+
26
+ import jax
27
+ import jax.numpy as jnp
28
+ from jax import lax
29
+ from jax.experimental import pallas as pl
30
+ from jax.experimental.pallas import triton as plgpu
31
+
32
+
33
+ @dataclasses.dataclass(frozen=True, slots=True)
34
+ class BlockSizes:
35
+ block_q: int
36
+ block_k: int
37
+
38
+ @classmethod
39
+ def get_default(cls):
40
+ return cls(block_q=128, block_k=128)
41
+
42
+
43
+ def noncausal_bias_mha_forward_kernel(
44
+ q_ref,
45
+ k_ref,
46
+ v_ref,
47
+ bias_ref,
48
+ o_ref: Any,
49
+ *,
50
+ block_q: int,
51
+ block_k: int,
52
+ head_dim: int,
53
+ ):
54
+ seq_len = k_ref.shape[0]
55
+ start_q = pl.program_id(0)
56
+ head_dim_padded = q_ref.shape[-1]
57
+ m_i = jnp.zeros(block_q, dtype=jnp.float32) - float("inf")
58
+ l_i = jnp.zeros(block_q, dtype=jnp.float32)
59
+ o = jnp.zeros((block_q, head_dim_padded), dtype=jnp.float32)
60
+ curr_q_slice = pl.dslice(start_q * block_q, block_q)
61
+ head_mask = (jnp.arange(head_dim_padded) < head_dim)[None, :]
62
+ q = plgpu.load(q_ref, mask=head_mask, other=0.0)
63
+
64
+ def body(start_k, carry):
65
+ o_prev, m_prev, l_prev = carry
66
+ curr_k_slice = pl.dslice(start_k * block_k, block_k)
67
+ k = plgpu.load(k_ref.at[curr_k_slice, :], mask=head_mask, other=0.0)
68
+ qk = pl.dot(q, k.T)
69
+ bias = plgpu.load(bias_ref.at[curr_q_slice, curr_k_slice])
70
+ qk += bias
71
+ qk *= math.log2(math.e)
72
+ qk = qk.astype(q_ref.dtype)
73
+ m_curr = jnp.max(qk, axis=-1)
74
+ m_next = jnp.maximum(m_prev, m_curr)
75
+ correction = jnp.exp2(m_prev - m_next)
76
+ l_prev_corr = correction * l_prev
77
+ s_curr = jnp.exp2(qk - m_next[:, None])
78
+ l_curr = s_curr.sum(axis=-1)
79
+ l_next = l_prev_corr + l_curr
80
+ o_prev_corr = correction[:, None] * o_prev
81
+ v = plgpu.load(v_ref.at[curr_k_slice, :], mask=head_mask)
82
+ o_curr = pl.dot(s_curr.astype(v.dtype), v)
83
+ o_next = o_prev_corr + o_curr
84
+ return o_next, m_next, l_next
85
+
86
+ upper_bound = pl.cdiv(seq_len, block_k)
87
+ o, _m_i, l_i = lax.fori_loop(0, upper_bound, body, (o, m_i, l_i))
88
+ o /= l_i[:, None]
89
+ plgpu.store(o_ref.at[:, : o.shape[-1]], o.astype(o_ref.dtype), mask=head_mask)
90
+
91
+
92
+ @functools.partial(jax.jit, static_argnames=["block_sizes"])
93
+ def noncausal_bias_mha(
94
+ q,
95
+ k,
96
+ v,
97
+ bias,
98
+ *,
99
+ block_sizes: BlockSizes = BlockSizes.get_default(),
100
+ ):
101
+ batch_size, q_seq_len, num_heads, head_dim = q.shape
102
+ kv_seq_len = k.shape[1]
103
+ block_q = min(block_sizes.block_q, q_seq_len)
104
+ block_k = min(block_sizes.block_k, kv_seq_len)
105
+ head_dim_padded = pl.next_power_of_2(head_dim)
106
+ if (q.shape[-1] != k.shape[-1]) or (q.shape[-1] != v.shape[-1]):
107
+ raise ValueError(
108
+ "This kernel expects q, k, and v to have the same head dimension, "
109
+ f"but found {q.shape=}, {k.shape=}, {v.shape=}."
110
+ )
111
+ if bias.shape != (batch_size, q_seq_len, kv_seq_len, num_heads):
112
+ raise ValueError(
113
+ f"bias must have shape [batch, query, key, heads]; got {bias.shape}"
114
+ )
115
+ if q_seq_len % block_q != 0:
116
+ raise ValueError(f"{q_seq_len=} must be a multiple of {block_q=}")
117
+ if kv_seq_len % block_k != 0:
118
+ raise ValueError(f"{kv_seq_len=} must be a multiple of {block_k=}")
119
+ grid = (pl.cdiv(q_seq_len, block_q), batch_size, num_heads)
120
+ num_warps = 4 if head_dim <= 64 else 8
121
+ kernel = functools.partial(
122
+ noncausal_bias_mha_forward_kernel,
123
+ block_q=block_q,
124
+ block_k=block_k,
125
+ head_dim=head_dim,
126
+ )
127
+ in_specs = [
128
+ pl.BlockSpec(
129
+ (None, block_q, None, head_dim_padded),
130
+ lambda i, j, k: (j, i, k, 0),
131
+ ),
132
+ pl.BlockSpec(
133
+ (None, kv_seq_len, None, head_dim_padded),
134
+ lambda i, j, k: (j, 0, k, 0),
135
+ ),
136
+ pl.BlockSpec(
137
+ (None, kv_seq_len, None, head_dim_padded),
138
+ lambda i, j, k: (j, 0, k, 0),
139
+ ),
140
+ pl.BlockSpec(
141
+ (None, block_q, kv_seq_len, None),
142
+ lambda i, j, k: (j, i, 0, k),
143
+ ),
144
+ ]
145
+ out_shape = [q]
146
+ out_specs = [
147
+ pl.BlockSpec(
148
+ (None, block_q, None, head_dim_padded),
149
+ lambda i, j, k: (j, i, k, 0),
150
+ )
151
+ ]
152
+ out = pl.pallas_call(
153
+ kernel,
154
+ grid=grid,
155
+ in_specs=in_specs,
156
+ out_specs=out_specs,
157
+ compiler_params=plgpu.CompilerParams(num_warps=num_warps, num_stages=2),
158
+ out_shape=out_shape,
159
+ name="mha_forward",
160
+ )(q, k, v, bias)
161
+ return out[0]
162
+
163
+
164
+ __all__ = ["BlockSizes", "noncausal_bias_mha"]
src/hamiltonzero/evaluation/runner.py ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026 Simulacra Research Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import time
7
+ from dataclasses import replace
8
+ from typing import Any, Callable
9
+
10
+ import jax
11
+ import jax.numpy as jnp
12
+ import numpy as np
13
+
14
+ from hamiltonzero.config import EvalConfig
15
+
16
+ from .backend import EvalBackend, MCMCPopulation
17
+ from .statistics import EnergyWindow, P01_TWO_SIDED, select_winner_per_physical
18
+ from .types import ContestCandidate, ContestResult, EvalMetric, EvalResult
19
+
20
+
21
+ def _permute_q_prefix(q, permutations):
22
+ index = jnp.broadcast_to(permutations[:, None, None, :, None], q.shape)
23
+ return jnp.take_along_axis(q, index, axis=3)
24
+
25
+
26
+ def _collapse_to_winner(q_virtual, permutations, winner_idx, *, P, K):
27
+ winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32)
28
+ inverse = jnp.argsort(permutations, axis=-1)
29
+ q_canonical = _permute_q_prefix(q_virtual, inverse)
30
+ batch_per_candidate = q_canonical.shape[1]
31
+ tail = q_canonical.shape[2:]
32
+ q_physical = q_canonical.reshape((P, K, batch_per_candidate) + tail).reshape(
33
+ (P, K * batch_per_candidate) + tail
34
+ )
35
+ permutations_pk = permutations.reshape((P, K, -1))
36
+ route = jnp.take_along_axis(
37
+ permutations_pk,
38
+ winner_idx[:, None, None],
39
+ axis=1,
40
+ )[:, 0]
41
+ return _permute_q_prefix(q_physical, route), route
42
+
43
+
44
+ def _gather_winner_ladder(values, winner_idx, *, P, K):
45
+ winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32)
46
+ values_pk = values.reshape((P, K) + values.shape[1:])
47
+ index = winner_idx[(slice(None), None) + (None,) * (values_pk.ndim - 2)]
48
+ return jnp.take_along_axis(values_pk, index, axis=1)[:, 0]
49
+
50
+
51
+ def _as_batched_route(permutation):
52
+ value = jnp.asarray(permutation, dtype=jnp.int32)
53
+ if value.ndim == 1:
54
+ value = value[None, :]
55
+ if value.ndim != 2 or value.shape[0] != 1:
56
+ raise ValueError("single-system evaluation requires a route with shape [1, N]")
57
+ return value
58
+
59
+
60
+ def _compose_walker_route(old_inverse, route):
61
+ return jnp.take_along_axis(
62
+ jnp.asarray(old_inverse, dtype=jnp.int32),
63
+ route,
64
+ axis=-1,
65
+ )
66
+
67
+
68
+ def _adapt(backend: EvalBackend, state: Any, config: EvalConfig):
69
+ return backend.adapt_mcmc(state, config.mcmc)
70
+
71
+
72
+ def _burn_in(
73
+ backend: EvalBackend,
74
+ state: Any,
75
+ model: Any,
76
+ context: Any,
77
+ config: EvalConfig,
78
+ *,
79
+ iterations: int,
80
+ replica_steps: int,
81
+ ):
82
+ for _ in range(int(iterations)):
83
+ state = backend.step_mcmc(
84
+ state,
85
+ model,
86
+ context,
87
+ replica_steps=int(replica_steps),
88
+ walker_chunk_size=int(config.mcmc.walker_chunk_size),
89
+ )
90
+ backend.block_until_ready(backend.cold_walkers(state))
91
+ state = _adapt(backend, state, config)
92
+ return state
93
+
94
+
95
+ def _measure(
96
+ backend: EvalBackend,
97
+ state: Any,
98
+ model: Any,
99
+ context: Any,
100
+ config: EvalConfig,
101
+ *,
102
+ started: float,
103
+ metric_sink: Callable[[EvalMetric], None] | None,
104
+ ):
105
+ window = EnergyWindow(
106
+ config.measurements,
107
+ systems=1,
108
+ batch_size=config.mcmc.batch_size,
109
+ )
110
+ for step in range(config.measurements):
111
+ step_started = time.perf_counter()
112
+ state = backend.step_mcmc(
113
+ state,
114
+ model,
115
+ context,
116
+ replica_steps=int(config.mcmc.steps),
117
+ walker_chunk_size=int(config.mcmc.walker_chunk_size),
118
+ )
119
+ q_cold = backend.cold_walkers(state)
120
+ total, exchange, _casimir, field = backend.custom_lap_energy(
121
+ model,
122
+ context,
123
+ q_cold,
124
+ config.energy,
125
+ )
126
+ backend.block_until_ready(total)
127
+ window.push(total, exchange, field)
128
+ state = _adapt(backend, state, config)
129
+ if metric_sink is not None:
130
+ energy = np.asarray(total).real
131
+ metric_sink(
132
+ EvalMetric(
133
+ step=step,
134
+ energy=float(np.mean(energy)),
135
+ energy_std=float(np.std(energy)),
136
+ step_walltime=float(time.perf_counter() - step_started),
137
+ walltime=float(time.perf_counter() - started),
138
+ )
139
+ )
140
+ return state, window
141
+
142
+
143
+ def _ordinary(
144
+ backend: EvalBackend,
145
+ model: Any,
146
+ context: Any,
147
+ canonical,
148
+ mcmc_key,
149
+ config: EvalConfig,
150
+ ):
151
+ state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
152
+ candidates = backend.beam_candidates(
153
+ model,
154
+ canonical.context,
155
+ beam_width=int(config.contest_beam_width),
156
+ top_k=1,
157
+ temperature=float(config.route_temperature),
158
+ )
159
+ permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32)
160
+ if permutations.ndim != 3 or permutations.shape[:2] != (1, 1):
161
+ raise ValueError("ordinary eval router must return shape [1, 1, N]")
162
+ route = permutations[:, 0]
163
+ walker_route = _compose_walker_route(canonical.old_inverse, route)
164
+ routed_context = backend.route_context(
165
+ canonical.context,
166
+ route,
167
+ compact_custom_lap=False,
168
+ )
169
+ state = backend.route_mcmc(state, walker_route)
170
+ wavefunction = backend.compile_single(model, routed_context)
171
+ backend.block_until_ready(wavefunction)
172
+ logp = float(np.asarray(candidates.log_probabilities)[0, 0])
173
+ return wavefunction, routed_context, state, route, logp, None
174
+
175
+
176
+ def _compiled_finetune_ordinary(
177
+ backend: EvalBackend,
178
+ model: Any,
179
+ context: Any,
180
+ canonical,
181
+ embedded_route,
182
+ mcmc_key,
183
+ config: EvalConfig,
184
+ ):
185
+ state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
186
+ route = _as_batched_route(embedded_route)
187
+ if route.shape[-1] != canonical.context.mask.shape[-1]:
188
+ raise ValueError(
189
+ "compiled fine-tune route width does not match the evaluation system"
190
+ )
191
+ walker_route = _compose_walker_route(canonical.old_inverse, route)
192
+ routed_context = backend.route_context(
193
+ canonical.context,
194
+ route,
195
+ compact_custom_lap=False,
196
+ )
197
+ state = backend.route_mcmc(state, walker_route)
198
+ wavefunction = backend.compile_embedded(model)
199
+ backend.block_until_ready(wavefunction)
200
+ return wavefunction, routed_context, state, route, None, None
201
+
202
+
203
+ def _contest(
204
+ backend: EvalBackend,
205
+ model: Any,
206
+ canonical,
207
+ root_key,
208
+ config: EvalConfig,
209
+ ):
210
+ K = int(config.contest_candidates)
211
+ batch_per_candidate = int(config.mcmc.batch_size) // K
212
+ candidates = backend.beam_candidates(
213
+ model,
214
+ canonical.context,
215
+ beam_width=int(config.contest_beam_width),
216
+ top_k=K,
217
+ temperature=float(config.route_temperature),
218
+ )
219
+ beam_permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32)
220
+ if beam_permutations.ndim != 3 or beam_permutations.shape[:2] != (1, K):
221
+ raise ValueError(f"contest router must return shape [1, {K}, N]")
222
+ n_sites = int(beam_permutations.shape[-1])
223
+ permutations = beam_permutations.reshape((K, n_sites))
224
+ virtual_context = backend.virtual_context(canonical.context, permutations)
225
+ race_mcmc = replace(config.mcmc, batch_size=batch_per_candidate)
226
+ state = backend.initialize_mcmc(
227
+ jax.random.fold_in(root_key, 7411),
228
+ model,
229
+ virtual_context,
230
+ race_mcmc,
231
+ )
232
+ wavefunctions = backend.compile_candidates(
233
+ model,
234
+ canonical.context,
235
+ permutations,
236
+ )
237
+ backend.block_until_ready(wavefunctions)
238
+ for _ in range(int(config.contest_preburn)):
239
+ state = backend.step_mcmc(
240
+ state,
241
+ wavefunctions,
242
+ virtual_context,
243
+ replica_steps=int(config.mcmc.burn_in_replica_steps),
244
+ walker_chunk_size=int(config.mcmc.walker_chunk_size),
245
+ )
246
+ backend.block_until_ready(backend.cold_walkers(state))
247
+ state = backend.adapt_mcmc(state, race_mcmc)
248
+ race_window = EnergyWindow(
249
+ config.contest_measurements,
250
+ systems=K,
251
+ batch_size=batch_per_candidate,
252
+ )
253
+ for _ in range(int(config.contest_measurements)):
254
+ state = backend.step_mcmc(
255
+ state,
256
+ wavefunctions,
257
+ virtual_context,
258
+ replica_steps=int(config.mcmc.steps),
259
+ walker_chunk_size=int(config.mcmc.walker_chunk_size),
260
+ )
261
+ state = backend.adapt_mcmc(state, race_mcmc)
262
+ q_cold = backend.cold_walkers(state)
263
+ total, exchange, _casimir, field = backend.custom_lap_energy(
264
+ wavefunctions,
265
+ virtual_context,
266
+ q_cold,
267
+ config.energy,
268
+ )
269
+ backend.block_until_ready(total)
270
+ race_window.push(total, exchange, field)
271
+ energies = np.asarray(
272
+ [[race_window.tail_mean("total", candidate) for candidate in range(K)]]
273
+ )
274
+ tailstd = np.asarray(
275
+ [[race_window.tail_std("total", candidate) for candidate in range(K)]]
276
+ )
277
+ beam_logp = np.asarray(candidates.log_probabilities, dtype=float)
278
+ winners, ties, reasons, _bands, standard_errors = select_winner_per_physical(
279
+ energies,
280
+ tailstd,
281
+ beam_logp,
282
+ batch_per_candidate,
283
+ z=P01_TWO_SIDED,
284
+ ucb_z=float(config.contest_se_multiplier),
285
+ )
286
+ winner = int(winners[0])
287
+ wavefunction = backend.select_candidate(wavefunctions, winner)
288
+ population = backend.mcmc_population(state)
289
+ q_final, route = _collapse_to_winner(
290
+ population.q,
291
+ permutations,
292
+ winners,
293
+ P=1,
294
+ K=K,
295
+ )
296
+ sigma = _gather_winner_ladder(population.sigma, winners, P=1, K=K)
297
+ beta = _gather_winner_ladder(population.beta, winners, P=1, K=K)
298
+ routed_context = backend.route_context(
299
+ canonical.context,
300
+ route,
301
+ compact_custom_lap=False,
302
+ )
303
+ final_state = backend.initialize_mcmc(
304
+ jax.random.fold_in(root_key, 7919),
305
+ wavefunction,
306
+ routed_context,
307
+ config.mcmc,
308
+ )
309
+ final_state = backend.replace_mcmc_population(
310
+ final_state,
311
+ MCMCPopulation(q=q_final, sigma=sigma, beta=beta),
312
+ )
313
+ backend.block_until_ready(backend.cold_walkers(final_state))
314
+ contest_candidates = tuple(
315
+ ContestCandidate(
316
+ index=index,
317
+ route_log_probability=float(beam_logp[0, index]),
318
+ energy=float(energies[0, index]),
319
+ standard_error=float(standard_errors[0, index]),
320
+ walker_tail_std=float(tailstd[0, index]),
321
+ in_tie_set=bool(ties[0, index]),
322
+ )
323
+ for index in range(K)
324
+ )
325
+ contest = ContestResult(
326
+ winner=winner,
327
+ reason=reasons[0],
328
+ candidates=contest_candidates,
329
+ )
330
+ backend.release_context(virtual_context)
331
+ return (
332
+ wavefunction,
333
+ routed_context,
334
+ final_state,
335
+ route,
336
+ float(beam_logp[0, winner]),
337
+ contest,
338
+ )
339
+
340
+
341
+ def _large_n(
342
+ backend: EvalBackend,
343
+ model: Any,
344
+ context: Any,
345
+ canonical,
346
+ mcmc_key,
347
+ config: EvalConfig,
348
+ ):
349
+ state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
350
+ compiled = backend.compile_large_n(
351
+ model,
352
+ canonical.context,
353
+ sequence_shards=int(config.large_n_sequence_shards),
354
+ pair_tile_size=int(config.large_n_pair_tile_size),
355
+ temperature=float(config.route_temperature),
356
+ )
357
+ route = _as_batched_route(compiled.permutation)
358
+ walker_route = _compose_walker_route(canonical.old_inverse, route)
359
+ routed_context = backend.route_context(
360
+ canonical.context,
361
+ route,
362
+ compact_custom_lap=True,
363
+ )
364
+ state = backend.route_mcmc(state, walker_route)
365
+ backend.block_until_ready(compiled.wavefunction)
366
+ logp = float(np.asarray(compiled.log_probability))
367
+ return compiled.wavefunction, routed_context, state, route, logp, None
368
+
369
+
370
+ def _validate(config: EvalConfig) -> None:
371
+ if config.contest and config.large_n:
372
+ raise ValueError("contest and large_n are mutually exclusive")
373
+ if int(config.measurements) < 1:
374
+ raise ValueError("measurements must be positive")
375
+ if int(config.mcmc.batch_size) < 1:
376
+ raise ValueError("MCMC batch size must be positive")
377
+ if int(config.mcmc.replicas) < 2:
378
+ raise ValueError("MCMC requires at least two replicas")
379
+ if int(config.mcmc.steps) < 1:
380
+ raise ValueError("MCMC replica steps must be positive")
381
+ if int(config.mcmc.burn_in_replica_steps) < 1:
382
+ raise ValueError("burn-in replica steps must be positive")
383
+ if int(config.mcmc.walker_chunk_size) < 1:
384
+ raise ValueError("walker chunk size must be positive")
385
+ if config.contest:
386
+ K = int(config.contest_candidates)
387
+ W = int(config.contest_beam_width)
388
+ if K < 2 or W < K:
389
+ raise ValueError("contest requires beam_width >= candidates >= 2")
390
+ if int(config.mcmc.batch_size) % K:
391
+ raise ValueError("MCMC batch size must be divisible by candidates")
392
+ if int(config.mcmc.batch_size) // K < 32:
393
+ raise ValueError("contest requires at least 32 walkers per candidate")
394
+ if int(config.contest_preburn) < 0:
395
+ raise ValueError("contest preburn must be non-negative")
396
+ if int(config.contest_measurements) < 1:
397
+ raise ValueError("contest measurements must be positive")
398
+ if int(config.large_n_sequence_shards) < 0:
399
+ raise ValueError("large-N sequence shards must be non-negative")
400
+ if int(config.large_n_pair_tile_size) < 1:
401
+ raise ValueError("large-N pair tile size must be positive")
402
+
403
+
404
+ def evaluate(
405
+ config: EvalConfig,
406
+ backend: EvalBackend,
407
+ *,
408
+ metric_sink: Callable[[EvalMetric], None] | None = None,
409
+ ) -> EvalResult:
410
+ _validate(config)
411
+ started = time.perf_counter()
412
+ root_key = jax.random.PRNGKey(int(config.seed))
413
+ model_key, mcmc_key = jax.random.split(root_key)
414
+ context = backend.load_system(config.system, config.energy)
415
+ model = backend.load_model(
416
+ config.checkpoint,
417
+ config.model,
418
+ model_key,
419
+ context,
420
+ contextualizer_attention=config.contextualizer_attention,
421
+ )
422
+ canonical = backend.canonicalize_context(context)
423
+ embedded_route = backend.embedded_route(model)
424
+ if embedded_route is not None and (config.contest or config.large_n):
425
+ raise ValueError(
426
+ "compiled fine-tune checkpoints support ordinary eval only; "
427
+ "contest and large_n require a router checkpoint"
428
+ )
429
+ if embedded_route is not None:
430
+ prepared = _compiled_finetune_ordinary(
431
+ backend,
432
+ model,
433
+ context,
434
+ canonical,
435
+ embedded_route,
436
+ mcmc_key,
437
+ config,
438
+ )
439
+ path = "ordinary"
440
+ elif config.contest:
441
+ prepared = _contest(
442
+ backend,
443
+ model,
444
+ canonical,
445
+ root_key,
446
+ config,
447
+ )
448
+ path = "contest"
449
+ elif config.large_n:
450
+ prepared = _large_n(
451
+ backend,
452
+ model,
453
+ context,
454
+ canonical,
455
+ mcmc_key,
456
+ config,
457
+ )
458
+ path = "large_n"
459
+ else:
460
+ prepared = _ordinary(
461
+ backend,
462
+ model,
463
+ context,
464
+ canonical,
465
+ mcmc_key,
466
+ config,
467
+ )
468
+ path = "ordinary"
469
+ wavefunction, routed_context, state, route, route_logp, contest = prepared
470
+ del model, context, canonical, embedded_route, prepared
471
+ wavefunction, routed_context, state = backend.prepare_singular(
472
+ wavefunction,
473
+ routed_context,
474
+ state,
475
+ )
476
+ state = _burn_in(
477
+ backend,
478
+ state,
479
+ wavefunction,
480
+ routed_context,
481
+ config,
482
+ iterations=int(config.mcmc.burn_in),
483
+ replica_steps=int(config.mcmc.burn_in_replica_steps),
484
+ )
485
+ _state, window = _measure(
486
+ backend,
487
+ state,
488
+ wavefunction,
489
+ routed_context,
490
+ config,
491
+ started=started,
492
+ metric_sink=metric_sink,
493
+ )
494
+ route_host = np.asarray(route, dtype=np.int32)
495
+ if route_host.shape[0] != 1:
496
+ raise ValueError("single-system eval produced more than one route")
497
+ return EvalResult(
498
+ path=path,
499
+ route=tuple(int(value) for value in route_host[0]),
500
+ route_log_probability=(None if route_logp is None else float(route_logp)),
501
+ measurements=int(window.count),
502
+ walltime_seconds=float(time.perf_counter() - started),
503
+ energy=window.metrics("total"),
504
+ channels={
505
+ "exchange": window.metrics("exchange"),
506
+ "field": window.metrics("field"),
507
+ },
508
+ contest=contest,
509
+ )