Instructions to use ZibinDong/ActionCodec2-Extended-1mm-2k with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZibinDong/ActionCodec2-Extended-1mm-2k with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZibinDong/ActionCodec2-Extended-1mm-2k", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Release extended first-order ActionCodec2 tokenizer
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- LICENSE +202 -0
- README.md +100 -0
- actioncodec2_kernels/__init__.py +1 -0
- actioncodec2_kernels/module.cpp +90 -0
- actioncodec2_kernels/parallel.hpp +116 -0
- actioncodec2_kernels/physical.cpp +407 -0
- actioncodec2_kernels/resample.cpp +833 -0
- actioncodec2_kernels/setbpe.cpp +394 -0
- actioncodec2_kernels/so3.cpp +313 -0
- config.json +4 -0
- processing_actioncodec2.py +94 -0
- processor_config.json +10 -0
- profiles/eef/bpe.json +5950 -0
- profiles/eef/config.json +4 -0
- profiles/eef/config.yaml +74 -0
- profiles/eef/primitives.npz +3 -0
- profiles/eef/processor_config.json +3 -0
- pyproject.toml +16 -0
- release.json +12 -0
- requirements.txt +6 -0
- router_config.yaml +120 -0
- runtime/__init__.py +1 -0
- runtime/backend/__init__.py +1 -0
- runtime/backend/native_backend.py +39 -0
- runtime/common/__init__.py +1 -0
- runtime/common/_frontend_base.py +108 -0
- runtime/common/_types.py +12 -0
- runtime/common/_validation.py +89 -0
- runtime/frontend/__init__.py +1 -0
- runtime/frontend/tokenizer.py +398 -0
- runtime/hub_acceleration.py +239 -0
- runtime/hub_entry.py +94 -0
- runtime/integration/__init__.py +1 -0
- runtime/integration/exporting.py +401 -0
- runtime/integration/hf.py +199 -0
- runtime/physical/__init__.py +1 -0
- runtime/physical/_so3.py +99 -0
- runtime/physical/boundary.py +153 -0
- runtime/physical/configuration.py +794 -0
- runtime/physical/physical_codec.py +950 -0
- runtime/physical/physical_processor.py +384 -0
- runtime/physical/primitives.py +83 -0
- runtime/physical/semantic.py +452 -0
- runtime/physical/trajectory.py +151 -0
- runtime/processing_actioncodec2.py +553 -0
- runtime/routing/__init__.py +1 -0
- runtime/routing/cache.py +45 -0
- runtime/routing/defaults.py +622 -0
- runtime/routing/presets.py +872 -0
- runtime/routing/routing.py +809 -0
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.
|
README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: actioncodec2
|
| 4 |
+
tags:
|
| 5 |
+
- robotics
|
| 6 |
+
- action-tokenization
|
| 7 |
+
- transformers
|
| 8 |
+
- extended-edition
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# ActionCodec2 Extended Edition — 1st Order
|
| 12 |
+
|
| 13 |
+
This tokenizer was trained on a curated corpus with lower-quality datasets and most stationary-arm segments removed. The data curation concerns tokenizer training, independently of a downstream policy's training data.
|
| 14 |
+
|
| 15 |
+
This is a complete tokenizer artifact for continuous robot actions, rather than
|
| 16 |
+
a neural policy checkpoint. Keep all files together when copying it.
|
| 17 |
+
|
| 18 |
+
- Encoding order: 1
|
| 19 |
+
- Codec rate: 15 Hz
|
| 20 |
+
- Codebook size: 2048 per profile
|
| 21 |
+
- Fitted profiles: eef
|
| 22 |
+
- Gripper representation: binary
|
| 23 |
+
|
| 24 |
+
- Trajectory tracking precision tier: 1mm
|
| 25 |
+
|
| 26 |
+
The [ActionCodec2 collection](https://huggingface.co/collections/ZibinDong/actioncodec2-6ab6c4860ee90a881dda0a8b)
|
| 27 |
+
contains the paper and extended editions. Edition and encoding order are separate choices.
|
| 28 |
+
|
| 29 |
+
## Install and enable C++ acceleration
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
hf download ZibinDong/ActionCodec2-Extended-1mm-2k --local-dir ./actioncodec2-tokenizer
|
| 33 |
+
pip install -r ./actioncodec2-tokenizer/requirements.txt
|
| 34 |
+
pip install ./actioncodec2-tokenizer
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
The last command compiles the bundled C++17 CPU kernels. It requires a C++17
|
| 38 |
+
compiler and installs `actioncodec2-kernels`, independently of the G0.5 package.
|
| 39 |
+
The processor automatically uses installed kernels; no GPU is needed for
|
| 40 |
+
tokenization. An installed copy of the
|
| 41 |
+
[source repository](https://github.com/ZibinDong/actioncodec2) also supplies them.
|
| 42 |
+
Loading without kernels uses the bundled Python implementation. Set
|
| 43 |
+
`ACTIONCODEC2_NO_NATIVE=1` to explicitly select that reference path.
|
| 44 |
+
|
| 45 |
+
Acceleration covers physical quantization, Set-BPE encode/decode, trajectory
|
| 46 |
+
resampling and pose operations. Equal-horizon batches use C++ worker threads,
|
| 47 |
+
which release the Python GIL. Thread counts adapt to measured chunk cost and
|
| 48 |
+
the process CPU allowance. Forked DataLoader workers use one thread each by
|
| 49 |
+
default; `ACTIONCODEC2_NUM_THREADS` sets an explicit per-process budget.
|
| 50 |
+
|
| 51 |
+
## Encode and decode
|
| 52 |
+
|
| 53 |
+
```python
|
| 54 |
+
import numpy as np
|
| 55 |
+
from transformers import AutoProcessor
|
| 56 |
+
|
| 57 |
+
codec = AutoProcessor.from_pretrained(
|
| 58 |
+
"ZibinDong/ActionCodec2-Extended-1mm-2k", trust_remote_code=True,
|
| 59 |
+
)
|
| 60 |
+
# Pin revision to a Hub commit for reproducible downloads.
|
| 61 |
+
actions = np.zeros((2, 12, 7), dtype=np.float32)
|
| 62 |
+
actions[..., 6] = 1.0
|
| 63 |
+
|
| 64 |
+
tokens = codec.encode(actions, fps=15)
|
| 65 |
+
reconstructed = codec.decode(tokens, fps=15)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Inputs are `(T, D)` or `(B, T, D)`, with a common horizon within a batch.
|
| 69 |
+
Tokens are ragged integer rows; decode returns CPU `torch.float32` tensors with
|
| 70 |
+
shape `(B, T_out, D)`. Pass the actual recording rate as `fps`; resampling can
|
| 71 |
+
change the decoded horizon. Batch chunks together for encoding throughput.
|
| 72 |
+
|
| 73 |
+
The default layout is `single_eef_delta`: position increments in metres,
|
| 74 |
+
body-frame rotation-vector increments in radians, then gripper (open at >= 0.8).
|
| 75 |
+
Rotation follows `R_next = R_previous @ Exp(rotvec)`. Convert normalized
|
| 76 |
+
controller commands to these units before encoding. Inspect other layouts with
|
| 77 |
+
`codec.print_action_spaces()` and select with `codec.for_action_space(name)`.
|
| 78 |
+
Absolute layouts require the same `current_state` of shape `(B, D)` during
|
| 79 |
+
encoding and decoding. Second-order profiles additionally require explicit
|
| 80 |
+
`previous_first_order` boundaries at the codec rate.
|
| 81 |
+
|
| 82 |
+
`codec.grammar(horizon, fps=...)` constrains generated token coverage. Remove
|
| 83 |
+
model prompt, EOS and padding IDs before decoding. Each profile occupies its
|
| 84 |
+
own namespace; `router_config.yaml` records token offsets. A policy must have
|
| 85 |
+
enough action vocabulary rows for its selected profile or routed namespace.
|
| 86 |
+
The source repository's G0.5 adapter currently accepts at most 4096 per profile.
|
| 87 |
+
|
| 88 |
+
## Validation scope
|
| 89 |
+
|
| 90 |
+
This configuration has not been evaluated in the selected extended-edition LIBERO comparison.
|
| 91 |
+
|
| 92 |
+
The extended edition currently contains first-order profiles. At 1 mm, EEF
|
| 93 |
+
starts at 2k and joint profiles at 4k because of the primitive vocabulary size;
|
| 94 |
+
5 mm and 10 mm provide 1k, 2k, 4k, 8k and 16k. Only six first-order configurations
|
| 95 |
+
have been tested on LIBERO. The full range has not been validated downstream.
|
| 96 |
+
Precision tiers describe tokenizer trajectory tracking, not robot execution
|
| 97 |
+
accuracy. Selected benchmark results do not establish a general advantage for
|
| 98 |
+
the extended edition or predict performance on other tasks or real robots.
|
| 99 |
+
|
| 100 |
+
See [configuration and measured results](https://github.com/ZibinDong/actioncodec2).
|
actioncodec2_kernels/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Portable ActionCodec2 CPU kernels."""
|
actioncodec2_kernels/module.cpp
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// pybind11 entry point for the ActionCodec2 physical-stage kernels.
|
| 2 |
+
//
|
| 3 |
+
// These are the encode/decode kernels the runtime bundled with an ActionCodec2
|
| 4 |
+
// Hugging Face artifact calls when a native module is available. Each one has
|
| 5 |
+
// a bit-identical NumPy implementation in that runtime.
|
| 6 |
+
|
| 7 |
+
#include <pybind11/numpy.h>
|
| 8 |
+
#include <pybind11/pybind11.h>
|
| 9 |
+
|
| 10 |
+
#include <cstdint>
|
| 11 |
+
#include <stdexcept>
|
| 12 |
+
|
| 13 |
+
#include "parallel.hpp"
|
| 14 |
+
|
| 15 |
+
namespace py = pybind11;
|
| 16 |
+
|
| 17 |
+
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 18 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 19 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 20 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 21 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 22 |
+
int32_t, bool, double, double, bool, int);
|
| 23 |
+
|
| 24 |
+
py::array_t<int32_t> additive_second_order_encode_batch(
|
| 25 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&,
|
| 26 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 27 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 28 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 29 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&, bool, int);
|
| 30 |
+
|
| 31 |
+
py::array_t<float> physical_decode_batch(
|
| 32 |
+
const py::array_t<float>&, const py::array_t<int32_t>&, const py::array_t<uint8_t>&,
|
| 33 |
+
const py::array_t<float>&, const py::array_t<float>&,
|
| 34 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 35 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&,
|
| 36 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&, int32_t, int);
|
| 37 |
+
|
| 38 |
+
py::array_t<int32_t> physical_encode_batch(
|
| 39 |
+
const py::array_t<float>&, const py::array_t<int32_t>&, const py::array_t<uint8_t>&,
|
| 40 |
+
const py::array_t<float>&, const py::array_t<float>&,
|
| 41 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&,
|
| 42 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&,
|
| 43 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&, int32_t, int,
|
| 44 |
+
const py::array_t<float>&, bool);
|
| 45 |
+
|
| 46 |
+
py::array_t<int32_t> so3_encode_batch(
|
| 47 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 48 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 49 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>&, int32_t, bool, bool,
|
| 50 |
+
double, bool, int);
|
| 51 |
+
|
| 52 |
+
void register_setbpe(py::module_& module);
|
| 53 |
+
void register_resample(py::module_& module);
|
| 54 |
+
|
| 55 |
+
PYBIND11_MODULE(_actioncodec2_native, module) {
|
| 56 |
+
module.doc() = "ActionCodec2 encode/decode kernels: physical stage, resampling, Set-BPE";
|
| 57 |
+
register_setbpe(module);
|
| 58 |
+
register_resample(module);
|
| 59 |
+
module.def(
|
| 60 |
+
"set_thread_budget",
|
| 61 |
+
[](int threads) {
|
| 62 |
+
if (threads < 0) throw std::invalid_argument("thread budget must be non-negative");
|
| 63 |
+
ac2::thread_budget().store(threads);
|
| 64 |
+
},
|
| 65 |
+
py::arg("threads"),
|
| 66 |
+
"Cap threads per call for this process; 0 uses every CPU it may run on.");
|
| 67 |
+
module.def("thread_budget", [] { return ac2::thread_budget().load(); });
|
| 68 |
+
module.def("plan_threads", &ac2::plan_threads, py::arg("count"), py::arg("requested"),
|
| 69 |
+
py::arg("seconds_each"), "Threads a call would use for ``count`` items.");
|
| 70 |
+
module.def("so3_second_order_encode_batch", &so3_second_order_encode_batch,
|
| 71 |
+
py::arg("table"), py::arg("bins"), py::arg("quaternions"),
|
| 72 |
+
py::arg("previous_first_order"), py::arg("input_order"), py::arg("left"),
|
| 73 |
+
py::arg("limit"), py::arg("radius"), py::arg("smooth"), py::arg("threads") = 0);
|
| 74 |
+
module.def("additive_second_order_encode_batch", &additive_second_order_encode_batch,
|
| 75 |
+
py::arg("table"), py::arg("bins"), py::arg("positions"),
|
| 76 |
+
py::arg("previous_first_order"), py::arg("epsilon"), py::arg("smooth"),
|
| 77 |
+
py::arg("threads") = 0);
|
| 78 |
+
module.def("physical_decode_batch", &physical_decode_batch, py::arg("table"),
|
| 79 |
+
py::arg("bins"), py::arg("binary"), py::arg("lower"), py::arg("upper"),
|
| 80 |
+
py::arg("cells"), py::arg("initial"), py::arg("initial_velocity"),
|
| 81 |
+
py::arg("order"), py::arg("threads") = 0);
|
| 82 |
+
module.def("physical_encode_batch", &physical_encode_batch, py::arg("table"), py::arg("bins"),
|
| 83 |
+
py::arg("binary"), py::arg("lower"), py::arg("upper"), py::arg("positions"),
|
| 84 |
+
py::arg("initial"), py::arg("initial_velocity"), py::arg("order"),
|
| 85 |
+
py::arg("threads") = 0, py::arg("epsilon") = py::array_t<float>(),
|
| 86 |
+
py::arg("smooth") = false);
|
| 87 |
+
module.def("so3_encode_batch", &so3_encode_batch, py::arg("table"), py::arg("bins"),
|
| 88 |
+
py::arg("actions"), py::arg("input_order"), py::arg("rotation_vector"),
|
| 89 |
+
py::arg("left"), py::arg("limit"), py::arg("smooth"), py::arg("threads") = 0);
|
| 90 |
+
}
|
actioncodec2_kernels/parallel.hpp
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Fork-safe data parallelism for the runtime encode/decode kernels.
|
| 2 |
+
//
|
| 3 |
+
// These kernels run inside forked processes (for example PyTorch DataLoader
|
| 4 |
+
// workers). GNU OpenMP keeps a process-wide thread pool that does not survive
|
| 5 |
+
// fork(): once the parent has entered a parallel region -- ours, or any other
|
| 6 |
+
// library's sharing libgomp -- the child hangs or crashes when it enters one.
|
| 7 |
+
// This helper instead starts its threads per call and joins them before
|
| 8 |
+
// returning, so no thread state outlives a call and fork() is always safe.
|
| 9 |
+
//
|
| 10 |
+
// Iterations are split into contiguous static blocks, like OpenMP's
|
| 11 |
+
// ``schedule(static)``, and every iteration writes only its own outputs, so
|
| 12 |
+
// results do not depend on the thread count.
|
| 13 |
+
//
|
| 14 |
+
// Starting a thread costs tens of microseconds, and one iteration costs from
|
| 15 |
+
// well under a microsecond to about a hundred depending on the kernel and the
|
| 16 |
+
// profile's primitive counts. So each call times its first iteration on the
|
| 17 |
+
// calling thread and sizes the split of the remaining work ``W`` from it: the
|
| 18 |
+
// calling thread starts threads one after another at ``s`` seconds each, so a
|
| 19 |
+
// split over ``w`` threads takes about ``W / w + s * w``, least at
|
| 20 |
+
// ``w = sqrt(W / s)``.
|
| 21 |
+
#pragma once
|
| 22 |
+
|
| 23 |
+
#include <algorithm>
|
| 24 |
+
#include <atomic>
|
| 25 |
+
#include <chrono>
|
| 26 |
+
#include <cmath>
|
| 27 |
+
#include <cstdint>
|
| 28 |
+
#include <exception>
|
| 29 |
+
#include <mutex>
|
| 30 |
+
#include <thread>
|
| 31 |
+
#include <vector>
|
| 32 |
+
|
| 33 |
+
#ifdef __linux__
|
| 34 |
+
#include <sched.h>
|
| 35 |
+
#endif
|
| 36 |
+
|
| 37 |
+
namespace ac2 {
|
| 38 |
+
|
| 39 |
+
// Process-wide cap on threads per call; 0 means every CPU the process may use.
|
| 40 |
+
inline std::atomic<int>& thread_budget() {
|
| 41 |
+
static std::atomic<int> value{0};
|
| 42 |
+
return value;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
inline int available_cpus() {
|
| 46 |
+
#ifdef __linux__
|
| 47 |
+
cpu_set_t allowed;
|
| 48 |
+
CPU_ZERO(&allowed);
|
| 49 |
+
if (sched_getaffinity(0, sizeof(allowed), &allowed) == 0) {
|
| 50 |
+
return std::max(1, CPU_COUNT(&allowed));
|
| 51 |
+
}
|
| 52 |
+
#endif
|
| 53 |
+
return std::max(1, static_cast<int>(std::thread::hardware_concurrency()));
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Measured cost of starting and joining one thread (seconds).
|
| 57 |
+
constexpr double kThreadStartSeconds = 30e-6;
|
| 58 |
+
|
| 59 |
+
// Threads for ``count`` iterations of ``seconds_each``: at most ``requested``
|
| 60 |
+
// when positive, at most the process budget, and at most ``sqrt(W / s)``.
|
| 61 |
+
inline int64_t plan_threads(int64_t count, int requested, double seconds_each) {
|
| 62 |
+
const int budget = thread_budget().load();
|
| 63 |
+
int64_t threads = budget > 0 ? budget : available_cpus();
|
| 64 |
+
if (requested > 0) threads = std::min<int64_t>(threads, requested);
|
| 65 |
+
const double by_work = std::sqrt(count * seconds_each / kThreadStartSeconds);
|
| 66 |
+
if (by_work < static_cast<double>(threads)) threads = static_cast<int64_t>(by_work);
|
| 67 |
+
return std::max<int64_t>(1, std::min(threads, count));
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Call ``body(index)`` for every index in [0, count). Iteration 0 runs first
|
| 71 |
+
// on the calling thread and sizes the split of the rest; the calling thread
|
| 72 |
+
// also runs the first block of the rest. The first exception thrown by any
|
| 73 |
+
// block is rethrown after all threads have joined.
|
| 74 |
+
template <class Body>
|
| 75 |
+
void parallel_for(int64_t count, int threads, Body&& body) {
|
| 76 |
+
if (count <= 0) return;
|
| 77 |
+
const auto start = std::chrono::steady_clock::now();
|
| 78 |
+
body(0);
|
| 79 |
+
const double seconds_each =
|
| 80 |
+
std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
|
| 81 |
+
const int64_t rest = count - 1;
|
| 82 |
+
if (rest == 0) return;
|
| 83 |
+
const int64_t workers = plan_threads(rest, threads, seconds_each);
|
| 84 |
+
auto run = [&](int64_t worker) {
|
| 85 |
+
const int64_t begin = 1 + rest * worker / workers;
|
| 86 |
+
const int64_t end = 1 + rest * (worker + 1) / workers;
|
| 87 |
+
for (int64_t index = begin; index < end; ++index) body(index);
|
| 88 |
+
};
|
| 89 |
+
if (workers == 1) {
|
| 90 |
+
run(0);
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
std::exception_ptr failure;
|
| 94 |
+
std::mutex failure_lock;
|
| 95 |
+
auto guarded = [&](int64_t worker) {
|
| 96 |
+
try {
|
| 97 |
+
run(worker);
|
| 98 |
+
} catch (...) {
|
| 99 |
+
std::lock_guard<std::mutex> hold(failure_lock);
|
| 100 |
+
if (!failure) failure = std::current_exception();
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
std::vector<std::thread> pool;
|
| 104 |
+
pool.reserve(static_cast<size_t>(workers - 1));
|
| 105 |
+
try {
|
| 106 |
+
for (int64_t worker = 1; worker < workers; ++worker) pool.emplace_back(guarded, worker);
|
| 107 |
+
} catch (...) {
|
| 108 |
+
for (auto& thread : pool) thread.join();
|
| 109 |
+
throw;
|
| 110 |
+
}
|
| 111 |
+
guarded(0);
|
| 112 |
+
for (auto& thread : pool) thread.join();
|
| 113 |
+
if (failure) std::rethrow_exception(failure);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
} // namespace ac2
|
actioncodec2_kernels/physical.cpp
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Native physical-stage recursions.
|
| 2 |
+
//
|
| 3 |
+
// The encoders are strictly sequential in time -- each step's request depends
|
| 4 |
+
// on the reconstruction error the previous step left behind -- so the win here
|
| 5 |
+
// is not vectorization but removing per-step interpreter and NumPy dispatch
|
| 6 |
+
// overhead. That overhead dominates exactly where it hurts most: caching a
|
| 7 |
+
// dataset as fixed-horizon chunks calls the encoder millions of times with
|
| 8 |
+
// T ~ 20, where a NumPy implementation spends nearly all of its time in call
|
| 9 |
+
// setup. The batch entry points additionally spread chunks over threads.
|
| 10 |
+
|
| 11 |
+
#include <pybind11/pybind11.h>
|
| 12 |
+
#include <pybind11/numpy.h>
|
| 13 |
+
#include <pybind11/stl.h>
|
| 14 |
+
|
| 15 |
+
#include <algorithm>
|
| 16 |
+
#include <cfloat>
|
| 17 |
+
#include <cmath>
|
| 18 |
+
#include <cstdint>
|
| 19 |
+
#include <vector>
|
| 20 |
+
|
| 21 |
+
#include "parallel.hpp"
|
| 22 |
+
|
| 23 |
+
namespace py = pybind11;
|
| 24 |
+
|
| 25 |
+
namespace {
|
| 26 |
+
|
| 27 |
+
struct SecondOrderAxis {
|
| 28 |
+
const float* values;
|
| 29 |
+
int32_t count;
|
| 30 |
+
bool ordered;
|
| 31 |
+
int32_t nonnegative;
|
| 32 |
+
int32_t nonpositive;
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
template <bool Smooth>
|
| 36 |
+
inline int32_t pick_second_order_axis(const SecondOrderAxis& axis, double request,
|
| 37 |
+
double direction, double epsilon) {
|
| 38 |
+
const float* values = axis.values;
|
| 39 |
+
if (axis.ordered) {
|
| 40 |
+
const int32_t insertion = std::lower_bound(values, values + axis.count, request) - values;
|
| 41 |
+
const int32_t left = std::max(0, insertion - 1);
|
| 42 |
+
const int32_t right = std::min(insertion, axis.count - 1);
|
| 43 |
+
int32_t nearest = std::fabs(request - values[left]) <= std::fabs(request - values[right])
|
| 44 |
+
? left : right;
|
| 45 |
+
// Rounded distance plateaus must keep the original first-cell tie-break.
|
| 46 |
+
while (nearest > 0 && std::fabs(request - values[nearest - 1]) == std::fabs(request - values[nearest]))
|
| 47 |
+
--nearest;
|
| 48 |
+
if constexpr (!Smooth) return nearest;
|
| 49 |
+
const int32_t preferred = direction > 0 ? std::max(nearest, axis.nonnegative)
|
| 50 |
+
: direction < 0 ? std::min(nearest, axis.nonpositive) : nearest;
|
| 51 |
+
const double primitive = values[preferred];
|
| 52 |
+
const double sign = primitive > 0 ? 1 : primitive < 0 ? -1 : 0;
|
| 53 |
+
return sign * direction >= 0 && std::fabs(request - primitive) <= epsilon ? preferred : nearest;
|
| 54 |
+
}
|
| 55 |
+
// Small tables are faster to scan; arbitrary stored order also uses this path.
|
| 56 |
+
int32_t nearest = 0, feasible = -1, preferred = -1;
|
| 57 |
+
double nearest_distance = INFINITY, feasible_distance = INFINITY, preferred_distance = INFINITY;
|
| 58 |
+
for (int32_t k = 0; k < axis.count; ++k) {
|
| 59 |
+
const double primitive = values[k];
|
| 60 |
+
const double distance = std::fabs(request - primitive);
|
| 61 |
+
if (distance < nearest_distance) { nearest = k; nearest_distance = distance; }
|
| 62 |
+
if constexpr (!Smooth) continue;
|
| 63 |
+
if (distance > epsilon) continue;
|
| 64 |
+
if (distance < feasible_distance) { feasible = k; feasible_distance = distance; }
|
| 65 |
+
const double sign = primitive > 0 ? 1 : primitive < 0 ? -1 : 0;
|
| 66 |
+
if (sign * direction >= 0 && distance < preferred_distance) {
|
| 67 |
+
preferred = k;
|
| 68 |
+
preferred_distance = distance;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
return preferred >= 0 ? preferred : feasible >= 0 ? feasible : nearest;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
} // namespace
|
| 75 |
+
|
| 76 |
+
py::array_t<int32_t> additive_second_order_encode_batch(
|
| 77 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>& table,
|
| 78 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& bins,
|
| 79 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& positions,
|
| 80 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& previous_first_order,
|
| 81 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>& epsilon,
|
| 82 |
+
bool smooth, int threads) {
|
| 83 |
+
if (positions.ndim() != 3 || table.ndim() != 2 || previous_first_order.ndim() != 2)
|
| 84 |
+
throw std::invalid_argument("invalid second-order additive batch shapes");
|
| 85 |
+
const int64_t batch = positions.shape(0), steps = positions.shape(1), dim = positions.shape(2);
|
| 86 |
+
if (table.shape(0) != dim || bins.size() != dim || epsilon.size() != dim ||
|
| 87 |
+
previous_first_order.shape(0) != batch || previous_first_order.shape(1) != dim)
|
| 88 |
+
throw std::invalid_argument("inconsistent second-order additive batch dimensions");
|
| 89 |
+
for (int64_t j = 0; j < dim; ++j) {
|
| 90 |
+
if (bins.data()[j] < 1 || bins.data()[j] > table.shape(1))
|
| 91 |
+
throw std::invalid_argument("primitive count exceeds table width");
|
| 92 |
+
}
|
| 93 |
+
py::array_t<int32_t> output({batch, steps, dim});
|
| 94 |
+
const auto* targets = positions.data();
|
| 95 |
+
const auto* initial = previous_first_order.data();
|
| 96 |
+
const auto* axes = table.data();
|
| 97 |
+
const auto* counts = bins.data();
|
| 98 |
+
const auto* limits = epsilon.data();
|
| 99 |
+
auto* cells = output.mutable_data();
|
| 100 |
+
const int64_t width = table.shape(1);
|
| 101 |
+
std::vector<SecondOrderAxis> search;
|
| 102 |
+
for (int64_t j = 0; j < dim; ++j) {
|
| 103 |
+
const float* begin = axes + j * width;
|
| 104 |
+
const float* end = begin + counts[j];
|
| 105 |
+
const bool ordered = counts[j] > 16 &&
|
| 106 |
+
std::adjacent_find(begin, end, [](float a, float b) { return a >= b; }) == end;
|
| 107 |
+
const int32_t nonnegative = ordered ? std::lower_bound(begin, end, 0.0f) - begin : 0;
|
| 108 |
+
const int32_t nonpositive = ordered ? std::upper_bound(begin, end, 0.0f) - begin - 1 : 0;
|
| 109 |
+
search.push_back({begin, counts[j], ordered, std::min(nonnegative, counts[j] - 1),
|
| 110 |
+
std::max(nonpositive, 0)});
|
| 111 |
+
}
|
| 112 |
+
py::gil_scoped_release release;
|
| 113 |
+
ac2::parallel_for(batch, threads, [&](int64_t i) {
|
| 114 |
+
std::vector<double> position(dim, 0), velocity(initial+i*dim, initial+(i+1)*dim), direction(dim, 0);
|
| 115 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 116 |
+
for (int64_t j = 0; j < dim; ++j) {
|
| 117 |
+
const double request = targets[(i*steps+t)*dim+j] - (position[j]+velocity[j]);
|
| 118 |
+
const int32_t choice = smooth
|
| 119 |
+
? pick_second_order_axis<true>(search[j], request, direction[j], limits[j])
|
| 120 |
+
: pick_second_order_axis<false>(search[j], request, direction[j], limits[j]);
|
| 121 |
+
const double primitive = axes[j*width+choice];
|
| 122 |
+
velocity[j] += primitive;
|
| 123 |
+
position[j] += velocity[j];
|
| 124 |
+
if (primitive != 0) direction[j] = primitive > 0 ? 1 : -1;
|
| 125 |
+
cells[(i*steps+t)*dim+j] = choice;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
return output;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
namespace {
|
| 133 |
+
|
| 134 |
+
struct Layout {
|
| 135 |
+
int32_t dimension;
|
| 136 |
+
int32_t width; // padded primitive-table stride
|
| 137 |
+
const float* table; // [dimension * width], +inf padding
|
| 138 |
+
const int32_t* bins; // [dimension]
|
| 139 |
+
const uint8_t* binary; // [dimension], 1 for absolute two-valued axes
|
| 140 |
+
const float* lower;
|
| 141 |
+
const float* upper;
|
| 142 |
+
const float* epsilon; // [dimension]; null unless `smooth` is set
|
| 143 |
+
bool smooth; // pick by FirstOrderCodec._select_smooth
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
// Matches PhysicalCodecBase.SOURCE_TOLERANCE.
|
| 147 |
+
constexpr float kSourceTolerance = 8.0F * FLT_EPSILON;
|
| 148 |
+
|
| 149 |
+
inline int32_t nearest(const Layout& layout, int32_t axis, float request) {
|
| 150 |
+
const float* values = layout.table + static_cast<size_t>(axis) * layout.width;
|
| 151 |
+
const int32_t count = layout.bins[axis];
|
| 152 |
+
int32_t best = 0;
|
| 153 |
+
float best_distance = std::fabs(values[0] - request);
|
| 154 |
+
for (int32_t index = 1; index < count; ++index) {
|
| 155 |
+
const float distance = std::fabs(values[index] - request);
|
| 156 |
+
if (distance < best_distance) {
|
| 157 |
+
best_distance = distance;
|
| 158 |
+
best = index;
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
return best;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// FirstOrderCodec._select_smooth, primitive for primitive. Lexicographic in
|
| 165 |
+
// (does not reverse `direction`, distance to the request), restricted to the
|
| 166 |
+
// primitives already within epsilon; nothing feasible falls back to `nearest`.
|
| 167 |
+
inline int32_t select_smooth(const Layout& layout, int32_t axis, float request, float direction) {
|
| 168 |
+
const float* values = layout.table + static_cast<size_t>(axis) * layout.width;
|
| 169 |
+
const int32_t count = layout.bins[axis];
|
| 170 |
+
const float radius = layout.epsilon[axis] + kSourceTolerance;
|
| 171 |
+
int32_t nearest_index = 0;
|
| 172 |
+
float nearest_distance = std::fabs(values[0] - request);
|
| 173 |
+
bool any_feasible = false;
|
| 174 |
+
int32_t feasible_index = 0;
|
| 175 |
+
float feasible_distance = 0.0F;
|
| 176 |
+
bool any_preferred = false;
|
| 177 |
+
int32_t preferred_index = 0;
|
| 178 |
+
float preferred_distance = 0.0F;
|
| 179 |
+
for (int32_t index = 0; index < count; ++index) {
|
| 180 |
+
const float distance = std::fabs(values[index] - request);
|
| 181 |
+
if (index > 0 && distance < nearest_distance) {
|
| 182 |
+
nearest_distance = distance;
|
| 183 |
+
nearest_index = index;
|
| 184 |
+
}
|
| 185 |
+
if (!(distance <= radius)) continue;
|
| 186 |
+
if (!any_feasible || distance < feasible_distance) {
|
| 187 |
+
any_feasible = true;
|
| 188 |
+
feasible_distance = distance;
|
| 189 |
+
feasible_index = index;
|
| 190 |
+
}
|
| 191 |
+
// np.sign(value) * direction >= 0: a zero primitive never reverses.
|
| 192 |
+
const float sign = values[index] > 0.0F ? 1.0F : (values[index] < 0.0F ? -1.0F : 0.0F);
|
| 193 |
+
if (!(sign * direction >= 0.0F)) continue;
|
| 194 |
+
if (!any_preferred || distance < preferred_distance) {
|
| 195 |
+
any_preferred = true;
|
| 196 |
+
preferred_distance = distance;
|
| 197 |
+
preferred_index = index;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
if (any_preferred) return preferred_index;
|
| 201 |
+
if (any_feasible) return feasible_index;
|
| 202 |
+
return nearest_index;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
void encode_first_order(const Layout& layout, const float* positions, int64_t steps,
|
| 206 |
+
const float* initial, int32_t* cells) {
|
| 207 |
+
const int32_t dimension = layout.dimension;
|
| 208 |
+
std::vector<float> state(initial, initial + dimension);
|
| 209 |
+
// Sign of the last non-zero primitive each coordinate emitted.
|
| 210 |
+
std::vector<float> direction(dimension, 0.0F);
|
| 211 |
+
for (int64_t step = 0; step < steps; ++step) {
|
| 212 |
+
const float* target = positions + step * dimension;
|
| 213 |
+
int32_t* row = cells + step * dimension;
|
| 214 |
+
for (int32_t axis = 0; axis < dimension; ++axis) {
|
| 215 |
+
const float* values = layout.table + static_cast<size_t>(axis) * layout.width;
|
| 216 |
+
if (layout.binary[axis]) {
|
| 217 |
+
const int32_t cell = nearest(layout, axis, target[axis]);
|
| 218 |
+
state[axis] = values[cell];
|
| 219 |
+
row[axis] = cell;
|
| 220 |
+
continue;
|
| 221 |
+
}
|
| 222 |
+
const float request = target[axis] - state[axis];
|
| 223 |
+
const int32_t cell = layout.smooth ? select_smooth(layout, axis, request, direction[axis])
|
| 224 |
+
: nearest(layout, axis, request);
|
| 225 |
+
const float picked = values[cell];
|
| 226 |
+
state[axis] = std::min(std::max(state[axis] + picked, layout.lower[axis]),
|
| 227 |
+
layout.upper[axis]);
|
| 228 |
+
if (picked != 0.0F) direction[axis] = picked > 0.0F ? 1.0F : -1.0F;
|
| 229 |
+
row[axis] = cell;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
void decode_first_order(const Layout& layout, const int32_t* cells, int64_t steps,
|
| 235 |
+
const float* initial, float* positions) {
|
| 236 |
+
const int32_t dimension = layout.dimension;
|
| 237 |
+
std::vector<float> state(initial, initial + dimension);
|
| 238 |
+
for (int64_t step = 0; step < steps; ++step) {
|
| 239 |
+
const int32_t* row = cells + step * dimension;
|
| 240 |
+
float* out = positions + step * dimension;
|
| 241 |
+
for (int32_t axis = 0; axis < dimension; ++axis) {
|
| 242 |
+
const float* values = layout.table + static_cast<size_t>(axis) * layout.width;
|
| 243 |
+
const float primitive = values[row[axis]];
|
| 244 |
+
if (layout.binary[axis]) {
|
| 245 |
+
state[axis] = primitive;
|
| 246 |
+
} else {
|
| 247 |
+
state[axis] = std::min(std::max(state[axis] + primitive, layout.lower[axis]),
|
| 248 |
+
layout.upper[axis]);
|
| 249 |
+
}
|
| 250 |
+
out[axis] = state[axis];
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
void encode_second_order(const Layout& layout, const float* positions, int64_t steps,
|
| 256 |
+
const float* initial, const float* initial_velocity, int32_t* cells) {
|
| 257 |
+
const int32_t dimension = layout.dimension;
|
| 258 |
+
std::vector<float> source(initial, initial + dimension);
|
| 259 |
+
std::vector<float> source_velocity(initial_velocity, initial_velocity + dimension);
|
| 260 |
+
std::vector<float> recon(initial, initial + dimension);
|
| 261 |
+
std::vector<float> recon_velocity(initial_velocity, initial_velocity + dimension);
|
| 262 |
+
std::vector<float> error(dimension, 0.0F);
|
| 263 |
+
std::vector<float> previous(dimension, 0.0F);
|
| 264 |
+
for (int64_t step = 0; step < steps; ++step) {
|
| 265 |
+
const float* target = positions + step * dimension;
|
| 266 |
+
int32_t* row = cells + step * dimension;
|
| 267 |
+
for (int32_t axis = 0; axis < dimension; ++axis) {
|
| 268 |
+
const float* values = layout.table + static_cast<size_t>(axis) * layout.width;
|
| 269 |
+
if (layout.binary[axis]) {
|
| 270 |
+
row[axis] = nearest(layout, axis, target[axis]);
|
| 271 |
+
continue;
|
| 272 |
+
}
|
| 273 |
+
const float velocity = target[axis] - source[axis];
|
| 274 |
+
const float request =
|
| 275 |
+
2.0F * error[axis] - previous[axis] + (velocity - source_velocity[axis]);
|
| 276 |
+
const int32_t cell = nearest(layout, axis, request);
|
| 277 |
+
recon_velocity[axis] += values[cell];
|
| 278 |
+
recon[axis] += recon_velocity[axis];
|
| 279 |
+
source[axis] = target[axis];
|
| 280 |
+
source_velocity[axis] = velocity;
|
| 281 |
+
previous[axis] = error[axis];
|
| 282 |
+
error[axis] = source[axis] - recon[axis];
|
| 283 |
+
row[axis] = cell;
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
void decode_second_order(const Layout& layout, const int32_t* cells, int64_t steps,
|
| 289 |
+
const float* initial, const float* initial_velocity, float* positions) {
|
| 290 |
+
const int32_t dimension = layout.dimension;
|
| 291 |
+
std::vector<float> state(initial, initial + dimension);
|
| 292 |
+
std::vector<float> velocity(initial_velocity, initial_velocity + dimension);
|
| 293 |
+
for (int64_t step = 0; step < steps; ++step) {
|
| 294 |
+
const int32_t* row = cells + step * dimension;
|
| 295 |
+
float* out = positions + step * dimension;
|
| 296 |
+
for (int32_t axis = 0; axis < dimension; ++axis) {
|
| 297 |
+
const float primitive = layout.table[static_cast<size_t>(axis) * layout.width + row[axis]];
|
| 298 |
+
if (layout.binary[axis]) {
|
| 299 |
+
out[axis] = primitive;
|
| 300 |
+
continue;
|
| 301 |
+
}
|
| 302 |
+
velocity[axis] += primitive;
|
| 303 |
+
state[axis] += velocity[axis];
|
| 304 |
+
out[axis] = state[axis];
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
Layout make_layout(const py::array_t<float>& table, const py::array_t<int32_t>& bins,
|
| 310 |
+
const py::array_t<uint8_t>& binary, const py::array_t<float>& lower,
|
| 311 |
+
const py::array_t<float>& upper, const py::array_t<float>& epsilon,
|
| 312 |
+
bool smooth) {
|
| 313 |
+
Layout layout;
|
| 314 |
+
layout.dimension = static_cast<int32_t>(table.shape(0));
|
| 315 |
+
layout.width = static_cast<int32_t>(table.shape(1));
|
| 316 |
+
layout.table = table.data();
|
| 317 |
+
layout.bins = bins.data();
|
| 318 |
+
layout.binary = binary.data();
|
| 319 |
+
layout.lower = lower.data();
|
| 320 |
+
layout.upper = upper.data();
|
| 321 |
+
layout.epsilon = epsilon.size() == 0 ? nullptr : epsilon.data();
|
| 322 |
+
layout.smooth = smooth && layout.epsilon != nullptr;
|
| 323 |
+
if (smooth && layout.epsilon == nullptr) {
|
| 324 |
+
throw std::invalid_argument("smooth selection needs a per-coordinate epsilon");
|
| 325 |
+
}
|
| 326 |
+
if (layout.epsilon != nullptr && epsilon.size() != layout.dimension) {
|
| 327 |
+
throw std::invalid_argument("epsilon must have one entry per coordinate");
|
| 328 |
+
}
|
| 329 |
+
return layout;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
using Array2D = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 333 |
+
using Array3D = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 334 |
+
using Cells3D = py::array_t<int32_t, py::array::c_style | py::array::forcecast>;
|
| 335 |
+
|
| 336 |
+
} // namespace
|
| 337 |
+
|
| 338 |
+
py::array_t<float> physical_decode_batch(const py::array_t<float>& table,
|
| 339 |
+
const py::array_t<int32_t>& bins,
|
| 340 |
+
const py::array_t<uint8_t>& binary,
|
| 341 |
+
const py::array_t<float>& lower,
|
| 342 |
+
const py::array_t<float>& upper, const Cells3D& cells,
|
| 343 |
+
const Array2D& initial,
|
| 344 |
+
const Array2D& initial_velocity, int32_t order,
|
| 345 |
+
int threads) {
|
| 346 |
+
if (cells.ndim() != 3 || cells.shape(2) != table.shape(0) || initial.ndim() != 2 ||
|
| 347 |
+
initial_velocity.ndim() != 2 || initial.shape(0) != cells.shape(0) ||
|
| 348 |
+
initial_velocity.shape(0) != cells.shape(0) || initial.shape(1) != cells.shape(2) ||
|
| 349 |
+
initial_velocity.shape(1) != cells.shape(2) || (order != 1 && order != 2)) {
|
| 350 |
+
throw std::invalid_argument("invalid physical decode batch shapes or order");
|
| 351 |
+
}
|
| 352 |
+
const Layout layout =
|
| 353 |
+
make_layout(table, bins, binary, lower, upper, py::array_t<float>(), false);
|
| 354 |
+
const int64_t batch = cells.shape(0);
|
| 355 |
+
const int64_t steps = cells.shape(1);
|
| 356 |
+
const int64_t dimension = layout.dimension;
|
| 357 |
+
py::array_t<float> positions({batch, steps, dimension});
|
| 358 |
+
{
|
| 359 |
+
py::gil_scoped_release release;
|
| 360 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 361 |
+
const int32_t* source = cells.data() + index * steps * dimension;
|
| 362 |
+
float* target = positions.mutable_data() + index * steps * dimension;
|
| 363 |
+
if (order == 1) {
|
| 364 |
+
decode_first_order(layout, source, steps, initial.data() + index * dimension, target);
|
| 365 |
+
} else {
|
| 366 |
+
decode_second_order(layout, source, steps, initial.data() + index * dimension,
|
| 367 |
+
initial_velocity.data() + index * dimension, target);
|
| 368 |
+
}
|
| 369 |
+
});
|
| 370 |
+
}
|
| 371 |
+
return positions;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// One thread per chunk. ``positions`` is [B, T, D] and ``initial`` is [B, D].
|
| 375 |
+
py::array_t<int32_t> physical_encode_batch(const py::array_t<float>& table,
|
| 376 |
+
const py::array_t<int32_t>& bins,
|
| 377 |
+
const py::array_t<uint8_t>& binary,
|
| 378 |
+
const py::array_t<float>& lower,
|
| 379 |
+
const py::array_t<float>& upper,
|
| 380 |
+
const Array3D& positions, const Array2D& initial,
|
| 381 |
+
const Array2D& initial_velocity, int32_t order,
|
| 382 |
+
int threads, const py::array_t<float>& epsilon,
|
| 383 |
+
bool smooth) {
|
| 384 |
+
const Layout layout = make_layout(table, bins, binary, lower, upper, epsilon, smooth);
|
| 385 |
+
const int64_t batch = positions.shape(0);
|
| 386 |
+
const int64_t steps = positions.shape(1);
|
| 387 |
+
const int64_t dimension = layout.dimension;
|
| 388 |
+
py::array_t<int32_t> cells({batch, steps, dimension});
|
| 389 |
+
const float* source = positions.data();
|
| 390 |
+
const float* starts = initial.data();
|
| 391 |
+
const float* velocities = initial_velocity.data();
|
| 392 |
+
int32_t* output = cells.mutable_data();
|
| 393 |
+
{
|
| 394 |
+
py::gil_scoped_release release;
|
| 395 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 396 |
+
const float* chunk = source + index * steps * dimension;
|
| 397 |
+
int32_t* target = output + index * steps * dimension;
|
| 398 |
+
if (order == 1) {
|
| 399 |
+
encode_first_order(layout, chunk, steps, starts + index * dimension, target);
|
| 400 |
+
} else {
|
| 401 |
+
encode_second_order(layout, chunk, steps, starts + index * dimension,
|
| 402 |
+
velocities + index * dimension, target);
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
return cells;
|
| 407 |
+
}
|
actioncodec2_kernels/resample.cpp
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Rate conversion and pose algebra for canonical ActionCodec2 trajectories.
|
| 2 |
+
//
|
| 3 |
+
// A port of the runtime's ``physical/semantic.py`` resampling and pose helpers,
|
| 4 |
+
// including the scipy pieces they call (natural CubicSpline, PchipInterpolator,
|
| 5 |
+
// PPoly evaluation, RotationSpline). Operations follow the NumPy/scipy order
|
| 6 |
+
// of evaluation, and the LAPACK solvers are the reference algorithms, so
|
| 7 |
+
// results agree with the Python runtime to the last bits of float64; the
|
| 8 |
+
// float32 outputs therefore match except where a value sits on a rounding
|
| 9 |
+
// boundary. Batch entry points run rows in parallel without the GIL.
|
| 10 |
+
|
| 11 |
+
#include <pybind11/numpy.h>
|
| 12 |
+
#include <pybind11/pybind11.h>
|
| 13 |
+
|
| 14 |
+
#include <algorithm>
|
| 15 |
+
#include <array>
|
| 16 |
+
#include <cmath>
|
| 17 |
+
#include <cstdint>
|
| 18 |
+
#include <stdexcept>
|
| 19 |
+
#include <string>
|
| 20 |
+
#include <vector>
|
| 21 |
+
|
| 22 |
+
#include "parallel.hpp"
|
| 23 |
+
|
| 24 |
+
namespace py = pybind11;
|
| 25 |
+
|
| 26 |
+
namespace {
|
| 27 |
+
|
| 28 |
+
using Vec = std::vector<double>;
|
| 29 |
+
|
| 30 |
+
struct Quat {
|
| 31 |
+
double x, y, z, w;
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
// ---- runtime ``_so3`` quaternion math (xyzw), NumPy evaluation order ----
|
| 35 |
+
|
| 36 |
+
Quat so3_normalize(const Quat& q) {
|
| 37 |
+
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 38 |
+
if (norm < 1e-12) throw std::invalid_argument("a pose contains a zero quaternion");
|
| 39 |
+
return {q.x / norm, q.y / norm, q.z / norm, q.w / norm};
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
Quat so3_multiply(const Quat& a, const Quat& b) {
|
| 43 |
+
const double cx = a.y * b.z - a.z * b.y;
|
| 44 |
+
const double cy = a.z * b.x - a.x * b.z;
|
| 45 |
+
const double cz = a.x * b.y - a.y * b.x;
|
| 46 |
+
return so3_normalize({a.w * b.x + b.w * a.x + cx, a.w * b.y + b.w * a.y + cy,
|
| 47 |
+
a.w * b.z + b.w * a.z + cz,
|
| 48 |
+
a.w * b.w - (a.x * b.x + a.y * b.y + a.z * b.z)});
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
Quat so3_from_rotvec(double x, double y, double z) {
|
| 52 |
+
const double angle = std::sqrt(x * x + y * y + z * z);
|
| 53 |
+
const double scale = angle >= 1e-12 ? std::sin(0.5 * angle) / angle : 0.5;
|
| 54 |
+
return so3_normalize({x * scale, y * scale, z * scale, std::cos(0.5 * angle)});
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
void so3_to_rotvec(const Quat& raw, double* out) {
|
| 58 |
+
Quat q = so3_normalize(raw);
|
| 59 |
+
if (q.w < 0.0) q = {-q.x, -q.y, -q.z, -q.w};
|
| 60 |
+
const double vector_norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z);
|
| 61 |
+
const double angle = 2.0 * std::atan2(vector_norm, std::min(std::max(q.w, 0.0), 1.0));
|
| 62 |
+
const double scale = vector_norm >= 1e-12 ? angle / vector_norm : 2.0;
|
| 63 |
+
out[0] = q.x * scale;
|
| 64 |
+
out[1] = q.y * scale;
|
| 65 |
+
out[2] = q.z * scale;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// ---- scipy ``Rotation`` operations used by RotationSpline ----
|
| 69 |
+
|
| 70 |
+
Quat sp_from_quat(const Quat& q) {
|
| 71 |
+
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 72 |
+
if (!(norm > 0.0)) throw std::invalid_argument("found zero norm quaternions in `quat`");
|
| 73 |
+
return {q.x / norm, q.y / norm, q.z / norm, q.w / norm};
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
Quat sp_inv(const Quat& q) { return {-q.x, -q.y, -q.z, q.w}; }
|
| 77 |
+
|
| 78 |
+
Quat sp_compose(const Quat& p, const Quat& q) {
|
| 79 |
+
const double cx = p.y * q.z - p.z * q.y;
|
| 80 |
+
const double cy = p.z * q.x - p.x * q.z;
|
| 81 |
+
const double cz = p.x * q.y - p.y * q.x;
|
| 82 |
+
const Quat r{p.w * q.x + q.w * p.x + cx, p.w * q.y + q.w * p.y + cy, p.w * q.z + q.w * p.z + cz,
|
| 83 |
+
p.w * q.w - (p.x * q.x + p.y * q.y + p.z * q.z)};
|
| 84 |
+
return sp_from_quat(r);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
void sp_as_rotvec(Quat q, double* out) {
|
| 88 |
+
if (q.w < 0.0) q = {-q.x, -q.y, -q.z, -q.w};
|
| 89 |
+
const double angle = 2.0 * std::atan2(std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z), q.w);
|
| 90 |
+
double scale;
|
| 91 |
+
if (angle <= 1e-3) {
|
| 92 |
+
const double angle2 = angle * angle;
|
| 93 |
+
scale = 2.0 + angle2 / 12.0 + 7.0 * angle2 * angle2 / 2880.0;
|
| 94 |
+
} else {
|
| 95 |
+
scale = angle / std::sin(angle / 2.0);
|
| 96 |
+
}
|
| 97 |
+
out[0] = scale * q.x;
|
| 98 |
+
out[1] = scale * q.y;
|
| 99 |
+
out[2] = scale * q.z;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
Quat sp_from_rotvec(const double* v) {
|
| 103 |
+
const double angle = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
| 104 |
+
double scale;
|
| 105 |
+
if (angle <= 1e-3) {
|
| 106 |
+
const double angle2 = angle * angle;
|
| 107 |
+
scale = 0.5 - angle2 / 48.0 + angle2 * angle2 / 3840.0;
|
| 108 |
+
} else {
|
| 109 |
+
scale = std::sin(angle / 2.0) / angle;
|
| 110 |
+
}
|
| 111 |
+
return {scale * v[0], scale * v[1], scale * v[2], std::cos(angle / 2.0)};
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// ---- piecewise cubics (scipy PPoly with CubicHermiteSpline coefficients) ----
|
| 115 |
+
|
| 116 |
+
// PPoly interval: x[i] <= t < x[i+1], extrapolating with the end intervals.
|
| 117 |
+
int64_t ppoly_interval(const Vec& x, double t) {
|
| 118 |
+
const int64_t n = static_cast<int64_t>(x.size());
|
| 119 |
+
if (t >= x[n - 1]) return n - 2;
|
| 120 |
+
if (t < x[0]) return 0;
|
| 121 |
+
return static_cast<int64_t>(std::upper_bound(x.begin(), x.end(), t) - x.begin()) - 1;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// scipy ``evaluate_poly1`` for a cubic: sum of c[k-1-p] * s**p, powers built by
|
| 125 |
+
// repeated multiplication.
|
| 126 |
+
double ppoly_value(const double* c, double s) {
|
| 127 |
+
double result = 0.0, z = 1.0;
|
| 128 |
+
result = result + c[3] * z;
|
| 129 |
+
z *= s;
|
| 130 |
+
result = result + c[2] * z;
|
| 131 |
+
z *= s;
|
| 132 |
+
result = result + c[1] * z;
|
| 133 |
+
z *= s;
|
| 134 |
+
result = result + c[0] * z;
|
| 135 |
+
return result;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Hermite coefficients per interval and column: c[(k*4 + p)*cols + a] with
|
| 139 |
+
// p = 0..3 the descending powers, as CubicHermiteSpline stacks them.
|
| 140 |
+
Vec hermite_coefficients(const Vec& x, const Vec& y, const Vec& slopes, int64_t cols) {
|
| 141 |
+
const int64_t n = static_cast<int64_t>(x.size());
|
| 142 |
+
Vec c(static_cast<size_t>((n - 1) * 4 * cols));
|
| 143 |
+
for (int64_t k = 0; k < n - 1; ++k) {
|
| 144 |
+
const double dx = x[k + 1] - x[k];
|
| 145 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 146 |
+
const double y0 = y[k * cols + a], y1 = y[(k + 1) * cols + a];
|
| 147 |
+
const double d0 = slopes[k * cols + a], d1 = slopes[(k + 1) * cols + a];
|
| 148 |
+
const double slope = (y1 - y0) / dx;
|
| 149 |
+
const double t = (d0 + d1 - 2.0 * slope) / dx;
|
| 150 |
+
c[(k * 4 + 0) * cols + a] = t / dx;
|
| 151 |
+
c[(k * 4 + 1) * cols + a] = (slope - d0) / dx - t;
|
| 152 |
+
c[(k * 4 + 2) * cols + a] = d0;
|
| 153 |
+
c[(k * 4 + 3) * cols + a] = y0;
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
return c;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
void ppoly_evaluate(const Vec& x, const Vec& c, int64_t cols, const Vec& times, int64_t out_cols,
|
| 160 |
+
int64_t col_offset, double* out) {
|
| 161 |
+
for (size_t i = 0; i < times.size(); ++i) {
|
| 162 |
+
const int64_t k = ppoly_interval(x, times[i]);
|
| 163 |
+
const double s = times[i] - x[k];
|
| 164 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 165 |
+
const double coeff[4] = {c[(k * 4 + 0) * cols + a], c[(k * 4 + 1) * cols + a],
|
| 166 |
+
c[(k * 4 + 2) * cols + a], c[(k * 4 + 3) * cols + a]};
|
| 167 |
+
out[i * out_cols + col_offset + a] = ppoly_value(coeff, s);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// LAPACK dgtsv (reference algorithm) for several right-hand sides b[n][cols].
|
| 173 |
+
void gtsv(Vec dl, Vec d, Vec du, Vec& b, int64_t cols) {
|
| 174 |
+
const int64_t n = static_cast<int64_t>(d.size());
|
| 175 |
+
auto pivot = [&](int64_t i, bool last) {
|
| 176 |
+
if (std::fabs(d[i]) >= std::fabs(dl[i])) {
|
| 177 |
+
if (d[i] == 0.0) throw std::invalid_argument("singular matrix");
|
| 178 |
+
const double fact = dl[i] / d[i];
|
| 179 |
+
d[i + 1] = d[i + 1] - fact * du[i];
|
| 180 |
+
for (int64_t j = 0; j < cols; ++j) b[(i + 1) * cols + j] = b[(i + 1) * cols + j] - fact * b[i * cols + j];
|
| 181 |
+
if (!last) dl[i] = 0.0;
|
| 182 |
+
} else {
|
| 183 |
+
const double fact = d[i] / dl[i];
|
| 184 |
+
d[i] = dl[i];
|
| 185 |
+
const double temp = d[i + 1];
|
| 186 |
+
d[i + 1] = du[i] - fact * temp;
|
| 187 |
+
if (!last) {
|
| 188 |
+
dl[i] = du[i + 1];
|
| 189 |
+
du[i + 1] = -fact * dl[i];
|
| 190 |
+
}
|
| 191 |
+
du[i] = temp;
|
| 192 |
+
for (int64_t j = 0; j < cols; ++j) {
|
| 193 |
+
const double swap = b[i * cols + j];
|
| 194 |
+
b[i * cols + j] = b[(i + 1) * cols + j];
|
| 195 |
+
b[(i + 1) * cols + j] = swap - fact * b[(i + 1) * cols + j];
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
};
|
| 199 |
+
for (int64_t i = 0; i + 2 < n; ++i) pivot(i, false);
|
| 200 |
+
if (n > 1) pivot(n - 2, true);
|
| 201 |
+
if (d[n - 1] == 0.0) throw std::invalid_argument("singular matrix");
|
| 202 |
+
for (int64_t j = 0; j < cols; ++j) {
|
| 203 |
+
b[(n - 1) * cols + j] = b[(n - 1) * cols + j] / d[n - 1];
|
| 204 |
+
if (n > 1)
|
| 205 |
+
b[(n - 2) * cols + j] = (b[(n - 2) * cols + j] - du[n - 2] * b[(n - 1) * cols + j]) / d[n - 2];
|
| 206 |
+
for (int64_t i = n - 3; i >= 0; --i) {
|
| 207 |
+
b[i * cols + j] =
|
| 208 |
+
(b[i * cols + j] - du[i] * b[(i + 1) * cols + j] - dl[i] * b[(i + 2) * cols + j]) / d[i];
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// scipy CubicSpline(bc_type="natural") slopes at the knots.
|
| 214 |
+
Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 215 |
+
const int64_t n = static_cast<int64_t>(x.size());
|
| 216 |
+
Vec dx(n - 1), slope((n - 1) * cols);
|
| 217 |
+
for (int64_t k = 0; k < n - 1; ++k) {
|
| 218 |
+
dx[k] = x[k + 1] - x[k];
|
| 219 |
+
for (int64_t a = 0; a < cols; ++a) slope[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / dx[k];
|
| 220 |
+
}
|
| 221 |
+
// Banded A (3 x n): A[0] superdiagonal, A[1] diagonal, A[2] subdiagonal.
|
| 222 |
+
Vec upper(n, 0.0), diagonal(n, 0.0), lower(n, 0.0), b(n * cols);
|
| 223 |
+
for (int64_t k = 1; k < n - 1; ++k) diagonal[k] = 2.0 * (dx[k - 1] + dx[k]);
|
| 224 |
+
for (int64_t k = 2; k < n; ++k) upper[k] = dx[k - 2];
|
| 225 |
+
for (int64_t k = 0; k + 2 < n; ++k) lower[k] = dx[k + 1];
|
| 226 |
+
for (int64_t k = 1; k < n - 1; ++k) {
|
| 227 |
+
for (int64_t a = 0; a < cols; ++a)
|
| 228 |
+
b[k * cols + a] = 3.0 * (dx[k] * slope[(k - 1) * cols + a] + dx[k - 1] * slope[k * cols + a]);
|
| 229 |
+
}
|
| 230 |
+
// Natural ends: second derivative zero (bc value 0.0).
|
| 231 |
+
diagonal[0] = 2.0 * dx[0];
|
| 232 |
+
upper[1] = dx[0];
|
| 233 |
+
diagonal[n - 1] = 2.0 * dx[n - 2];
|
| 234 |
+
lower[n - 2] = dx[n - 2];
|
| 235 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 236 |
+
b[a] = -0.5 * 0.0 * (dx[0] * dx[0]) + 3.0 * (y[cols + a] - y[a]);
|
| 237 |
+
b[(n - 1) * cols + a] =
|
| 238 |
+
0.5 * 0.0 * (dx[n - 2] * dx[n - 2]) + 3.0 * (y[(n - 1) * cols + a] - y[(n - 2) * cols + a]);
|
| 239 |
+
}
|
| 240 |
+
Vec dl(lower.begin(), lower.end() - 1), du(upper.begin() + 1, upper.end());
|
| 241 |
+
gtsv(dl, diagonal, du, b, cols);
|
| 242 |
+
return b;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
double sign(double v) { return v > 0.0 ? 1.0 : (v < 0.0 ? -1.0 : (v == 0.0 ? 0.0 : v)); }
|
| 246 |
+
|
| 247 |
+
double pchip_edge(double h0, double h1, double m0, double m1) {
|
| 248 |
+
double d = ((2.0 * h0 + h1) * m0 - h0 * m1) / (h0 + h1);
|
| 249 |
+
const bool mask = sign(d) != sign(m0);
|
| 250 |
+
const bool mask2 = (sign(m0) != sign(m1)) && (std::fabs(d) > 3.0 * std::fabs(m0));
|
| 251 |
+
if (mask) d = 0.0;
|
| 252 |
+
else if (mask2) d = 3.0 * m0;
|
| 253 |
+
return d;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// scipy PchipInterpolator derivatives at the knots.
|
| 257 |
+
Vec pchip_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 258 |
+
const int64_t n = static_cast<int64_t>(x.size());
|
| 259 |
+
Vec h(n - 1), m((n - 1) * cols), dk(n * cols, 0.0);
|
| 260 |
+
for (int64_t k = 0; k < n - 1; ++k) {
|
| 261 |
+
h[k] = x[k + 1] - x[k];
|
| 262 |
+
for (int64_t a = 0; a < cols; ++a) m[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / h[k];
|
| 263 |
+
}
|
| 264 |
+
if (n == 2) {
|
| 265 |
+
for (int64_t a = 0; a < cols; ++a) dk[a] = dk[cols + a] = m[a];
|
| 266 |
+
return dk;
|
| 267 |
+
}
|
| 268 |
+
for (int64_t k = 1; k < n - 1; ++k) {
|
| 269 |
+
const double w1 = 2.0 * h[k] + h[k - 1];
|
| 270 |
+
const double w2 = h[k] + 2.0 * h[k - 1];
|
| 271 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 272 |
+
const double left = m[(k - 1) * cols + a], right = m[k * cols + a];
|
| 273 |
+
const bool condition = sign(right) != sign(left) || right == 0.0 || left == 0.0;
|
| 274 |
+
if (condition) {
|
| 275 |
+
dk[k * cols + a] = 0.0;
|
| 276 |
+
} else {
|
| 277 |
+
const double whmean = (w1 / left + w2 / right) / (w1 + w2);
|
| 278 |
+
dk[k * cols + a] = 1.0 / whmean;
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 283 |
+
dk[a] = pchip_edge(h[0], h[1], m[a], m[cols + a]);
|
| 284 |
+
dk[(n - 1) * cols + a] = pchip_edge(h[n - 2], h[n - 3], m[(n - 2) * cols + a], m[(n - 3) * cols + a]);
|
| 285 |
+
}
|
| 286 |
+
return dk;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// ---- scipy RotationSpline ----
|
| 290 |
+
|
| 291 |
+
using Mat3 = std::array<double, 9>;
|
| 292 |
+
|
| 293 |
+
Mat3 skew(const double* v) { return {0.0, -v[2], v[1], v[2], 0.0, -v[0], -v[1], v[0], 0.0}; }
|
| 294 |
+
|
| 295 |
+
Mat3 matmul(const Mat3& a, const Mat3& b) {
|
| 296 |
+
Mat3 r{};
|
| 297 |
+
for (int i = 0; i < 3; ++i)
|
| 298 |
+
for (int j = 0; j < 3; ++j)
|
| 299 |
+
r[i * 3 + j] = a[i * 3 + 0] * b[0 * 3 + j] + a[i * 3 + 1] * b[1 * 3 + j] + a[i * 3 + 2] * b[2 * 3 + j];
|
| 300 |
+
return r;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
void matvec(const Mat3& a, const double* v, double* out) {
|
| 304 |
+
for (int i = 0; i < 3; ++i) out[i] = a[i * 3 + 0] * v[0] + a[i * 3 + 1] * v[1] + a[i * 3 + 2] * v[2];
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
double norm3(const double* v) { return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); }
|
| 308 |
+
|
| 309 |
+
void cross(const double* a, const double* b, double* out) {
|
| 310 |
+
out[0] = a[1] * b[2] - a[2] * b[1];
|
| 311 |
+
out[1] = a[2] * b[0] - a[0] * b[2];
|
| 312 |
+
out[2] = a[0] * b[1] - a[1] * b[0];
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
Mat3 rate_to_rotvec_dot(const double* rotvec) {
|
| 316 |
+
const double nm = norm3(rotvec);
|
| 317 |
+
const double k = nm > 1e-4 ? (1.0 - 0.5 * nm / std::tan(0.5 * nm)) / (nm * nm)
|
| 318 |
+
: 1.0 / 12.0 + 1.0 / 720.0 * (nm * nm);
|
| 319 |
+
const Mat3 s = skew(rotvec), ss = matmul(s, s);
|
| 320 |
+
Mat3 r{};
|
| 321 |
+
for (int i = 0; i < 9; ++i) r[i] = ((i % 4 == 0 ? 1.0 : 0.0) + 0.5 * s[i]) + k * ss[i];
|
| 322 |
+
return r;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
Mat3 rotvec_dot_to_rate(const double* rotvec) {
|
| 326 |
+
const double nm = norm3(rotvec);
|
| 327 |
+
double k1, k2;
|
| 328 |
+
if (nm > 1e-4) {
|
| 329 |
+
k1 = (1.0 - std::cos(nm)) / (nm * nm);
|
| 330 |
+
k2 = (nm - std::sin(nm)) / std::pow(nm, 3.0);
|
| 331 |
+
} else {
|
| 332 |
+
k1 = 0.5 - (nm * nm) / 24.0;
|
| 333 |
+
k2 = 1.0 / 6.0 - (nm * nm) / 120.0;
|
| 334 |
+
}
|
| 335 |
+
const Mat3 s = skew(rotvec), ss = matmul(s, s);
|
| 336 |
+
Mat3 r{};
|
| 337 |
+
for (int i = 0; i < 9; ++i) r[i] = ((i % 4 == 0 ? 1.0 : 0.0) - k1 * s[i]) + k2 * ss[i];
|
| 338 |
+
return r;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
void nonlinear_term(const double* rv, const double* rvd, double* out) {
|
| 342 |
+
const double nm = norm3(rv);
|
| 343 |
+
const double dp = rv[0] * rvd[0] + rv[1] * rvd[1] + rv[2] * rvd[2];
|
| 344 |
+
double cp[3], ccp[3], dccp[3];
|
| 345 |
+
cross(rv, rvd, cp);
|
| 346 |
+
cross(rv, cp, ccp);
|
| 347 |
+
cross(rvd, cp, dccp);
|
| 348 |
+
double k1, k2, k3;
|
| 349 |
+
if (nm > 1e-4) {
|
| 350 |
+
const double sn = std::sin(nm), cs = std::cos(nm);
|
| 351 |
+
k1 = (-nm * sn - 2.0 * (cs - 1.0)) / std::pow(nm, 4.0);
|
| 352 |
+
k2 = (-2.0 * nm + 3.0 * sn - nm * cs) / std::pow(nm, 5.0);
|
| 353 |
+
k3 = (nm - sn) / std::pow(nm, 3.0);
|
| 354 |
+
} else {
|
| 355 |
+
k1 = 1.0 / 12.0 - (nm * nm) / 180.0;
|
| 356 |
+
k2 = -1.0 / 60.0 + (nm * nm) / 12604.0;
|
| 357 |
+
k3 = 1.0 / 6.0 - (nm * nm) / 120.0;
|
| 358 |
+
}
|
| 359 |
+
for (int i = 0; i < 3; ++i) out[i] = dp * (k1 * cp[i] + k2 * ccp[i]) + k3 * dccp[i];
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// LAPACK dgbsv (reference dgbtf2 + dgbtrs) for one right-hand side. ``ab``
|
| 363 |
+
// holds A in band storage with ``kl`` extra rows for fill-in: ldab =
|
| 364 |
+
// 2*kl+ku+1 and ab[(kl+ku+i-j) + ldab*j] = A(i, j), all zero-based.
|
| 365 |
+
void gbsv(int64_t n, int64_t kl, int64_t ku, Vec& ab, Vec& b) {
|
| 366 |
+
const int64_t ldab = 2 * kl + ku + 1, kv = ku + kl;
|
| 367 |
+
auto at = [&](int64_t row, int64_t col) -> double& { return ab[row + ldab * col]; };
|
| 368 |
+
std::vector<int64_t> pivots(n);
|
| 369 |
+
int64_t ju = 0;
|
| 370 |
+
for (int64_t j = 0; j < n; ++j) {
|
| 371 |
+
const int64_t km = std::min(kl, n - 1 - j);
|
| 372 |
+
int64_t jp = 0;
|
| 373 |
+
double best = std::fabs(at(kv, j));
|
| 374 |
+
for (int64_t i = 1; i <= km; ++i) {
|
| 375 |
+
if (std::fabs(at(kv + i, j)) > best) { best = std::fabs(at(kv + i, j)); jp = i; }
|
| 376 |
+
}
|
| 377 |
+
pivots[j] = jp + j;
|
| 378 |
+
if (at(kv + jp, j) == 0.0) throw std::invalid_argument("singular matrix");
|
| 379 |
+
ju = std::max(ju, std::min(j + ku + jp, n - 1));
|
| 380 |
+
if (jp != 0) {
|
| 381 |
+
for (int64_t c = 0; c <= ju - j; ++c) std::swap(at(kv + jp - c, j + c), at(kv - c, j + c));
|
| 382 |
+
}
|
| 383 |
+
if (km > 0) {
|
| 384 |
+
const double reciprocal = 1.0 / at(kv, j);
|
| 385 |
+
for (int64_t i = 1; i <= km; ++i) at(kv + i, j) = reciprocal * at(kv + i, j);
|
| 386 |
+
for (int64_t c = 1; c <= ju - j; ++c) {
|
| 387 |
+
const double y = at(kv - c, j + c);
|
| 388 |
+
if (y != 0.0) {
|
| 389 |
+
const double temp = -1.0 * y;
|
| 390 |
+
for (int64_t i = 1; i <= km; ++i) at(kv - c + i, j + c) = at(kv - c + i, j + c) + at(kv + i, j) * temp;
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
// Forward: L, with the row interchanges.
|
| 396 |
+
for (int64_t j = 0; j + 1 < n; ++j) {
|
| 397 |
+
const int64_t lm = std::min(kl, n - 1 - j);
|
| 398 |
+
if (pivots[j] != j) std::swap(b[pivots[j]], b[j]);
|
| 399 |
+
if (b[j] != 0.0) {
|
| 400 |
+
const double temp = -1.0 * b[j];
|
| 401 |
+
for (int64_t i = 1; i <= lm; ++i) b[j + i] = b[j + i] + at(kv + i, j) * temp;
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
// Backward: U, a band of width kl+ku above the diagonal (dtbsv).
|
| 405 |
+
const int64_t k = kl + ku;
|
| 406 |
+
for (int64_t j = n - 1; j >= 0; --j) {
|
| 407 |
+
if (b[j] != 0.0) {
|
| 408 |
+
b[j] = b[j] / at(k, j);
|
| 409 |
+
const double temp = b[j];
|
| 410 |
+
for (int64_t i = j - 1; i >= std::max<int64_t>(0, j - k); --i) b[i] = b[i] - temp * at(k + i - j, j);
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
struct RotationSpline {
|
| 416 |
+
Vec times;
|
| 417 |
+
std::vector<Quat> rotations;
|
| 418 |
+
Vec coefficients; // ppoly layout, 3 columns
|
| 419 |
+
|
| 420 |
+
RotationSpline(const Vec& knots, const std::vector<Quat>& raw) : times(knots) {
|
| 421 |
+
const int64_t n = static_cast<int64_t>(knots.size());
|
| 422 |
+
rotations.reserve(n);
|
| 423 |
+
for (const Quat& q : raw) rotations.push_back(sp_from_quat(q));
|
| 424 |
+
const int64_t m = n - 1;
|
| 425 |
+
Vec dt(m), rotvecs(m * 3), rates(m * 3), rotvecs_dot;
|
| 426 |
+
for (int64_t k = 0; k < m; ++k) {
|
| 427 |
+
dt[k] = times[k + 1] - times[k];
|
| 428 |
+
sp_as_rotvec(sp_compose(sp_inv(rotations[k]), rotations[k + 1]), &rotvecs[k * 3]);
|
| 429 |
+
for (int a = 0; a < 3; ++a) rates[k * 3 + a] = rotvecs[k * 3 + a] / dt[k];
|
| 430 |
+
}
|
| 431 |
+
if (n == 2) {
|
| 432 |
+
rotvecs_dot = rates;
|
| 433 |
+
} else {
|
| 434 |
+
solve_rates(dt, rates, rotvecs, rotvecs_dot);
|
| 435 |
+
}
|
| 436 |
+
coefficients.assign(m * 4 * 3, 0.0);
|
| 437 |
+
for (int64_t k = 0; k < m; ++k) {
|
| 438 |
+
for (int a = 0; a < 3; ++a) {
|
| 439 |
+
const double rv = rotvecs[k * 3 + a], ar = rates[k * 3 + a], rd = rotvecs_dot[k * 3 + a];
|
| 440 |
+
coefficients[(k * 4 + 0) * 3 + a] = (-2.0 * rv + dt[k] * ar + dt[k] * rd) / std::pow(dt[k], 3.0);
|
| 441 |
+
coefficients[(k * 4 + 1) * 3 + a] = (3.0 * rv - 2.0 * dt[k] * ar - dt[k] * rd) / (dt[k] * dt[k]);
|
| 442 |
+
coefficients[(k * 4 + 2) * 3 + a] = ar;
|
| 443 |
+
coefficients[(k * 4 + 3) * 3 + a] = 0.0;
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
// scipy RotationSpline._solve_for_angular_rates; ``rates`` comes back as the
|
| 449 |
+
// knot angular rates, ``rotvecs_dot`` as the segment rotation-vector rates.
|
| 450 |
+
static void solve_rates(const Vec& dt, Vec& rates, const Vec& rotvecs, Vec& rotvecs_dot) {
|
| 451 |
+
const int64_t m = static_cast<int64_t>(dt.size());
|
| 452 |
+
const double first[3] = {rates[0], rates[1], rates[2]};
|
| 453 |
+
std::vector<Mat3> a(m), a_inv(m);
|
| 454 |
+
for (int64_t k = 0; k < m; ++k) {
|
| 455 |
+
a[k] = rate_to_rotvec_dot(&rotvecs[k * 3]);
|
| 456 |
+
a_inv[k] = rotvec_dot_to_rate(&rotvecs[k * 3]);
|
| 457 |
+
}
|
| 458 |
+
// Block-tridiagonal system of m-1 unknown 3-vectors in (5, 5) band form.
|
| 459 |
+
const int64_t size = 3 * (m - 1), kl = 5, ku = 5, ldab = 2 * kl + ku + 1;
|
| 460 |
+
Vec band(ldab * size, 0.0);
|
| 461 |
+
auto set = [&](int64_t i, int64_t j, double value) { band[(kl + ku + i - j) + ldab * j] = value; };
|
| 462 |
+
for (int64_t k = 0; k < m - 2; ++k) {
|
| 463 |
+
for (int r = 0; r < 3; ++r) {
|
| 464 |
+
for (int c = 0; c < 3; ++c) {
|
| 465 |
+
set(3 * (k + 1) + r, 3 * k + c, 2.0 * a_inv[k + 1][r * 3 + c] / dt[k + 1]);
|
| 466 |
+
set(3 * k + r, 3 * (k + 1) + c, 2.0 * a[k + 1][r * 3 + c] / dt[k + 1]);
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
for (int64_t k = 0; k < m - 1; ++k) {
|
| 471 |
+
const double diagonal = 4.0 * (1.0 / dt[k] + 1.0 / dt[k + 1]);
|
| 472 |
+
for (int r = 0; r < 3; ++r) set(3 * k + r, 3 * k + r, diagonal);
|
| 473 |
+
}
|
| 474 |
+
Vec b0(size);
|
| 475 |
+
for (int64_t k = 0; k < m - 1; ++k) {
|
| 476 |
+
for (int r = 0; r < 3; ++r)
|
| 477 |
+
b0[3 * k + r] = 6.0 * (rotvecs[k * 3 + r] * std::pow(dt[k], -2.0) +
|
| 478 |
+
rotvecs[(k + 1) * 3 + r] * std::pow(dt[k + 1], -2.0));
|
| 479 |
+
}
|
| 480 |
+
double product[3];
|
| 481 |
+
matvec(a_inv[0], first, product);
|
| 482 |
+
for (int r = 0; r < 3; ++r) b0[r] -= 2.0 / dt[0] * product[r];
|
| 483 |
+
matvec(a[m - 1], &rates[(m - 1) * 3], product);
|
| 484 |
+
for (int r = 0; r < 3; ++r) b0[size - 3 + r] -= 2.0 / dt[m - 1] * product[r];
|
| 485 |
+
|
| 486 |
+
rotvecs_dot.assign(m * 3, 0.0);
|
| 487 |
+
for (int iteration = 0; iteration < 10; ++iteration) {
|
| 488 |
+
for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]);
|
| 489 |
+
Vec b(size);
|
| 490 |
+
for (int64_t k = 0; k < m - 1; ++k) {
|
| 491 |
+
double delta_beta[3];
|
| 492 |
+
nonlinear_term(&rotvecs[k * 3], &rotvecs_dot[k * 3], delta_beta);
|
| 493 |
+
for (int r = 0; r < 3; ++r) b[3 * k + r] = b0[3 * k + r] - delta_beta[r];
|
| 494 |
+
}
|
| 495 |
+
Vec work = band;
|
| 496 |
+
gbsv(size, kl, ku, work, b);
|
| 497 |
+
bool converged = true;
|
| 498 |
+
for (int64_t i = 0; i < size; ++i) {
|
| 499 |
+
const double delta = std::fabs(b[i] - rates[i]);
|
| 500 |
+
rates[i] = b[i];
|
| 501 |
+
if (!(delta < 1e-9 * (1.0 + std::fabs(b[i])))) converged = false;
|
| 502 |
+
}
|
| 503 |
+
if (converged) break;
|
| 504 |
+
}
|
| 505 |
+
for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]);
|
| 506 |
+
// Knot rates: the fixed first rate, then the solved interior ones.
|
| 507 |
+
Vec shifted(m * 3);
|
| 508 |
+
for (int r = 0; r < 3; ++r) shifted[r] = first[r];
|
| 509 |
+
for (int64_t i = 0; i < 3 * (m - 1); ++i) shifted[3 + i] = rates[i];
|
| 510 |
+
rates = shifted;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
Quat operator()(double t) const {
|
| 514 |
+
const int64_t m = static_cast<int64_t>(times.size()) - 1;
|
| 515 |
+
const int64_t k = ppoly_interval(times, t);
|
| 516 |
+
const double s = t - times[k];
|
| 517 |
+
double rotvec[3];
|
| 518 |
+
for (int a = 0; a < 3; ++a) {
|
| 519 |
+
const double coeff[4] = {coefficients[(k * 4 + 0) * 3 + a], coefficients[(k * 4 + 1) * 3 + a],
|
| 520 |
+
coefficients[(k * 4 + 2) * 3 + a], coefficients[(k * 4 + 3) * 3 + a]};
|
| 521 |
+
rotvec[a] = ppoly_value(coeff, s);
|
| 522 |
+
}
|
| 523 |
+
int64_t index = static_cast<int64_t>(std::upper_bound(times.begin(), times.end(), t) - times.begin()) - 1;
|
| 524 |
+
index = std::min(std::max<int64_t>(index, 0), m - 1);
|
| 525 |
+
return sp_compose(rotations[index], sp_from_rotvec(rotvec));
|
| 526 |
+
}
|
| 527 |
+
};
|
| 528 |
+
|
| 529 |
+
Quat slerp(const Quat& left, const Quat& right, double weight) {
|
| 530 |
+
const Quat first = so3_normalize(left);
|
| 531 |
+
Quat second = so3_normalize(right);
|
| 532 |
+
double dot = first.x * second.x + first.y * second.y + first.z * second.z + first.w * second.w;
|
| 533 |
+
if (dot < 0.0) second = {-second.x, -second.y, -second.z, -second.w};
|
| 534 |
+
dot = std::min(std::max(std::fabs(dot), 0.0), 1.0);
|
| 535 |
+
Quat out;
|
| 536 |
+
if (dot > 0.9995) {
|
| 537 |
+
out = {first.x + weight * (second.x - first.x), first.y + weight * (second.y - first.y),
|
| 538 |
+
first.z + weight * (second.z - first.z), first.w + weight * (second.w - first.w)};
|
| 539 |
+
} else {
|
| 540 |
+
const double angle = std::acos(dot), denominator = std::sin(angle);
|
| 541 |
+
const double a = std::sin((1.0 - weight) * angle) / denominator, b = std::sin(weight * angle) / denominator;
|
| 542 |
+
out = {a * first.x + b * second.x, a * first.y + b * second.y, a * first.z + b * second.z,
|
| 543 |
+
a * first.w + b * second.w};
|
| 544 |
+
}
|
| 545 |
+
return so3_normalize(out);
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
// ---- runtime resample_uniform ----
|
| 549 |
+
|
| 550 |
+
enum Kind { kLinear = 0, kHold = 1, kPose = 2 };
|
| 551 |
+
enum Method { kLinearMethod = 0, kPchip = 1, kCubic = 2 };
|
| 552 |
+
|
| 553 |
+
int64_t resampled_horizon(int64_t length, double source_hz, double target_hz) {
|
| 554 |
+
return std::max<int64_t>(1, static_cast<int64_t>(std::floor(length * target_hz / source_hz + 0.5)));
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
int64_t output_width(int kind, int64_t width) { return kind == kPose ? 7 : width; }
|
| 558 |
+
|
| 559 |
+
// One row: float32 [T, D] in, float32 [count, D'] out.
|
| 560 |
+
void resample_row(const float* input, int64_t steps, int64_t width, double source_hz, double target_hz,
|
| 561 |
+
int kind, int64_t count, bool identity, int method, float* output) {
|
| 562 |
+
Vec values(steps * width);
|
| 563 |
+
for (int64_t i = 0; i < steps * width; ++i) values[i] = input[i];
|
| 564 |
+
const int64_t out_width = output_width(kind, width);
|
| 565 |
+
if (identity) {
|
| 566 |
+
for (int64_t i = 0; i < steps * width; ++i) output[i] = static_cast<float>(values[i]);
|
| 567 |
+
return;
|
| 568 |
+
}
|
| 569 |
+
Vec target_time(count), source_time(steps), interpolation_time(count);
|
| 570 |
+
for (int64_t i = 0; i < count; ++i) target_time[i] = static_cast<double>(i) / target_hz;
|
| 571 |
+
for (int64_t i = 0; i < steps; ++i) source_time[i] = static_cast<double>(i) / source_hz;
|
| 572 |
+
std::vector<int64_t> left(count), right(count);
|
| 573 |
+
Vec weight(count);
|
| 574 |
+
for (int64_t i = 0; i < count; ++i) {
|
| 575 |
+
interpolation_time[i] = std::min(target_time[i], source_time[steps - 1]);
|
| 576 |
+
const double position = target_time[i] * source_hz;
|
| 577 |
+
left[i] = std::min(static_cast<int64_t>(std::floor(position)), steps - 1);
|
| 578 |
+
right[i] = std::min(left[i] + 1, steps - 1);
|
| 579 |
+
weight[i] = position - static_cast<double>(left[i]);
|
| 580 |
+
}
|
| 581 |
+
Vec result(count * out_width);
|
| 582 |
+
if (kind == kHold) {
|
| 583 |
+
for (int64_t i = 0; i < count; ++i)
|
| 584 |
+
for (int64_t a = 0; a < width; ++a) result[i * out_width + a] = values[left[i] * width + a];
|
| 585 |
+
} else {
|
| 586 |
+
const bool pose = kind == kPose;
|
| 587 |
+
const int64_t cols = pose ? 3 : width;
|
| 588 |
+
Vec continuous(steps * cols);
|
| 589 |
+
for (int64_t t = 0; t < steps; ++t)
|
| 590 |
+
for (int64_t a = 0; a < cols; ++a) continuous[t * cols + a] = values[t * width + a];
|
| 591 |
+
if (steps == 1) {
|
| 592 |
+
for (int64_t i = 0; i < count; ++i)
|
| 593 |
+
for (int64_t a = 0; a < cols; ++a) result[i * out_width + a] = continuous[a];
|
| 594 |
+
} else if (method == kPchip) {
|
| 595 |
+
const Vec c = hermite_coefficients(source_time, continuous, pchip_slopes(source_time, continuous, cols), cols);
|
| 596 |
+
ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data());
|
| 597 |
+
} else if (method == kCubic) {
|
| 598 |
+
const Vec c = hermite_coefficients(source_time, continuous, natural_cubic_slopes(source_time, continuous, cols), cols);
|
| 599 |
+
ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data());
|
| 600 |
+
// Guard: fall back to PCHIP on axes that materially leave their envelope.
|
| 601 |
+
std::vector<int64_t> fallback;
|
| 602 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 603 |
+
double lower = continuous[a], upper = continuous[a];
|
| 604 |
+
for (int64_t t = 1; t < steps; ++t) {
|
| 605 |
+
lower = std::min(lower, continuous[t * cols + a]);
|
| 606 |
+
upper = std::max(upper, continuous[t * cols + a]);
|
| 607 |
+
}
|
| 608 |
+
const double tolerance = 1e-9 + 0.05 * (upper - lower);
|
| 609 |
+
for (int64_t i = 0; i < count; ++i) {
|
| 610 |
+
const double v = result[i * out_width + a];
|
| 611 |
+
if (v < lower - tolerance || v > upper + tolerance) { fallback.push_back(a); break; }
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
if (!fallback.empty()) {
|
| 615 |
+
const int64_t f = static_cast<int64_t>(fallback.size());
|
| 616 |
+
Vec subset(steps * f);
|
| 617 |
+
for (int64_t t = 0; t < steps; ++t)
|
| 618 |
+
for (int64_t j = 0; j < f; ++j) subset[t * f + j] = continuous[t * cols + fallback[j]];
|
| 619 |
+
const Vec c = hermite_coefficients(source_time, subset, pchip_slopes(source_time, subset, f), f);
|
| 620 |
+
Vec sampled(count * f);
|
| 621 |
+
ppoly_evaluate(source_time, c, f, interpolation_time, f, 0, sampled.data());
|
| 622 |
+
for (int64_t i = 0; i < count; ++i)
|
| 623 |
+
for (int64_t j = 0; j < f; ++j) result[i * out_width + fallback[j]] = sampled[i * f + j];
|
| 624 |
+
}
|
| 625 |
+
} else {
|
| 626 |
+
for (int64_t i = 0; i < count; ++i)
|
| 627 |
+
for (int64_t a = 0; a < cols; ++a) {
|
| 628 |
+
const double lo = values[left[i] * width + a], hi = values[right[i] * width + a];
|
| 629 |
+
result[i * out_width + a] = lo + weight[i] * (hi - lo);
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
if (pose) {
|
| 633 |
+
std::vector<Quat> quaternions(steps);
|
| 634 |
+
for (int64_t t = 0; t < steps; ++t)
|
| 635 |
+
quaternions[t] = {values[t * width + 3], values[t * width + 4], values[t * width + 5], values[t * width + 6]};
|
| 636 |
+
auto store = [&](int64_t i, const Quat& q) {
|
| 637 |
+
result[i * out_width + 3] = q.x;
|
| 638 |
+
result[i * out_width + 4] = q.y;
|
| 639 |
+
result[i * out_width + 5] = q.z;
|
| 640 |
+
result[i * out_width + 6] = q.w;
|
| 641 |
+
};
|
| 642 |
+
if (steps == 1) {
|
| 643 |
+
for (int64_t i = 0; i < count; ++i) store(i, quaternions[0]);
|
| 644 |
+
} else if (method == kLinearMethod) {
|
| 645 |
+
for (int64_t i = 0; i < count; ++i) store(i, slerp(quaternions[left[i]], quaternions[right[i]], weight[i]));
|
| 646 |
+
} else {
|
| 647 |
+
const RotationSpline spline(source_time, quaternions);
|
| 648 |
+
for (int64_t i = 0; i < count; ++i) store(i, spline(interpolation_time[i]));
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
for (int64_t i = 0; i < count * out_width; ++i) output[i] = static_cast<float>(result[i]);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
int parse_kind(const std::string& kind) {
|
| 656 |
+
if (kind == "linear") return kLinear;
|
| 657 |
+
if (kind == "hold") return kHold;
|
| 658 |
+
if (kind == "pose_xyzw") return kPose;
|
| 659 |
+
throw std::invalid_argument("unsupported resampling kind");
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
int parse_method(const std::string& method) {
|
| 663 |
+
if (method == "linear") return kLinearMethod;
|
| 664 |
+
if (method == "pchip") return kPchip;
|
| 665 |
+
if (method == "cubic") return kCubic;
|
| 666 |
+
throw std::invalid_argument("method must be one of ['cubic', 'linear', 'pchip']");
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
using F32 = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 670 |
+
|
| 671 |
+
void check_finite(const F32& values) {
|
| 672 |
+
const float* data = values.data();
|
| 673 |
+
for (py::ssize_t i = 0; i < values.size(); ++i)
|
| 674 |
+
if (!std::isfinite(data[i])) throw std::invalid_argument("values must be a non-empty finite [T, D] array");
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
F32 resample_uniform_batch(const F32& values, double source_hz, double target_hz, const std::string& kind_name,
|
| 678 |
+
int64_t target_length, const std::string& method_name, int threads) {
|
| 679 |
+
if (values.ndim() != 3 || values.shape(0) < 1 || values.shape(1) < 1)
|
| 680 |
+
throw std::invalid_argument("values must have non-empty shape [B, T, D]");
|
| 681 |
+
if (!(std::isfinite(source_hz) && source_hz > 0.0)) throw std::invalid_argument("source_hz must be positive and finite");
|
| 682 |
+
if (!(std::isfinite(target_hz) && target_hz > 0.0)) throw std::invalid_argument("target_hz must be positive and finite");
|
| 683 |
+
check_finite(values);
|
| 684 |
+
const int kind = parse_kind(kind_name), method = parse_method(method_name);
|
| 685 |
+
const int64_t batch = values.shape(0), steps = values.shape(1), width = values.shape(2);
|
| 686 |
+
if (kind == kPose && width != 7) throw std::invalid_argument("pose_xyzw values must have shape [T, 7]");
|
| 687 |
+
const int64_t standard = resampled_horizon(steps, source_hz, target_hz);
|
| 688 |
+
const bool identity = source_hz == target_hz && (target_length < 0 || target_length == standard);
|
| 689 |
+
const int64_t count = identity ? steps : (target_length < 0 ? standard : target_length);
|
| 690 |
+
if (count < 1) throw std::invalid_argument("target_length must be a positive integer");
|
| 691 |
+
const int64_t out_width = output_width(kind, width);
|
| 692 |
+
F32 output({batch, count, out_width});
|
| 693 |
+
const float* in = values.data();
|
| 694 |
+
float* out = output.mutable_data();
|
| 695 |
+
{
|
| 696 |
+
py::gil_scoped_release release;
|
| 697 |
+
ac2::parallel_for(batch, threads, [&](int64_t row) {
|
| 698 |
+
resample_row(in + row * steps * width, steps, width, source_hz, target_hz, kind, count, identity, method,
|
| 699 |
+
out + row * count * out_width);
|
| 700 |
+
});
|
| 701 |
+
}
|
| 702 |
+
return output;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
// Elementwise pose maps over [B, T, D] float32 arrays.
|
| 706 |
+
template <class Row>
|
| 707 |
+
F32 map_rows(const F32& values, int64_t in_width, int64_t out_width, int threads, Row&& row) {
|
| 708 |
+
if (values.ndim() != 3 || values.shape(2) != in_width || values.shape(1) < 1)
|
| 709 |
+
throw std::invalid_argument("expected shape [B, T, " + std::to_string(in_width) + "]");
|
| 710 |
+
check_finite(values);
|
| 711 |
+
const int64_t batch = values.shape(0), steps = values.shape(1);
|
| 712 |
+
F32 output({batch, steps, out_width});
|
| 713 |
+
const float* in = values.data();
|
| 714 |
+
float* out = output.mutable_data();
|
| 715 |
+
{
|
| 716 |
+
py::gil_scoped_release release;
|
| 717 |
+
ac2::parallel_for(batch, threads, [&](int64_t b) {
|
| 718 |
+
row(in + b * steps * in_width, steps, out + b * steps * out_width);
|
| 719 |
+
});
|
| 720 |
+
}
|
| 721 |
+
return output;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
// runtime integrate_eef_deltas: [T, 6] body increments -> [T, 7] absolute poses.
|
| 725 |
+
F32 integrate_eef_deltas_batch(const F32& deltas, int threads) {
|
| 726 |
+
return map_rows(deltas, 6, 7, threads, [](const float* in, int64_t steps, float* out) {
|
| 727 |
+
double position[3] = {0.0, 0.0, 0.0};
|
| 728 |
+
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 729 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 730 |
+
for (int a = 0; a < 3; ++a) {
|
| 731 |
+
position[a] = position[a] + static_cast<double>(in[t * 6 + a]);
|
| 732 |
+
out[t * 7 + a] = static_cast<float>(position[a]);
|
| 733 |
+
}
|
| 734 |
+
const Quat increment = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]);
|
| 735 |
+
current = so3_multiply(current, increment);
|
| 736 |
+
out[t * 7 + 3] = static_cast<float>(current.x);
|
| 737 |
+
out[t * 7 + 4] = static_cast<float>(current.y);
|
| 738 |
+
out[t * 7 + 5] = static_cast<float>(current.z);
|
| 739 |
+
out[t * 7 + 6] = static_cast<float>(current.w);
|
| 740 |
+
}
|
| 741 |
+
});
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// runtime eef_pose_from_components(..., "rotation_vector").
|
| 745 |
+
F32 eef_pose_from_rotvec_batch(const F32& poses, int threads) {
|
| 746 |
+
return map_rows(poses, 6, 7, threads, [](const float* in, int64_t steps, float* out) {
|
| 747 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 748 |
+
for (int a = 0; a < 3; ++a) out[t * 7 + a] = in[t * 6 + a];
|
| 749 |
+
const Quat q = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]);
|
| 750 |
+
out[t * 7 + 3] = static_cast<float>(q.x);
|
| 751 |
+
out[t * 7 + 4] = static_cast<float>(q.y);
|
| 752 |
+
out[t * 7 + 5] = static_cast<float>(q.z);
|
| 753 |
+
out[t * 7 + 6] = static_cast<float>(q.w);
|
| 754 |
+
}
|
| 755 |
+
});
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// runtime eef_pose_absolute: [T, 7] xyz + xyzw -> [T, 6] xyz + rotation vector.
|
| 759 |
+
F32 eef_pose_absolute_batch(const F32& poses, int threads) {
|
| 760 |
+
return map_rows(poses, 7, 6, threads, [](const float* in, int64_t steps, float* out) {
|
| 761 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 762 |
+
for (int a = 0; a < 3; ++a) out[t * 6 + a] = in[t * 7 + a];
|
| 763 |
+
double rotvec[3];
|
| 764 |
+
so3_to_rotvec({in[t * 7 + 3], in[t * 7 + 4], in[t * 7 + 5], in[t * 7 + 6]}, rotvec);
|
| 765 |
+
for (int a = 0; a < 3; ++a) out[t * 6 + 3 + a] = static_cast<float>(rotvec[a]);
|
| 766 |
+
}
|
| 767 |
+
});
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
// runtime eef_pose_deltas: local [T, 7] path -> [T, 6] body increments.
|
| 771 |
+
F32 eef_pose_deltas_batch(const F32& poses, int threads) {
|
| 772 |
+
return map_rows(poses, 7, 6, threads, [](const float* in, int64_t steps, float* out) {
|
| 773 |
+
Quat previous{0.0, 0.0, 0.0, 1.0};
|
| 774 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 775 |
+
for (int a = 0; a < 3; ++a) {
|
| 776 |
+
const double before = t == 0 ? 0.0 : static_cast<double>(in[(t - 1) * 7 + a]);
|
| 777 |
+
out[t * 6 + a] = static_cast<float>(static_cast<double>(in[t * 7 + a]) - before);
|
| 778 |
+
}
|
| 779 |
+
const Quat q = so3_normalize({in[t * 7 + 3], in[t * 7 + 4], in[t * 7 + 5], in[t * 7 + 6]});
|
| 780 |
+
double rotvec[3];
|
| 781 |
+
so3_to_rotvec(so3_multiply({-previous.x, -previous.y, -previous.z, previous.w}, q), rotvec);
|
| 782 |
+
for (int a = 0; a < 3; ++a) out[t * 6 + 3 + a] = static_cast<float>(rotvec[a]);
|
| 783 |
+
previous = q;
|
| 784 |
+
}
|
| 785 |
+
});
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
// Re-express per-step rotation increments between world-frame (left) and
|
| 789 |
+
// body-frame (right) multiplication, as scipy ``Rotation`` composes them.
|
| 790 |
+
F32 rotvec_left_to_right_batch(const F32& increments, int threads) {
|
| 791 |
+
return map_rows(increments, 3, 3, threads, [](const float* in, int64_t steps, float* out) {
|
| 792 |
+
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 793 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 794 |
+
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
| 795 |
+
const Quat following = sp_compose(sp_from_rotvec(v), current);
|
| 796 |
+
double rotvec[3];
|
| 797 |
+
sp_as_rotvec(sp_compose(sp_inv(current), following), rotvec);
|
| 798 |
+
for (int a = 0; a < 3; ++a) out[t * 3 + a] = static_cast<float>(rotvec[a]);
|
| 799 |
+
current = following;
|
| 800 |
+
}
|
| 801 |
+
});
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
F32 rotvec_right_to_left_batch(const F32& increments, int threads) {
|
| 805 |
+
return map_rows(increments, 3, 3, threads, [](const float* in, int64_t steps, float* out) {
|
| 806 |
+
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 807 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 808 |
+
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
| 809 |
+
const Quat following = sp_compose(current, sp_from_rotvec(v));
|
| 810 |
+
double rotvec[3];
|
| 811 |
+
sp_as_rotvec(sp_compose(following, sp_inv(current)), rotvec);
|
| 812 |
+
for (int a = 0; a < 3; ++a) out[t * 3 + a] = static_cast<float>(rotvec[a]);
|
| 813 |
+
current = following;
|
| 814 |
+
}
|
| 815 |
+
});
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
} // namespace
|
| 819 |
+
|
| 820 |
+
void register_resample(py::module_& module) {
|
| 821 |
+
module.def("resample_uniform_batch", &resample_uniform_batch, py::arg("values"), py::arg("source_hz"),
|
| 822 |
+
py::arg("target_hz"), py::arg("kind"), py::arg("target_length") = -1,
|
| 823 |
+
py::arg("method") = "linear", py::arg("threads") = 0,
|
| 824 |
+
"runtime resample_uniform over each [T, D] row of a [B, T, D] batch.");
|
| 825 |
+
module.def("integrate_eef_deltas_batch", &integrate_eef_deltas_batch, py::arg("deltas"), py::arg("threads") = 0);
|
| 826 |
+
module.def("eef_pose_from_rotvec_batch", &eef_pose_from_rotvec_batch, py::arg("poses"), py::arg("threads") = 0);
|
| 827 |
+
module.def("eef_pose_absolute_batch", &eef_pose_absolute_batch, py::arg("poses"), py::arg("threads") = 0);
|
| 828 |
+
module.def("eef_pose_deltas_batch", &eef_pose_deltas_batch, py::arg("poses"), py::arg("threads") = 0);
|
| 829 |
+
module.def("rotvec_left_to_right_batch", &rotvec_left_to_right_batch, py::arg("increments"),
|
| 830 |
+
py::arg("threads") = 0);
|
| 831 |
+
module.def("rotvec_right_to_left_batch", &rotvec_right_to_left_batch, py::arg("increments"),
|
| 832 |
+
py::arg("threads") = 0);
|
| 833 |
+
}
|
actioncodec2_kernels/setbpe.cpp
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Set-BPE serialization: quantized cell trajectories <-> token streams.
|
| 2 |
+
//
|
| 3 |
+
// A port of the runtime's SpatialVocabulary / TemporalVocabulary (Python,
|
| 4 |
+
// ``tokenization/setbpe_{spatial,temporal}.py``). Every merge is applied from
|
| 5 |
+
// a priority queue ordered exactly as the Python heaps order their tuples, so
|
| 6 |
+
// the token streams are identical, not merely equivalent.
|
| 7 |
+
|
| 8 |
+
#include <pybind11/numpy.h>
|
| 9 |
+
#include <pybind11/pybind11.h>
|
| 10 |
+
#include <pybind11/stl.h>
|
| 11 |
+
|
| 12 |
+
#include <algorithm>
|
| 13 |
+
#include <cstdint>
|
| 14 |
+
#include <functional>
|
| 15 |
+
#include <queue>
|
| 16 |
+
#include <stdexcept>
|
| 17 |
+
#include <string>
|
| 18 |
+
#include <tuple>
|
| 19 |
+
#include <unordered_map>
|
| 20 |
+
#include <utility>
|
| 21 |
+
#include <vector>
|
| 22 |
+
|
| 23 |
+
#include "parallel.hpp"
|
| 24 |
+
|
| 25 |
+
namespace py = pybind11;
|
| 26 |
+
|
| 27 |
+
namespace {
|
| 28 |
+
|
| 29 |
+
int lowest_bit(uint64_t mask) { return mask ? __builtin_ctzll(mask) : -1; }
|
| 30 |
+
|
| 31 |
+
uint64_t pair_key(int64_t left, int64_t right) {
|
| 32 |
+
return (static_cast<uint64_t>(left) << 32) | static_cast<uint32_t>(right);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
struct Rule {
|
| 36 |
+
int64_t child;
|
| 37 |
+
int64_t rank;
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
class SetBPE {
|
| 41 |
+
public:
|
| 42 |
+
SetBPE(std::vector<int64_t> bins, int64_t lookahead,
|
| 43 |
+
const std::vector<std::pair<int64_t, int64_t>>& spatial_merges,
|
| 44 |
+
const std::vector<std::tuple<int64_t, int64_t, int64_t>>& temporal_merges)
|
| 45 |
+
: bins_(std::move(bins)), lookahead_(lookahead) {
|
| 46 |
+
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 47 |
+
if (dimension < 1 || dimension > 63) throw std::invalid_argument("bins must have 1..63 slots");
|
| 48 |
+
if (lookahead_ < 1) throw std::invalid_argument("lookahead must be positive");
|
| 49 |
+
offsets_.assign(dimension + 1, 0);
|
| 50 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 51 |
+
if (bins_[d] < 1) throw std::invalid_argument("every dimension needs at least one bin");
|
| 52 |
+
offsets_[d + 1] = offsets_[d] + bins_[d];
|
| 53 |
+
}
|
| 54 |
+
atom_count_ = offsets_[dimension];
|
| 55 |
+
spatial_size_ = atom_count_ + static_cast<int64_t>(spatial_merges.size());
|
| 56 |
+
support_.assign(spatial_size_, 0);
|
| 57 |
+
expansion_.assign(spatial_size_ * dimension, -1);
|
| 58 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 59 |
+
for (int64_t v = 0; v < bins_[d]; ++v) {
|
| 60 |
+
const int64_t token = offsets_[d] + v;
|
| 61 |
+
support_[token] = uint64_t{1} << d;
|
| 62 |
+
expansion_[token * dimension + d] = v;
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
for (size_t rank = 0; rank < spatial_merges.size(); ++rank) {
|
| 66 |
+
const auto [left, right] = spatial_merges[rank];
|
| 67 |
+
const int64_t token = atom_count_ + static_cast<int64_t>(rank);
|
| 68 |
+
if (left < 0 || right < 0 || left >= token || right >= token)
|
| 69 |
+
throw std::invalid_argument("a merge rule must reference already-created tokens");
|
| 70 |
+
if (support_[left] & support_[right])
|
| 71 |
+
throw std::invalid_argument("a merge rule joins two overlapping supports");
|
| 72 |
+
support_[token] = support_[left] | support_[right];
|
| 73 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 74 |
+
expansion_[token * dimension + d] =
|
| 75 |
+
std::max(expansion_[left * dimension + d], expansion_[right * dimension + d]);
|
| 76 |
+
}
|
| 77 |
+
spatial_rules_[pair_key(std::min(left, right), std::max(left, right))] =
|
| 78 |
+
Rule{token, static_cast<int64_t>(rank)};
|
| 79 |
+
}
|
| 80 |
+
spatial_anchor_.resize(spatial_size_);
|
| 81 |
+
for (int64_t token = 0; token < spatial_size_; ++token)
|
| 82 |
+
spatial_anchor_[token] = lowest_bit(support_[token]);
|
| 83 |
+
|
| 84 |
+
// Rooted temporal tokens: a pattern of (time offset, spatial token) pairs.
|
| 85 |
+
vocab_size_ = spatial_size_ + static_cast<int64_t>(temporal_merges.size());
|
| 86 |
+
patterns_.resize(vocab_size_);
|
| 87 |
+
root_.resize(vocab_size_);
|
| 88 |
+
for (int64_t token = 0; token < spatial_size_; ++token) {
|
| 89 |
+
patterns_[token] = {{0, token}};
|
| 90 |
+
root_[token] = token;
|
| 91 |
+
}
|
| 92 |
+
for (size_t rank = 0; rank < temporal_merges.size(); ++rank) {
|
| 93 |
+
const auto [left, right, offset] = temporal_merges[rank];
|
| 94 |
+
const int64_t child = spatial_size_ + static_cast<int64_t>(rank);
|
| 95 |
+
if (left < 0 || right < 0 || left >= child || right >= child)
|
| 96 |
+
throw std::invalid_argument("a temporal merge must reference already-created tokens");
|
| 97 |
+
if (offset < 1 || offset >= lookahead_)
|
| 98 |
+
throw std::invalid_argument("a temporal merge offset must be in [1, lookahead)");
|
| 99 |
+
std::vector<std::pair<int64_t, int64_t>> pattern = patterns_[left];
|
| 100 |
+
for (const auto& [time, token] : patterns_[right]) pattern.emplace_back(time + offset, token);
|
| 101 |
+
std::sort(pattern.begin(), pattern.end());
|
| 102 |
+
const int64_t span = 1 + pattern.back().first;
|
| 103 |
+
if (span > lookahead_) throw std::invalid_argument("temporal merge exceeds the lookahead horizon");
|
| 104 |
+
std::vector<uint64_t> masks(span, 0);
|
| 105 |
+
int roots = 0;
|
| 106 |
+
for (const auto& [time, token] : pattern) {
|
| 107 |
+
if (masks[time] & support_[token])
|
| 108 |
+
throw std::invalid_argument("temporal merge joins overlapping spatial supports");
|
| 109 |
+
masks[time] |= support_[token];
|
| 110 |
+
roots += time == 0;
|
| 111 |
+
}
|
| 112 |
+
if (roots != 1)
|
| 113 |
+
throw std::invalid_argument("a rooted temporal token must have exactly one time-zero spatial token");
|
| 114 |
+
patterns_[child] = std::move(pattern);
|
| 115 |
+
root_[child] = root_[left];
|
| 116 |
+
const auto key = std::make_tuple(left, right, offset);
|
| 117 |
+
if (temporal_rules_.count(key)) throw std::invalid_argument("duplicate temporal merge rule");
|
| 118 |
+
temporal_rules_[key] = Rule{child, static_cast<int64_t>(rank)};
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
int64_t vocab_size() const { return vocab_size_; }
|
| 123 |
+
|
| 124 |
+
// One dense [T, D] trajectory with -1 for semantically absent slots.
|
| 125 |
+
std::vector<int64_t> encode(const int64_t* cells, int64_t steps) const {
|
| 126 |
+
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 127 |
+
if (steps < 1) throw std::invalid_argument("every episode must have at least one step");
|
| 128 |
+
uint64_t first_support = 0;
|
| 129 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 130 |
+
uint64_t support = 0;
|
| 131 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 132 |
+
const int64_t value = cells[t * dimension + d];
|
| 133 |
+
if (value < -1 || value >= bins_[d])
|
| 134 |
+
throw std::invalid_argument("episodes must contain -1 or an in-range cell index");
|
| 135 |
+
if (value >= 0) support |= uint64_t{1} << d;
|
| 136 |
+
}
|
| 137 |
+
if (!support) throw std::invalid_argument("every timestep needs at least one active coordinate");
|
| 138 |
+
if (t == 0) first_support = support;
|
| 139 |
+
else if (support != first_support)
|
| 140 |
+
throw std::invalid_argument("active dimensions must remain constant within an episode");
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Instances, as in the Python encoder: token, anchor step, liveness.
|
| 144 |
+
std::vector<int64_t> tokens, anchors;
|
| 145 |
+
std::vector<char> active;
|
| 146 |
+
std::vector<std::vector<std::pair<int64_t, int64_t>>> at(steps); // (token, instance)
|
| 147 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 148 |
+
for (int64_t token : spatial_encode(cells + t * dimension)) {
|
| 149 |
+
at[t].emplace_back(token, static_cast<int64_t>(tokens.size()));
|
| 150 |
+
tokens.push_back(token);
|
| 151 |
+
anchors.push_back(t);
|
| 152 |
+
active.push_back(1);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// (rank, anchor, root anchor dimension, left instance, right instance, child)
|
| 157 |
+
using Offer = std::tuple<int64_t, int64_t, int64_t, int64_t, int64_t, int64_t>;
|
| 158 |
+
std::priority_queue<Offer, std::vector<Offer>, std::greater<Offer>> frontier;
|
| 159 |
+
auto offer = [&](int64_t left_instance, int64_t right_instance) {
|
| 160 |
+
const auto found = temporal_rules_.find(std::make_tuple(
|
| 161 |
+
tokens[left_instance], tokens[right_instance],
|
| 162 |
+
anchors[right_instance] - anchors[left_instance]));
|
| 163 |
+
if (found == temporal_rules_.end()) return;
|
| 164 |
+
const Rule& rule = found->second;
|
| 165 |
+
frontier.emplace(rule.rank, anchors[left_instance],
|
| 166 |
+
spatial_anchor_[root_[rule.child]], left_instance, right_instance,
|
| 167 |
+
rule.child);
|
| 168 |
+
};
|
| 169 |
+
for (int64_t anchor = 0; anchor < steps; ++anchor) {
|
| 170 |
+
for (int64_t offset = 1; offset < std::min<int64_t>(lookahead_, steps - anchor); ++offset) {
|
| 171 |
+
for (const auto& [left_token, left_instance] : at[anchor]) {
|
| 172 |
+
for (const auto& [right_token, right_instance] : at[anchor + offset]) {
|
| 173 |
+
offer(left_instance, right_instance);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
auto erase = [&](int64_t step, int64_t token) {
|
| 179 |
+
auto& items = at[step];
|
| 180 |
+
for (auto it = items.begin(); it != items.end(); ++it) {
|
| 181 |
+
if (it->first == token) { items.erase(it); return; }
|
| 182 |
+
}
|
| 183 |
+
};
|
| 184 |
+
while (!frontier.empty()) {
|
| 185 |
+
const auto [rank, anchor, root_anchor, left_instance, right_instance, child] = frontier.top();
|
| 186 |
+
frontier.pop();
|
| 187 |
+
(void)rank; (void)root_anchor;
|
| 188 |
+
if (!active[left_instance] || !active[right_instance]) continue;
|
| 189 |
+
if (anchors[left_instance] != anchor) continue;
|
| 190 |
+
const auto resolved = temporal_rules_.find(std::make_tuple(
|
| 191 |
+
tokens[left_instance], tokens[right_instance], anchors[right_instance] - anchor));
|
| 192 |
+
if (resolved == temporal_rules_.end() || resolved->second.child != child) continue;
|
| 193 |
+
active[left_instance] = 0;
|
| 194 |
+
active[right_instance] = 0;
|
| 195 |
+
erase(anchor, tokens[left_instance]);
|
| 196 |
+
erase(anchors[right_instance], tokens[right_instance]);
|
| 197 |
+
const int64_t new_instance = static_cast<int64_t>(tokens.size());
|
| 198 |
+
tokens.push_back(child);
|
| 199 |
+
anchors.push_back(anchor);
|
| 200 |
+
active.push_back(1);
|
| 201 |
+
at[anchor].emplace_back(child, new_instance);
|
| 202 |
+
for (int64_t offset = 1; offset < lookahead_; ++offset) {
|
| 203 |
+
const int64_t future = anchor + offset;
|
| 204 |
+
if (future < steps) {
|
| 205 |
+
for (const auto& [token, other] : at[future]) offer(new_instance, other);
|
| 206 |
+
}
|
| 207 |
+
const int64_t past = anchor - offset;
|
| 208 |
+
if (past >= 0) {
|
| 209 |
+
for (const auto& [token, other] : at[past]) offer(other, new_instance);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
std::vector<std::tuple<int64_t, int64_t, int64_t>> ordered;
|
| 215 |
+
for (int64_t anchor = 0; anchor < steps; ++anchor) {
|
| 216 |
+
for (const auto& [token, instance] : at[anchor]) {
|
| 217 |
+
if (active[instance]) ordered.emplace_back(anchor, spatial_anchor_[root_[token]], token);
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
std::sort(ordered.begin(), ordered.end());
|
| 221 |
+
std::vector<int64_t> stream;
|
| 222 |
+
stream.reserve(ordered.size());
|
| 223 |
+
for (const auto& item : ordered) stream.push_back(std::get<2>(item));
|
| 224 |
+
return stream;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Tokens -> dense [T, D] cells (-1 outside ``full``), validating like the runtime.
|
| 228 |
+
std::vector<int64_t> decode(const int64_t* stream, int64_t length, uint64_t full) const {
|
| 229 |
+
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 230 |
+
std::vector<uint64_t> coverage{0};
|
| 231 |
+
std::vector<std::vector<int64_t>> spatial_tokens(1);
|
| 232 |
+
size_t front = 0;
|
| 233 |
+
for (int64_t index = 0; index < length; ++index) {
|
| 234 |
+
const int64_t token = stream[index];
|
| 235 |
+
if (token < 0 || token >= vocab_size_)
|
| 236 |
+
throw std::invalid_argument("temporal token " + std::to_string(token) + " is outside the vocabulary");
|
| 237 |
+
while (front < coverage.size() && coverage[front] == full) ++front;
|
| 238 |
+
if (front == coverage.size()) {
|
| 239 |
+
coverage.push_back(0);
|
| 240 |
+
spatial_tokens.emplace_back();
|
| 241 |
+
}
|
| 242 |
+
const uint64_t uncovered = full & ~coverage[front];
|
| 243 |
+
if (spatial_anchor_[root_[token]] != lowest_bit(uncovered))
|
| 244 |
+
throw std::invalid_argument("temporal stream violates root order at the earliest incomplete step");
|
| 245 |
+
for (const auto& [offset, spatial_token] : patterns_[token]) {
|
| 246 |
+
const size_t step = front + static_cast<size_t>(offset);
|
| 247 |
+
while (step >= coverage.size()) {
|
| 248 |
+
coverage.push_back(0);
|
| 249 |
+
spatial_tokens.emplace_back();
|
| 250 |
+
}
|
| 251 |
+
if (coverage[step] & support_[spatial_token])
|
| 252 |
+
throw std::invalid_argument("temporal stream contains overlapping spatial support");
|
| 253 |
+
coverage[step] |= support_[spatial_token];
|
| 254 |
+
spatial_tokens[step].push_back(spatial_token);
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
for (uint64_t mask : coverage) {
|
| 258 |
+
if (mask != full) throw std::invalid_argument("temporal stream ended with an incomplete control step");
|
| 259 |
+
}
|
| 260 |
+
std::vector<int64_t> cells(coverage.size() * dimension, -1);
|
| 261 |
+
for (size_t step = 0; step < coverage.size(); ++step) {
|
| 262 |
+
for (int64_t token : spatial_tokens[step]) {
|
| 263 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 264 |
+
const int64_t value = expansion_[token * dimension + d];
|
| 265 |
+
if (value >= 0) cells[step * dimension + d] = value;
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
return cells;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
private:
|
| 273 |
+
// Spatial Set-BPE of one dense row: merges in rank order, then anchor order.
|
| 274 |
+
std::vector<int64_t> spatial_encode(const int64_t* row) const {
|
| 275 |
+
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 276 |
+
std::vector<int64_t> present;
|
| 277 |
+
for (int64_t d = 0; d < dimension; ++d) {
|
| 278 |
+
if (row[d] >= 0) present.push_back(offsets_[d] + row[d]);
|
| 279 |
+
}
|
| 280 |
+
std::sort(present.begin(), present.end());
|
| 281 |
+
using Candidate = std::tuple<int64_t, int64_t, int64_t, int64_t>; // rank, child, left, right
|
| 282 |
+
std::priority_queue<Candidate, std::vector<Candidate>, std::greater<Candidate>> heap;
|
| 283 |
+
for (size_t i = 0; i < present.size(); ++i) {
|
| 284 |
+
for (size_t j = i + 1; j < present.size(); ++j) {
|
| 285 |
+
const auto found = spatial_rules_.find(pair_key(present[i], present[j]));
|
| 286 |
+
if (found != spatial_rules_.end())
|
| 287 |
+
heap.emplace(found->second.rank, found->second.child, present[i], present[j]);
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
std::vector<int64_t> tokens = present; // a small set; linear lookups are fastest
|
| 291 |
+
auto contains = [&](int64_t token) {
|
| 292 |
+
return std::find(tokens.begin(), tokens.end(), token) != tokens.end();
|
| 293 |
+
};
|
| 294 |
+
while (!heap.empty()) {
|
| 295 |
+
const auto [rank, child, left, right] = heap.top();
|
| 296 |
+
heap.pop();
|
| 297 |
+
(void)rank;
|
| 298 |
+
if (!contains(left) || !contains(right)) continue;
|
| 299 |
+
tokens.erase(std::find(tokens.begin(), tokens.end(), left));
|
| 300 |
+
tokens.erase(std::find(tokens.begin(), tokens.end(), right));
|
| 301 |
+
for (int64_t other : tokens) {
|
| 302 |
+
const int64_t low = std::min(child, other), high = std::max(child, other);
|
| 303 |
+
const auto found = spatial_rules_.find(pair_key(low, high));
|
| 304 |
+
if (found != spatial_rules_.end())
|
| 305 |
+
heap.emplace(found->second.rank, found->second.child, low, high);
|
| 306 |
+
}
|
| 307 |
+
tokens.push_back(child);
|
| 308 |
+
}
|
| 309 |
+
std::sort(tokens.begin(), tokens.end(), [&](int64_t a, int64_t b) {
|
| 310 |
+
return std::make_pair(spatial_anchor_[a], a) < std::make_pair(spatial_anchor_[b], b);
|
| 311 |
+
});
|
| 312 |
+
return tokens;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
struct TupleHash {
|
| 316 |
+
size_t operator()(const std::tuple<int64_t, int64_t, int64_t>& key) const {
|
| 317 |
+
const auto [a, b, c] = key;
|
| 318 |
+
return std::hash<uint64_t>()((static_cast<uint64_t>(a) * 1000003u) ^
|
| 319 |
+
(static_cast<uint64_t>(b) << 20) ^ static_cast<uint64_t>(c));
|
| 320 |
+
}
|
| 321 |
+
};
|
| 322 |
+
|
| 323 |
+
std::vector<int64_t> bins_;
|
| 324 |
+
int64_t lookahead_;
|
| 325 |
+
std::vector<int64_t> offsets_;
|
| 326 |
+
int64_t atom_count_ = 0, spatial_size_ = 0, vocab_size_ = 0;
|
| 327 |
+
std::vector<uint64_t> support_;
|
| 328 |
+
std::vector<int64_t> expansion_;
|
| 329 |
+
std::vector<int> spatial_anchor_;
|
| 330 |
+
std::unordered_map<uint64_t, Rule> spatial_rules_;
|
| 331 |
+
std::vector<std::vector<std::pair<int64_t, int64_t>>> patterns_;
|
| 332 |
+
std::vector<int64_t> root_;
|
| 333 |
+
std::unordered_map<std::tuple<int64_t, int64_t, int64_t>, Rule, TupleHash> temporal_rules_;
|
| 334 |
+
};
|
| 335 |
+
|
| 336 |
+
// Encode B dense [T, D] trajectories in parallel; returns one stream per row.
|
| 337 |
+
py::list encode_batch(const SetBPE& codec,
|
| 338 |
+
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>& cells,
|
| 339 |
+
int threads) {
|
| 340 |
+
if (cells.ndim() != 3) throw std::invalid_argument("cells must have shape [B, T, D]");
|
| 341 |
+
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 342 |
+
const int64_t* data = cells.data();
|
| 343 |
+
std::vector<std::vector<int64_t>> streams(batch);
|
| 344 |
+
{
|
| 345 |
+
py::gil_scoped_release release;
|
| 346 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 347 |
+
streams[index] = codec.encode(data + index * steps * width, steps);
|
| 348 |
+
});
|
| 349 |
+
}
|
| 350 |
+
py::list output(batch);
|
| 351 |
+
for (int64_t index = 0; index < batch; ++index) output[index] = py::cast(streams[index]);
|
| 352 |
+
return output;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Decode B equal-horizon streams to dense [B, T, D] cells (-1 outside ``full``).
|
| 356 |
+
py::array_t<int64_t> decode_batch(const SetBPE& codec, const std::vector<std::vector<int64_t>>& streams,
|
| 357 |
+
int64_t width, uint64_t full, int threads) {
|
| 358 |
+
const int64_t batch = static_cast<int64_t>(streams.size());
|
| 359 |
+
if (batch < 1) throw std::invalid_argument("streams must not be empty");
|
| 360 |
+
std::vector<std::vector<int64_t>> cells(batch);
|
| 361 |
+
{
|
| 362 |
+
py::gil_scoped_release release;
|
| 363 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 364 |
+
if (streams[index].empty()) throw std::invalid_argument("token stream must not be empty");
|
| 365 |
+
cells[index] = codec.decode(streams[index].data(), static_cast<int64_t>(streams[index].size()), full);
|
| 366 |
+
});
|
| 367 |
+
}
|
| 368 |
+
const int64_t steps = static_cast<int64_t>(cells[0].size()) / width;
|
| 369 |
+
for (const auto& row : cells) {
|
| 370 |
+
if (static_cast<int64_t>(row.size()) != steps * width)
|
| 371 |
+
throw std::invalid_argument("all token streams in a batch must decode to the same length");
|
| 372 |
+
}
|
| 373 |
+
py::array_t<int64_t> output({batch, steps, width});
|
| 374 |
+
int64_t* out = output.mutable_data();
|
| 375 |
+
for (int64_t index = 0; index < batch; ++index)
|
| 376 |
+
std::copy(cells[index].begin(), cells[index].end(), out + index * steps * width);
|
| 377 |
+
return output;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
} // namespace
|
| 381 |
+
|
| 382 |
+
void register_setbpe(py::module_& module) {
|
| 383 |
+
py::class_<SetBPE>(module, "SetBPE")
|
| 384 |
+
.def(py::init<std::vector<int64_t>, int64_t, const std::vector<std::pair<int64_t, int64_t>>&,
|
| 385 |
+
const std::vector<std::tuple<int64_t, int64_t, int64_t>>&>(),
|
| 386 |
+
py::arg("bins"), py::arg("lookahead"), py::arg("spatial_merges"),
|
| 387 |
+
py::arg("temporal_merges"))
|
| 388 |
+
.def_property_readonly("vocab_size", &SetBPE::vocab_size)
|
| 389 |
+
.def("encode_batch", &encode_batch, py::arg("cells"), py::arg("threads") = 0,
|
| 390 |
+
"Dense [B, T, D] cells (-1 = absent) -> one token stream per row.")
|
| 391 |
+
.def("decode_batch", &decode_batch, py::arg("streams"), py::arg("width"),
|
| 392 |
+
py::arg("full_support"), py::arg("threads") = 0,
|
| 393 |
+
"Equal-horizon streams -> dense [B, T, D] cells, -1 outside full_support.");
|
| 394 |
+
}
|
actioncodec2_kernels/so3.cpp
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Intrinsic first- and stateful second-order SO(3) error-feedback encoders.
|
| 2 |
+
|
| 3 |
+
#include <pybind11/numpy.h>
|
| 4 |
+
#include <pybind11/pybind11.h>
|
| 5 |
+
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <array>
|
| 8 |
+
#include <cmath>
|
| 9 |
+
#include <cstdint>
|
| 10 |
+
#include <limits>
|
| 11 |
+
#include <vector>
|
| 12 |
+
|
| 13 |
+
#include "parallel.hpp"
|
| 14 |
+
|
| 15 |
+
namespace py = pybind11;
|
| 16 |
+
|
| 17 |
+
namespace {
|
| 18 |
+
|
| 19 |
+
struct Quaternion {
|
| 20 |
+
double x;
|
| 21 |
+
double y;
|
| 22 |
+
double z;
|
| 23 |
+
double w;
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
Quaternion normalize(Quaternion q) {
|
| 27 |
+
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 28 |
+
q.x /= norm;
|
| 29 |
+
q.y /= norm;
|
| 30 |
+
q.z /= norm;
|
| 31 |
+
q.w /= norm;
|
| 32 |
+
return q;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
Quaternion multiply(const Quaternion& a, const Quaternion& b) {
|
| 36 |
+
return normalize({a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
| 37 |
+
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
| 38 |
+
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
| 39 |
+
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
Quaternion conjugate(const Quaternion& q) { return {-q.x, -q.y, -q.z, q.w}; }
|
| 43 |
+
|
| 44 |
+
Quaternion from_euler(const float* euler) {
|
| 45 |
+
const double roll = 0.5 * static_cast<double>(euler[0]);
|
| 46 |
+
const double pitch = 0.5 * static_cast<double>(euler[1]);
|
| 47 |
+
const double yaw = 0.5 * static_cast<double>(euler[2]);
|
| 48 |
+
const double sr = std::sin(roll), cr = std::cos(roll);
|
| 49 |
+
const double sp = std::sin(pitch), cp = std::cos(pitch);
|
| 50 |
+
const double sy = std::sin(yaw), cy = std::cos(yaw);
|
| 51 |
+
return normalize({sr * cp * cy - cr * sp * sy, cr * sp * cy + sr * cp * sy,
|
| 52 |
+
cr * cp * sy - sr * sp * cy, cr * cp * cy + sr * sp * sy});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
void to_rotvec(Quaternion q, double* output) {
|
| 56 |
+
if (q.w < 0.0) {
|
| 57 |
+
q.x = -q.x;
|
| 58 |
+
q.y = -q.y;
|
| 59 |
+
q.z = -q.z;
|
| 60 |
+
q.w = -q.w;
|
| 61 |
+
}
|
| 62 |
+
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z);
|
| 63 |
+
const double scale = norm < 1e-12 ? 2.0 : 2.0 * std::atan2(norm, std::max(0.0, q.w)) / norm;
|
| 64 |
+
output[0] = q.x * scale;
|
| 65 |
+
output[1] = q.y * scale;
|
| 66 |
+
output[2] = q.z * scale;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
Quaternion from_rotvec(const double* value) {
|
| 70 |
+
const double angle = std::sqrt(value[0] * value[0] + value[1] * value[1] +
|
| 71 |
+
value[2] * value[2]);
|
| 72 |
+
const double scale = angle < 1e-12 ? 0.5 : std::sin(0.5 * angle) / angle;
|
| 73 |
+
return normalize({value[0] * scale, value[1] * scale, value[2] * scale,
|
| 74 |
+
std::cos(0.5 * angle)});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
Quaternion from_input(const float* value, bool rotation_vector) {
|
| 78 |
+
if (!rotation_vector) return from_euler(value);
|
| 79 |
+
const double converted[3] = {static_cast<double>(value[0]), static_cast<double>(value[1]),
|
| 80 |
+
static_cast<double>(value[2])};
|
| 81 |
+
return from_rotvec(converted);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
Quaternion residual(const Quaternion& target, const Quaternion& reconstruction, bool left) {
|
| 85 |
+
return left ? multiply(target, conjugate(reconstruction))
|
| 86 |
+
: multiply(conjugate(reconstruction), target);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
Quaternion apply(const Quaternion& state, const Quaternion& increment, bool left) {
|
| 90 |
+
return left ? multiply(increment, state) : multiply(state, increment);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
std::array<double, 3> as_rotvec(const Quaternion& value) {
|
| 94 |
+
std::array<double, 3> output{};
|
| 95 |
+
to_rotvec(value, output.data());
|
| 96 |
+
return output;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
struct SO3Layout {
|
| 100 |
+
const double* table;
|
| 101 |
+
const int32_t* bins;
|
| 102 |
+
int64_t width;
|
| 103 |
+
double limit;
|
| 104 |
+
bool smooth;
|
| 105 |
+
bool rotation_vector;
|
| 106 |
+
bool left;
|
| 107 |
+
int32_t input_order;
|
| 108 |
+
double projection_radius = std::numeric_limits<double>::infinity();
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
void project_primitive(double* value, double radius) {
|
| 112 |
+
if (!std::isfinite(radius)) return;
|
| 113 |
+
const double norm = std::sqrt(value[0]*value[0] + value[1]*value[1] + value[2]*value[2]);
|
| 114 |
+
if (norm > radius) {
|
| 115 |
+
const double scale = radius / norm;
|
| 116 |
+
for (int j = 0; j < 3; ++j) value[j] *= scale;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
inline double primitive(const SO3Layout& layout, int axis, int32_t index) {
|
| 121 |
+
return layout.table[static_cast<int64_t>(axis) * layout.width + index];
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
std::array<int32_t, 3> nearest_choice(const SO3Layout& layout,
|
| 125 |
+
const std::array<double, 3>& request) {
|
| 126 |
+
std::array<int32_t, 3> choice{};
|
| 127 |
+
for (int axis = 0; axis < 3; ++axis) {
|
| 128 |
+
double best = std::numeric_limits<double>::infinity();
|
| 129 |
+
for (int32_t index = 0; index < layout.bins[axis]; ++index) {
|
| 130 |
+
const double distance = std::fabs(request[axis] - primitive(layout, axis, index));
|
| 131 |
+
if (distance < best) {
|
| 132 |
+
best = distance;
|
| 133 |
+
choice[axis] = index;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
return choice;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
std::vector<int32_t> smooth_axis(const SO3Layout& layout, int axis, double request,
|
| 141 |
+
int32_t nearest) {
|
| 142 |
+
std::vector<int32_t> output;
|
| 143 |
+
bool has_nearest = false;
|
| 144 |
+
const double radius = 2.0 * layout.limit + 1e-12;
|
| 145 |
+
for (int32_t index = 0; index < layout.bins[axis]; ++index) {
|
| 146 |
+
if (std::fabs(primitive(layout, axis, index) - request) <= radius) {
|
| 147 |
+
output.push_back(index);
|
| 148 |
+
has_nearest = has_nearest || index == nearest;
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
if (!has_nearest) output.push_back(nearest);
|
| 152 |
+
return output;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
bool earlier(double error, const std::array<int32_t, 3>& choice, double best_error,
|
| 156 |
+
const std::array<int32_t, 3>& best_choice, bool found) {
|
| 157 |
+
return !found || error < best_error || (error == best_error && choice < best_choice);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
std::array<int32_t, 3> smooth_choice(const SO3Layout& layout,
|
| 161 |
+
const std::array<double, 3>& request,
|
| 162 |
+
const Quaternion& reconstruction,
|
| 163 |
+
const Quaternion& target,
|
| 164 |
+
const std::array<double, 3>& direction,
|
| 165 |
+
const std::array<int32_t, 3>& nearest) {
|
| 166 |
+
const auto x = smooth_axis(layout, 0, request[0], nearest[0]);
|
| 167 |
+
const auto y = smooth_axis(layout, 1, request[1], nearest[1]);
|
| 168 |
+
const auto z = smooth_axis(layout, 2, request[2], nearest[2]);
|
| 169 |
+
std::array<int32_t, 3> best_any{}, best_preferred{};
|
| 170 |
+
double any_error = 0.0, preferred_error = 0.0;
|
| 171 |
+
bool found_any = false, found_preferred = false;
|
| 172 |
+
for (const int32_t ix : x) {
|
| 173 |
+
for (const int32_t iy : y) {
|
| 174 |
+
for (const int32_t iz : z) {
|
| 175 |
+
const std::array<int32_t, 3> choice{ix, iy, iz};
|
| 176 |
+
double vector[3] = {primitive(layout, 0, ix), primitive(layout, 1, iy),
|
| 177 |
+
primitive(layout, 2, iz)};
|
| 178 |
+
project_primitive(vector, layout.projection_radius);
|
| 179 |
+
const Quaternion rebuilt = apply(reconstruction, from_rotvec(vector), layout.left);
|
| 180 |
+
const auto remaining = as_rotvec(residual(target, rebuilt, layout.left));
|
| 181 |
+
const double error = std::sqrt(remaining[0] * remaining[0] + remaining[1] * remaining[1] +
|
| 182 |
+
remaining[2] * remaining[2]);
|
| 183 |
+
if (error > layout.limit + 1e-12) continue;
|
| 184 |
+
if (earlier(error, choice, any_error, best_any, found_any)) {
|
| 185 |
+
best_any = choice;
|
| 186 |
+
any_error = error;
|
| 187 |
+
found_any = true;
|
| 188 |
+
}
|
| 189 |
+
const double dot = vector[0] * direction[0] + vector[1] * direction[1] +
|
| 190 |
+
vector[2] * direction[2];
|
| 191 |
+
if (dot >= -1e-15 && earlier(error, choice, preferred_error, best_preferred,
|
| 192 |
+
found_preferred)) {
|
| 193 |
+
best_preferred = choice;
|
| 194 |
+
preferred_error = error;
|
| 195 |
+
found_preferred = true;
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
return found_preferred ? best_preferred : (found_any ? best_any : nearest);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
void encode_trajectory(const SO3Layout& layout, const float* actions, int64_t steps,
|
| 204 |
+
int32_t* cells) {
|
| 205 |
+
Quaternion truth{0.0, 0.0, 0.0, 1.0};
|
| 206 |
+
Quaternion reconstruction = truth;
|
| 207 |
+
std::array<double, 3> direction{};
|
| 208 |
+
for (int64_t step = 0; step < steps; ++step) {
|
| 209 |
+
const Quaternion value = from_input(actions + 3 * step, layout.rotation_vector);
|
| 210 |
+
truth = layout.input_order == 0 ? value : apply(truth, value, layout.left);
|
| 211 |
+
const auto request = as_rotvec(residual(truth, reconstruction, layout.left));
|
| 212 |
+
auto choice = nearest_choice(layout, request);
|
| 213 |
+
if (layout.smooth) {
|
| 214 |
+
choice = smooth_choice(layout, request, reconstruction, truth, direction, choice);
|
| 215 |
+
}
|
| 216 |
+
const double vector[3] = {primitive(layout, 0, choice[0]),
|
| 217 |
+
primitive(layout, 1, choice[1]),
|
| 218 |
+
primitive(layout, 2, choice[2])};
|
| 219 |
+
reconstruction = apply(reconstruction, from_rotvec(vector), layout.left);
|
| 220 |
+
if (vector[0] != 0.0 || vector[1] != 0.0 || vector[2] != 0.0) {
|
| 221 |
+
direction = {vector[0], vector[1], vector[2]};
|
| 222 |
+
}
|
| 223 |
+
for (int axis = 0; axis < 3; ++axis) cells[3 * step + axis] = choice[axis];
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
} // namespace
|
| 228 |
+
|
| 229 |
+
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 230 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
| 231 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& bins,
|
| 232 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& quaternions,
|
| 233 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& previous_first_order,
|
| 234 |
+
int32_t input_order, bool left, double limit, double radius, bool smooth, int threads) {
|
| 235 |
+
if (table.ndim() != 2 || table.shape(0) != 3 || bins.size() != 3 ||
|
| 236 |
+
quaternions.ndim() != 3 || quaternions.shape(2) != 4 ||
|
| 237 |
+
previous_first_order.ndim() != 2 || previous_first_order.shape(1) != 4 ||
|
| 238 |
+
previous_first_order.shape(0) != quaternions.shape(0) ||
|
| 239 |
+
(input_order != 0 && input_order != 1) || !(limit > 0) || !(radius > 0)) {
|
| 240 |
+
throw std::invalid_argument("invalid second-order SO(3) batch arguments");
|
| 241 |
+
}
|
| 242 |
+
for (int j = 0; j < 3; ++j) {
|
| 243 |
+
if (bins.data()[j] < 1 || bins.data()[j] > table.shape(1))
|
| 244 |
+
throw std::invalid_argument("SO(3) primitive count exceeds table width");
|
| 245 |
+
}
|
| 246 |
+
const SO3Layout layout{table.data(), bins.data(), table.shape(1), limit, smooth,
|
| 247 |
+
true, left, input_order, radius};
|
| 248 |
+
const int64_t batch = quaternions.shape(0), steps = quaternions.shape(1);
|
| 249 |
+
py::array_t<int32_t> output({batch, steps, int64_t{3}});
|
| 250 |
+
const double* values = quaternions.data();
|
| 251 |
+
const double* previous = previous_first_order.data();
|
| 252 |
+
int32_t* result = output.mutable_data();
|
| 253 |
+
const Quaternion identity{0, 0, 0, 1};
|
| 254 |
+
py::gil_scoped_release release;
|
| 255 |
+
ac2::parallel_for(batch, threads, [&](int64_t i) {
|
| 256 |
+
Quaternion truth = identity, reconstruction = identity;
|
| 257 |
+
Quaternion increment{previous[4*i], previous[4*i+1], previous[4*i+2], previous[4*i+3]};
|
| 258 |
+
std::array<double, 3> direction{};
|
| 259 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 260 |
+
const double* raw = values + (i*steps+t)*4;
|
| 261 |
+
const Quaternion value{raw[0], raw[1], raw[2], raw[3]};
|
| 262 |
+
truth = input_order == 0 ? value : apply(truth, value, left);
|
| 263 |
+
const auto desired = residual(truth, reconstruction, left);
|
| 264 |
+
const auto correction = residual(desired, increment, left);
|
| 265 |
+
const auto request = as_rotvec(correction);
|
| 266 |
+
auto choice = nearest_choice(layout, request);
|
| 267 |
+
if (smooth) {
|
| 268 |
+
// Python scores U^-1 W for both conventions; use exactly that order.
|
| 269 |
+
SO3Layout selection = layout;
|
| 270 |
+
selection.left = false;
|
| 271 |
+
choice = smooth_choice(selection, request, identity, correction, direction, choice);
|
| 272 |
+
}
|
| 273 |
+
double vector[3] = {primitive(layout, 0, choice[0]), primitive(layout, 1, choice[1]), primitive(layout, 2, choice[2])};
|
| 274 |
+
project_primitive(vector, radius);
|
| 275 |
+
increment = apply(increment, from_rotvec(vector), left);
|
| 276 |
+
reconstruction = apply(reconstruction, increment, left);
|
| 277 |
+
if (vector[0] != 0 || vector[1] != 0 || vector[2] != 0)
|
| 278 |
+
direction = {vector[0], vector[1], vector[2]};
|
| 279 |
+
for (int j = 0; j < 3; ++j) result[(i*steps+t)*3+j] = choice[j];
|
| 280 |
+
}
|
| 281 |
+
});
|
| 282 |
+
return output;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
py::array_t<int32_t> so3_encode_batch(
|
| 286 |
+
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
| 287 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& bins,
|
| 288 |
+
const py::array_t<float, py::array::c_style | py::array::forcecast>& actions,
|
| 289 |
+
int32_t input_order, bool rotation_vector, bool left, double limit, bool smooth,
|
| 290 |
+
int threads) {
|
| 291 |
+
if (table.ndim() != 2 || table.shape(0) != 3 || bins.size() != 3 || actions.ndim() != 3 ||
|
| 292 |
+
actions.shape(2) != 3 || (input_order != 0 && input_order != 1) || limit <= 0.0) {
|
| 293 |
+
throw std::invalid_argument("invalid SO(3) batch encoder arguments");
|
| 294 |
+
}
|
| 295 |
+
for (int axis = 0; axis < 3; ++axis) {
|
| 296 |
+
if (bins.data()[axis] < 1 || bins.data()[axis] > table.shape(1)) {
|
| 297 |
+
throw std::invalid_argument("SO(3) primitive count exceeds table width");
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
const SO3Layout layout{table.data(), bins.data(), table.shape(1), limit, smooth,
|
| 301 |
+
rotation_vector, left, input_order};
|
| 302 |
+
const int64_t batch = actions.shape(0);
|
| 303 |
+
const int64_t steps = actions.shape(1);
|
| 304 |
+
py::array_t<int32_t> output({batch, steps, int64_t{3}});
|
| 305 |
+
{
|
| 306 |
+
py::gil_scoped_release release;
|
| 307 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 308 |
+
encode_trajectory(layout, actions.data() + index * steps * 3, steps,
|
| 309 |
+
output.mutable_data() + index * steps * 3);
|
| 310 |
+
});
|
| 311 |
+
}
|
| 312 |
+
return output;
|
| 313 |
+
}
|
config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "actioncodec2",
|
| 3 |
+
"processor_class": "ActionCodec2"
|
| 4 |
+
}
|
processing_actioncodec2.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small Hugging Face entry point for a grouped ActionCodec2 artifact.
|
| 2 |
+
|
| 3 |
+
Transformers copies only top-level Python files from a Hub repository into its
|
| 4 |
+
dynamic-module cache. This entry point locates the artifact, then loads the
|
| 5 |
+
versioned runtime stored in ``runtime/`` without installing another package.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import importlib
|
| 12 |
+
import importlib.util
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ActionCodec2:
|
| 18 |
+
"""Load the processor implementation bundled with a pretrained artifact."""
|
| 19 |
+
|
| 20 |
+
@classmethod
|
| 21 |
+
def register_for_auto_class(cls, auto_class="AutoProcessor"):
|
| 22 |
+
"""Satisfy Transformers' dynamic class hook for this thin loader."""
|
| 23 |
+
return cls
|
| 24 |
+
|
| 25 |
+
@classmethod
|
| 26 |
+
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
|
| 27 |
+
source = Path(pretrained_model_name_or_path)
|
| 28 |
+
subfolder = str(kwargs.pop("subfolder", ""))
|
| 29 |
+
action_space = kwargs.pop("action_space", None)
|
| 30 |
+
kwargs.pop("_from_auto", None)
|
| 31 |
+
kwargs.pop("trust_remote_code", None)
|
| 32 |
+
allowed = {
|
| 33 |
+
"cache_dir",
|
| 34 |
+
"force_download",
|
| 35 |
+
"local_files_only",
|
| 36 |
+
"token",
|
| 37 |
+
"revision",
|
| 38 |
+
"repo_type",
|
| 39 |
+
}
|
| 40 |
+
unknown = sorted(set(kwargs) - allowed)
|
| 41 |
+
if unknown:
|
| 42 |
+
raise TypeError(
|
| 43 |
+
f"unsupported from_pretrained keyword(s): {', '.join(unknown)}"
|
| 44 |
+
)
|
| 45 |
+
if source.is_dir():
|
| 46 |
+
root = source / subfolder
|
| 47 |
+
else:
|
| 48 |
+
from huggingface_hub import snapshot_download
|
| 49 |
+
|
| 50 |
+
prefix = f"{subfolder.rstrip('/')}/" if subfolder else ""
|
| 51 |
+
patterns = [
|
| 52 |
+
"config.json",
|
| 53 |
+
"processor_config.json",
|
| 54 |
+
"router_config.yaml",
|
| 55 |
+
"profiles/**",
|
| 56 |
+
"runtime/**",
|
| 57 |
+
"actioncodec2_kernels/**",
|
| 58 |
+
"setup.py", "pyproject.toml", "LICENSE",
|
| 59 |
+
"README.md",
|
| 60 |
+
"requirements.txt",
|
| 61 |
+
"fit_report.json",
|
| 62 |
+
]
|
| 63 |
+
snapshot = snapshot_download(
|
| 64 |
+
repo_id=str(pretrained_model_name_or_path),
|
| 65 |
+
allow_patterns=[prefix + pattern for pattern in patterns],
|
| 66 |
+
**kwargs,
|
| 67 |
+
)
|
| 68 |
+
root = Path(snapshot) / subfolder
|
| 69 |
+
|
| 70 |
+
runtime = root / "runtime"
|
| 71 |
+
package_file = runtime / "__init__.py"
|
| 72 |
+
if not package_file.is_file():
|
| 73 |
+
raise FileNotFoundError(
|
| 74 |
+
f"ActionCodec2 runtime is missing from {root}; copy the whole artifact"
|
| 75 |
+
)
|
| 76 |
+
identity = hashlib.sha256(str(runtime.resolve()).encode()).hexdigest()[:16]
|
| 77 |
+
package_name = f"_actioncodec2_artifact_{identity}"
|
| 78 |
+
if package_name not in sys.modules:
|
| 79 |
+
spec = importlib.util.spec_from_file_location(
|
| 80 |
+
package_name, package_file, submodule_search_locations=[str(runtime)]
|
| 81 |
+
)
|
| 82 |
+
if spec is None or spec.loader is None:
|
| 83 |
+
raise ImportError(f"cannot load ActionCodec2 runtime from {runtime}")
|
| 84 |
+
package = importlib.util.module_from_spec(spec)
|
| 85 |
+
sys.modules[package_name] = package
|
| 86 |
+
try:
|
| 87 |
+
spec.loader.exec_module(package)
|
| 88 |
+
except BaseException:
|
| 89 |
+
del sys.modules[package_name]
|
| 90 |
+
raise
|
| 91 |
+
processor = importlib.import_module(f"{package_name}.processing_actioncodec2")
|
| 92 |
+
acceleration = importlib.import_module(f"{package_name}.hub_acceleration")
|
| 93 |
+
acceleration.install(package_name)
|
| 94 |
+
return processor.ActionCodec2.from_pretrained(root, action_space=action_space)
|
processor_config.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"processor_class": "ActionCodec2",
|
| 3 |
+
"action_space": "single_eef_delta",
|
| 4 |
+
"fitted_profiles": [
|
| 5 |
+
"eef"
|
| 6 |
+
],
|
| 7 |
+
"auto_map": {
|
| 8 |
+
"AutoProcessor": "processing_actioncodec2.ActionCodec2"
|
| 9 |
+
}
|
| 10 |
+
}
|
profiles/eef/bpe.json
ADDED
|
@@ -0,0 +1,5950 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"spatial_merges": [
|
| 3 |
+
[
|
| 4 |
+
40,
|
| 5 |
+
121
|
| 6 |
+
],
|
| 7 |
+
[
|
| 8 |
+
202,
|
| 9 |
+
720
|
| 10 |
+
],
|
| 11 |
+
[
|
| 12 |
+
338,
|
| 13 |
+
816
|
| 14 |
+
],
|
| 15 |
+
[
|
| 16 |
+
529,
|
| 17 |
+
818
|
| 18 |
+
],
|
| 19 |
+
[
|
| 20 |
+
819,
|
| 21 |
+
821
|
| 22 |
+
],
|
| 23 |
+
[
|
| 24 |
+
338,
|
| 25 |
+
817
|
| 26 |
+
],
|
| 27 |
+
[
|
| 28 |
+
820,
|
| 29 |
+
822
|
| 30 |
+
],
|
| 31 |
+
[
|
| 32 |
+
337,
|
| 33 |
+
816
|
| 34 |
+
],
|
| 35 |
+
[
|
| 36 |
+
339,
|
| 37 |
+
816
|
| 38 |
+
],
|
| 39 |
+
[
|
| 40 |
+
201,
|
| 41 |
+
720
|
| 42 |
+
],
|
| 43 |
+
[
|
| 44 |
+
822,
|
| 45 |
+
823
|
| 46 |
+
],
|
| 47 |
+
[
|
| 48 |
+
41,
|
| 49 |
+
121
|
| 50 |
+
],
|
| 51 |
+
[
|
| 52 |
+
529,
|
| 53 |
+
819
|
| 54 |
+
],
|
| 55 |
+
[
|
| 56 |
+
39,
|
| 57 |
+
121
|
| 58 |
+
],
|
| 59 |
+
[
|
| 60 |
+
40,
|
| 61 |
+
120
|
| 62 |
+
],
|
| 63 |
+
[
|
| 64 |
+
202,
|
| 65 |
+
721
|
| 66 |
+
],
|
| 67 |
+
[
|
| 68 |
+
337,
|
| 69 |
+
817
|
| 70 |
+
],
|
| 71 |
+
[
|
| 72 |
+
40,
|
| 73 |
+
122
|
| 74 |
+
],
|
| 75 |
+
[
|
| 76 |
+
202,
|
| 77 |
+
719
|
| 78 |
+
],
|
| 79 |
+
[
|
| 80 |
+
339,
|
| 81 |
+
817
|
| 82 |
+
],
|
| 83 |
+
[
|
| 84 |
+
203,
|
| 85 |
+
720
|
| 86 |
+
],
|
| 87 |
+
[
|
| 88 |
+
528,
|
| 89 |
+
818
|
| 90 |
+
],
|
| 91 |
+
[
|
| 92 |
+
336,
|
| 93 |
+
816
|
| 94 |
+
],
|
| 95 |
+
[
|
| 96 |
+
340,
|
| 97 |
+
816
|
| 98 |
+
],
|
| 99 |
+
[
|
| 100 |
+
530,
|
| 101 |
+
818
|
| 102 |
+
],
|
| 103 |
+
[
|
| 104 |
+
201,
|
| 105 |
+
721
|
| 106 |
+
],
|
| 107 |
+
[
|
| 108 |
+
820,
|
| 109 |
+
821
|
| 110 |
+
],
|
| 111 |
+
[
|
| 112 |
+
201,
|
| 113 |
+
719
|
| 114 |
+
],
|
| 115 |
+
[
|
| 116 |
+
41,
|
| 117 |
+
120
|
| 118 |
+
],
|
| 119 |
+
[
|
| 120 |
+
41,
|
| 121 |
+
122
|
| 122 |
+
],
|
| 123 |
+
[
|
| 124 |
+
335,
|
| 125 |
+
816
|
| 126 |
+
],
|
| 127 |
+
[
|
| 128 |
+
341,
|
| 129 |
+
816
|
| 130 |
+
],
|
| 131 |
+
[
|
| 132 |
+
819,
|
| 133 |
+
820
|
| 134 |
+
],
|
| 135 |
+
[
|
| 136 |
+
203,
|
| 137 |
+
719
|
| 138 |
+
],
|
| 139 |
+
[
|
| 140 |
+
39,
|
| 141 |
+
120
|
| 142 |
+
],
|
| 143 |
+
[
|
| 144 |
+
39,
|
| 145 |
+
122
|
| 146 |
+
],
|
| 147 |
+
[
|
| 148 |
+
529,
|
| 149 |
+
820
|
| 150 |
+
],
|
| 151 |
+
[
|
| 152 |
+
203,
|
| 153 |
+
721
|
| 154 |
+
],
|
| 155 |
+
[
|
| 156 |
+
336,
|
| 157 |
+
817
|
| 158 |
+
],
|
| 159 |
+
[
|
| 160 |
+
340,
|
| 161 |
+
817
|
| 162 |
+
],
|
| 163 |
+
[
|
| 164 |
+
200,
|
| 165 |
+
720
|
| 166 |
+
],
|
| 167 |
+
[
|
| 168 |
+
821,
|
| 169 |
+
823
|
| 170 |
+
],
|
| 171 |
+
[
|
| 172 |
+
820,
|
| 173 |
+
830
|
| 174 |
+
],
|
| 175 |
+
[
|
| 176 |
+
42,
|
| 177 |
+
121
|
| 178 |
+
],
|
| 179 |
+
[
|
| 180 |
+
334,
|
| 181 |
+
816
|
| 182 |
+
],
|
| 183 |
+
[
|
| 184 |
+
342,
|
| 185 |
+
816
|
| 186 |
+
],
|
| 187 |
+
[
|
| 188 |
+
528,
|
| 189 |
+
827
|
| 190 |
+
],
|
| 191 |
+
[
|
| 192 |
+
819,
|
| 193 |
+
823
|
| 194 |
+
],
|
| 195 |
+
[
|
| 196 |
+
823,
|
| 197 |
+
830
|
| 198 |
+
],
|
| 199 |
+
[
|
| 200 |
+
204,
|
| 201 |
+
720
|
| 202 |
+
],
|
| 203 |
+
[
|
| 204 |
+
529,
|
| 205 |
+
823
|
| 206 |
+
],
|
| 207 |
+
[
|
| 208 |
+
530,
|
| 209 |
+
820
|
| 210 |
+
],
|
| 211 |
+
[
|
| 212 |
+
38,
|
| 213 |
+
121
|
| 214 |
+
],
|
| 215 |
+
[
|
| 216 |
+
202,
|
| 217 |
+
722
|
| 218 |
+
],
|
| 219 |
+
[
|
| 220 |
+
202,
|
| 221 |
+
718
|
| 222 |
+
],
|
| 223 |
+
[
|
| 224 |
+
200,
|
| 225 |
+
721
|
| 226 |
+
],
|
| 227 |
+
[
|
| 228 |
+
200,
|
| 229 |
+
719
|
| 230 |
+
],
|
| 231 |
+
[
|
| 232 |
+
333,
|
| 233 |
+
816
|
| 234 |
+
],
|
| 235 |
+
[
|
| 236 |
+
40,
|
| 237 |
+
119
|
| 238 |
+
],
|
| 239 |
+
[
|
| 240 |
+
343,
|
| 241 |
+
816
|
| 242 |
+
],
|
| 243 |
+
[
|
| 244 |
+
40,
|
| 245 |
+
123
|
| 246 |
+
],
|
| 247 |
+
[
|
| 248 |
+
528,
|
| 249 |
+
820
|
| 250 |
+
],
|
| 251 |
+
[
|
| 252 |
+
335,
|
| 253 |
+
817
|
| 254 |
+
],
|
| 255 |
+
[
|
| 256 |
+
341,
|
| 257 |
+
817
|
| 258 |
+
],
|
| 259 |
+
[
|
| 260 |
+
529,
|
| 261 |
+
825
|
| 262 |
+
],
|
| 263 |
+
[
|
| 264 |
+
529,
|
| 265 |
+
826
|
| 266 |
+
],
|
| 267 |
+
[
|
| 268 |
+
820,
|
| 269 |
+
839
|
| 270 |
+
],
|
| 271 |
+
[
|
| 272 |
+
42,
|
| 273 |
+
120
|
| 274 |
+
],
|
| 275 |
+
[
|
| 276 |
+
531,
|
| 277 |
+
820
|
| 278 |
+
],
|
| 279 |
+
[
|
| 280 |
+
42,
|
| 281 |
+
122
|
| 282 |
+
],
|
| 283 |
+
[
|
| 284 |
+
201,
|
| 285 |
+
722
|
| 286 |
+
],
|
| 287 |
+
[
|
| 288 |
+
527,
|
| 289 |
+
820
|
| 290 |
+
],
|
| 291 |
+
[
|
| 292 |
+
204,
|
| 293 |
+
719
|
| 294 |
+
],
|
| 295 |
+
[
|
| 296 |
+
201,
|
| 297 |
+
718
|
| 298 |
+
],
|
| 299 |
+
[
|
| 300 |
+
530,
|
| 301 |
+
825
|
| 302 |
+
],
|
| 303 |
+
[
|
| 304 |
+
530,
|
| 305 |
+
826
|
| 306 |
+
],
|
| 307 |
+
[
|
| 308 |
+
41,
|
| 309 |
+
119
|
| 310 |
+
],
|
| 311 |
+
[
|
| 312 |
+
204,
|
| 313 |
+
721
|
| 314 |
+
],
|
| 315 |
+
[
|
| 316 |
+
528,
|
| 317 |
+
819
|
| 318 |
+
],
|
| 319 |
+
[
|
| 320 |
+
41,
|
| 321 |
+
123
|
| 322 |
+
],
|
| 323 |
+
[
|
| 324 |
+
38,
|
| 325 |
+
122
|
| 326 |
+
],
|
| 327 |
+
[
|
| 328 |
+
38,
|
| 329 |
+
120
|
| 330 |
+
],
|
| 331 |
+
[
|
| 332 |
+
332,
|
| 333 |
+
816
|
| 334 |
+
],
|
| 335 |
+
[
|
| 336 |
+
820,
|
| 337 |
+
842
|
| 338 |
+
],
|
| 339 |
+
[
|
| 340 |
+
203,
|
| 341 |
+
718
|
| 342 |
+
],
|
| 343 |
+
[
|
| 344 |
+
344,
|
| 345 |
+
816
|
| 346 |
+
],
|
| 347 |
+
[
|
| 348 |
+
39,
|
| 349 |
+
123
|
| 350 |
+
],
|
| 351 |
+
[
|
| 352 |
+
39,
|
| 353 |
+
119
|
| 354 |
+
],
|
| 355 |
+
[
|
| 356 |
+
203,
|
| 357 |
+
722
|
| 358 |
+
],
|
| 359 |
+
[
|
| 360 |
+
832,
|
| 361 |
+
834
|
| 362 |
+
],
|
| 363 |
+
[
|
| 364 |
+
835,
|
| 365 |
+
837
|
| 366 |
+
],
|
| 367 |
+
[
|
| 368 |
+
839,
|
| 369 |
+
850
|
| 370 |
+
],
|
| 371 |
+
[
|
| 372 |
+
43,
|
| 373 |
+
121
|
| 374 |
+
],
|
| 375 |
+
[
|
| 376 |
+
205,
|
| 377 |
+
720
|
| 378 |
+
],
|
| 379 |
+
[
|
| 380 |
+
530,
|
| 381 |
+
823
|
| 382 |
+
],
|
| 383 |
+
[
|
| 384 |
+
199,
|
| 385 |
+
720
|
| 386 |
+
],
|
| 387 |
+
[
|
| 388 |
+
528,
|
| 389 |
+
829
|
| 390 |
+
],
|
| 391 |
+
[
|
| 392 |
+
827,
|
| 393 |
+
844
|
| 394 |
+
],
|
| 395 |
+
[
|
| 396 |
+
334,
|
| 397 |
+
817
|
| 398 |
+
],
|
| 399 |
+
[
|
| 400 |
+
823,
|
| 401 |
+
839
|
| 402 |
+
],
|
| 403 |
+
[
|
| 404 |
+
342,
|
| 405 |
+
817
|
| 406 |
+
],
|
| 407 |
+
[
|
| 408 |
+
37,
|
| 409 |
+
121
|
| 410 |
+
],
|
| 411 |
+
[
|
| 412 |
+
528,
|
| 413 |
+
825
|
| 414 |
+
],
|
| 415 |
+
[
|
| 416 |
+
531,
|
| 417 |
+
825
|
| 418 |
+
],
|
| 419 |
+
[
|
| 420 |
+
531,
|
| 421 |
+
826
|
| 422 |
+
],
|
| 423 |
+
[
|
| 424 |
+
529,
|
| 425 |
+
833
|
| 426 |
+
],
|
| 427 |
+
[
|
| 428 |
+
842,
|
| 429 |
+
850
|
| 430 |
+
],
|
| 431 |
+
[
|
| 432 |
+
200,
|
| 433 |
+
722
|
| 434 |
+
],
|
| 435 |
+
[
|
| 436 |
+
527,
|
| 437 |
+
818
|
| 438 |
+
],
|
| 439 |
+
[
|
| 440 |
+
529,
|
| 441 |
+
836
|
| 442 |
+
],
|
| 443 |
+
[
|
| 444 |
+
202,
|
| 445 |
+
717
|
| 446 |
+
],
|
| 447 |
+
[
|
| 448 |
+
528,
|
| 449 |
+
826
|
| 450 |
+
],
|
| 451 |
+
[
|
| 452 |
+
345,
|
| 453 |
+
816
|
| 454 |
+
],
|
| 455 |
+
[
|
| 456 |
+
202,
|
| 457 |
+
723
|
| 458 |
+
],
|
| 459 |
+
[
|
| 460 |
+
331,
|
| 461 |
+
816
|
| 462 |
+
],
|
| 463 |
+
[
|
| 464 |
+
530,
|
| 465 |
+
819
|
| 466 |
+
],
|
| 467 |
+
[
|
| 468 |
+
200,
|
| 469 |
+
718
|
| 470 |
+
],
|
| 471 |
+
[
|
| 472 |
+
822,
|
| 473 |
+
825
|
| 474 |
+
],
|
| 475 |
+
[
|
| 476 |
+
528,
|
| 477 |
+
823
|
| 478 |
+
],
|
| 479 |
+
[
|
| 480 |
+
822,
|
| 481 |
+
826
|
| 482 |
+
],
|
| 483 |
+
[
|
| 484 |
+
833,
|
| 485 |
+
844
|
| 486 |
+
],
|
| 487 |
+
[
|
| 488 |
+
532,
|
| 489 |
+
820
|
| 490 |
+
],
|
| 491 |
+
[
|
| 492 |
+
836,
|
| 493 |
+
844
|
| 494 |
+
],
|
| 495 |
+
[
|
| 496 |
+
199,
|
| 497 |
+
721
|
| 498 |
+
],
|
| 499 |
+
[
|
| 500 |
+
42,
|
| 501 |
+
119
|
| 502 |
+
],
|
| 503 |
+
[
|
| 504 |
+
40,
|
| 505 |
+
118
|
| 506 |
+
],
|
| 507 |
+
[
|
| 508 |
+
529,
|
| 509 |
+
827
|
| 510 |
+
],
|
| 511 |
+
[
|
| 512 |
+
40,
|
| 513 |
+
124
|
| 514 |
+
],
|
| 515 |
+
[
|
| 516 |
+
205,
|
| 517 |
+
719
|
| 518 |
+
],
|
| 519 |
+
[
|
| 520 |
+
822,
|
| 521 |
+
834
|
| 522 |
+
],
|
| 523 |
+
[
|
| 524 |
+
527,
|
| 525 |
+
825
|
| 526 |
+
],
|
| 527 |
+
[
|
| 528 |
+
42,
|
| 529 |
+
123
|
| 530 |
+
],
|
| 531 |
+
[
|
| 532 |
+
839,
|
| 533 |
+
865
|
| 534 |
+
],
|
| 535 |
+
[
|
| 536 |
+
527,
|
| 537 |
+
826
|
| 538 |
+
],
|
| 539 |
+
[
|
| 540 |
+
204,
|
| 541 |
+
718
|
| 542 |
+
],
|
| 543 |
+
[
|
| 544 |
+
199,
|
| 545 |
+
719
|
| 546 |
+
],
|
| 547 |
+
[
|
| 548 |
+
822,
|
| 549 |
+
837
|
| 550 |
+
],
|
| 551 |
+
[
|
| 552 |
+
530,
|
| 553 |
+
838
|
| 554 |
+
],
|
| 555 |
+
[
|
| 556 |
+
526,
|
| 557 |
+
820
|
| 558 |
+
],
|
| 559 |
+
[
|
| 560 |
+
205,
|
| 561 |
+
721
|
| 562 |
+
],
|
| 563 |
+
[
|
| 564 |
+
38,
|
| 565 |
+
123
|
| 566 |
+
],
|
| 567 |
+
[
|
| 568 |
+
201,
|
| 569 |
+
717
|
| 570 |
+
],
|
| 571 |
+
[
|
| 572 |
+
201,
|
| 573 |
+
723
|
| 574 |
+
],
|
| 575 |
+
[
|
| 576 |
+
825,
|
| 577 |
+
830
|
| 578 |
+
],
|
| 579 |
+
[
|
| 580 |
+
821,
|
| 581 |
+
825
|
| 582 |
+
],
|
| 583 |
+
[
|
| 584 |
+
821,
|
| 585 |
+
826
|
| 586 |
+
],
|
| 587 |
+
[
|
| 588 |
+
826,
|
| 589 |
+
830
|
| 590 |
+
],
|
| 591 |
+
[
|
| 592 |
+
43,
|
| 593 |
+
120
|
| 594 |
+
],
|
| 595 |
+
[
|
| 596 |
+
204,
|
| 597 |
+
722
|
| 598 |
+
],
|
| 599 |
+
[
|
| 600 |
+
38,
|
| 601 |
+
119
|
| 602 |
+
],
|
| 603 |
+
[
|
| 604 |
+
838,
|
| 605 |
+
844
|
| 606 |
+
],
|
| 607 |
+
[
|
| 608 |
+
43,
|
| 609 |
+
122
|
| 610 |
+
],
|
| 611 |
+
[
|
| 612 |
+
827,
|
| 613 |
+
829
|
| 614 |
+
],
|
| 615 |
+
[
|
| 616 |
+
346,
|
| 617 |
+
816
|
| 618 |
+
],
|
| 619 |
+
[
|
| 620 |
+
41,
|
| 621 |
+
118
|
| 622 |
+
],
|
| 623 |
+
[
|
| 624 |
+
330,
|
| 625 |
+
816
|
| 626 |
+
],
|
| 627 |
+
[
|
| 628 |
+
41,
|
| 629 |
+
124
|
| 630 |
+
],
|
| 631 |
+
[
|
| 632 |
+
823,
|
| 633 |
+
842
|
| 634 |
+
],
|
| 635 |
+
[
|
| 636 |
+
831,
|
| 637 |
+
838
|
| 638 |
+
],
|
| 639 |
+
[
|
| 640 |
+
819,
|
| 641 |
+
839
|
| 642 |
+
],
|
| 643 |
+
[
|
| 644 |
+
37,
|
| 645 |
+
122
|
| 646 |
+
],
|
| 647 |
+
[
|
| 648 |
+
37,
|
| 649 |
+
120
|
| 650 |
+
],
|
| 651 |
+
[
|
| 652 |
+
333,
|
| 653 |
+
817
|
| 654 |
+
],
|
| 655 |
+
[
|
| 656 |
+
527,
|
| 657 |
+
823
|
| 658 |
+
],
|
| 659 |
+
[
|
| 660 |
+
343,
|
| 661 |
+
817
|
| 662 |
+
],
|
| 663 |
+
[
|
| 664 |
+
39,
|
| 665 |
+
118
|
| 666 |
+
],
|
| 667 |
+
[
|
| 668 |
+
827,
|
| 669 |
+
859
|
| 670 |
+
],
|
| 671 |
+
[
|
| 672 |
+
531,
|
| 673 |
+
823
|
| 674 |
+
],
|
| 675 |
+
[
|
| 676 |
+
831,
|
| 677 |
+
860
|
| 678 |
+
],
|
| 679 |
+
[
|
| 680 |
+
39,
|
| 681 |
+
124
|
| 682 |
+
],
|
| 683 |
+
[
|
| 684 |
+
530,
|
| 685 |
+
840
|
| 686 |
+
],
|
| 687 |
+
[
|
| 688 |
+
203,
|
| 689 |
+
717
|
| 690 |
+
],
|
| 691 |
+
[
|
| 692 |
+
203,
|
| 693 |
+
723
|
| 694 |
+
],
|
| 695 |
+
[
|
| 696 |
+
829,
|
| 697 |
+
834
|
| 698 |
+
],
|
| 699 |
+
[
|
| 700 |
+
529,
|
| 701 |
+
840
|
| 702 |
+
],
|
| 703 |
+
[
|
| 704 |
+
530,
|
| 705 |
+
841
|
| 706 |
+
],
|
| 707 |
+
[
|
| 708 |
+
833,
|
| 709 |
+
859
|
| 710 |
+
],
|
| 711 |
+
[
|
| 712 |
+
842,
|
| 713 |
+
865
|
| 714 |
+
],
|
| 715 |
+
[
|
| 716 |
+
827,
|
| 717 |
+
884
|
| 718 |
+
],
|
| 719 |
+
[
|
| 720 |
+
836,
|
| 721 |
+
859
|
| 722 |
+
],
|
| 723 |
+
[
|
| 724 |
+
528,
|
| 725 |
+
840
|
| 726 |
+
],
|
| 727 |
+
[
|
| 728 |
+
44,
|
| 729 |
+
121
|
| 730 |
+
],
|
| 731 |
+
[
|
| 732 |
+
206,
|
| 733 |
+
720
|
| 734 |
+
],
|
| 735 |
+
[
|
| 736 |
+
529,
|
| 737 |
+
841
|
| 738 |
+
],
|
| 739 |
+
[
|
| 740 |
+
528,
|
| 741 |
+
841
|
| 742 |
+
],
|
| 743 |
+
[
|
| 744 |
+
819,
|
| 745 |
+
842
|
| 746 |
+
],
|
| 747 |
+
[
|
| 748 |
+
821,
|
| 749 |
+
834
|
| 750 |
+
],
|
| 751 |
+
[
|
| 752 |
+
532,
|
| 753 |
+
826
|
| 754 |
+
],
|
| 755 |
+
[
|
| 756 |
+
532,
|
| 757 |
+
825
|
| 758 |
+
],
|
| 759 |
+
[
|
| 760 |
+
531,
|
| 761 |
+
840
|
| 762 |
+
],
|
| 763 |
+
[
|
| 764 |
+
821,
|
| 765 |
+
837
|
| 766 |
+
],
|
| 767 |
+
[
|
| 768 |
+
531,
|
| 769 |
+
841
|
| 770 |
+
],
|
| 771 |
+
[
|
| 772 |
+
529,
|
| 773 |
+
837
|
| 774 |
+
],
|
| 775 |
+
[
|
| 776 |
+
820,
|
| 777 |
+
864
|
| 778 |
+
],
|
| 779 |
+
[
|
| 780 |
+
829,
|
| 781 |
+
860
|
| 782 |
+
],
|
| 783 |
+
[
|
| 784 |
+
36,
|
| 785 |
+
121
|
| 786 |
+
],
|
| 787 |
+
[
|
| 788 |
+
199,
|
| 789 |
+
722
|
| 790 |
+
],
|
| 791 |
+
[
|
| 792 |
+
832,
|
| 793 |
+
860
|
| 794 |
+
],
|
| 795 |
+
[
|
| 796 |
+
835,
|
| 797 |
+
860
|
| 798 |
+
],
|
| 799 |
+
[
|
| 800 |
+
834,
|
| 801 |
+
846
|
| 802 |
+
],
|
| 803 |
+
[
|
| 804 |
+
202,
|
| 805 |
+
716
|
| 806 |
+
],
|
| 807 |
+
[
|
| 808 |
+
829,
|
| 809 |
+
866
|
| 810 |
+
],
|
| 811 |
+
[
|
| 812 |
+
832,
|
| 813 |
+
866
|
| 814 |
+
],
|
| 815 |
+
[
|
| 816 |
+
347,
|
| 817 |
+
816
|
| 818 |
+
],
|
| 819 |
+
[
|
| 820 |
+
835,
|
| 821 |
+
866
|
| 822 |
+
],
|
| 823 |
+
[
|
| 824 |
+
530,
|
| 825 |
+
837
|
| 826 |
+
]
|
| 827 |
+
],
|
| 828 |
+
"temporal_merges": [
|
| 829 |
+
[
|
| 830 |
+
824,
|
| 831 |
+
824,
|
| 832 |
+
1
|
| 833 |
+
],
|
| 834 |
+
[
|
| 835 |
+
1024,
|
| 836 |
+
1024,
|
| 837 |
+
2
|
| 838 |
+
],
|
| 839 |
+
[
|
| 840 |
+
828,
|
| 841 |
+
828,
|
| 842 |
+
1
|
| 843 |
+
],
|
| 844 |
+
[
|
| 845 |
+
1025,
|
| 846 |
+
1025,
|
| 847 |
+
4
|
| 848 |
+
],
|
| 849 |
+
[
|
| 850 |
+
816,
|
| 851 |
+
816,
|
| 852 |
+
1
|
| 853 |
+
],
|
| 854 |
+
[
|
| 855 |
+
817,
|
| 856 |
+
817,
|
| 857 |
+
1
|
| 858 |
+
],
|
| 859 |
+
[
|
| 860 |
+
831,
|
| 861 |
+
831,
|
| 862 |
+
2
|
| 863 |
+
],
|
| 864 |
+
[
|
| 865 |
+
527,
|
| 866 |
+
527,
|
| 867 |
+
1
|
| 868 |
+
],
|
| 869 |
+
[
|
| 870 |
+
829,
|
| 871 |
+
829,
|
| 872 |
+
2
|
| 873 |
+
],
|
| 874 |
+
[
|
| 875 |
+
1026,
|
| 876 |
+
1026,
|
| 877 |
+
2
|
| 878 |
+
],
|
| 879 |
+
[
|
| 880 |
+
526,
|
| 881 |
+
526,
|
| 882 |
+
1
|
| 883 |
+
],
|
| 884 |
+
[
|
| 885 |
+
832,
|
| 886 |
+
832,
|
| 887 |
+
2
|
| 888 |
+
],
|
| 889 |
+
[
|
| 890 |
+
528,
|
| 891 |
+
528,
|
| 892 |
+
1
|
| 893 |
+
],
|
| 894 |
+
[
|
| 895 |
+
835,
|
| 896 |
+
835,
|
| 897 |
+
2
|
| 898 |
+
],
|
| 899 |
+
[
|
| 900 |
+
530,
|
| 901 |
+
530,
|
| 902 |
+
1
|
| 903 |
+
],
|
| 904 |
+
[
|
| 905 |
+
531,
|
| 906 |
+
531,
|
| 907 |
+
1
|
| 908 |
+
],
|
| 909 |
+
[
|
| 910 |
+
848,
|
| 911 |
+
848,
|
| 912 |
+
1
|
| 913 |
+
],
|
| 914 |
+
[
|
| 915 |
+
849,
|
| 916 |
+
849,
|
| 917 |
+
1
|
| 918 |
+
],
|
| 919 |
+
[
|
| 920 |
+
529,
|
| 921 |
+
529,
|
| 922 |
+
1
|
| 923 |
+
],
|
| 924 |
+
[
|
| 925 |
+
834,
|
| 926 |
+
834,
|
| 927 |
+
1
|
| 928 |
+
],
|
| 929 |
+
[
|
| 930 |
+
532,
|
| 931 |
+
532,
|
| 932 |
+
1
|
| 933 |
+
],
|
| 934 |
+
[
|
| 935 |
+
847,
|
| 936 |
+
847,
|
| 937 |
+
2
|
| 938 |
+
],
|
| 939 |
+
[
|
| 940 |
+
533,
|
| 941 |
+
533,
|
| 942 |
+
1
|
| 943 |
+
],
|
| 944 |
+
[
|
| 945 |
+
837,
|
| 946 |
+
837,
|
| 947 |
+
1
|
| 948 |
+
],
|
| 949 |
+
[
|
| 950 |
+
856,
|
| 951 |
+
856,
|
| 952 |
+
1
|
| 953 |
+
],
|
| 954 |
+
[
|
| 955 |
+
838,
|
| 956 |
+
838,
|
| 957 |
+
2
|
| 958 |
+
],
|
| 959 |
+
[
|
| 960 |
+
525,
|
| 961 |
+
525,
|
| 962 |
+
1
|
| 963 |
+
],
|
| 964 |
+
[
|
| 965 |
+
198,
|
| 966 |
+
198,
|
| 967 |
+
1
|
| 968 |
+
],
|
| 969 |
+
[
|
| 970 |
+
857,
|
| 971 |
+
857,
|
| 972 |
+
1
|
| 973 |
+
],
|
| 974 |
+
[
|
| 975 |
+
840,
|
| 976 |
+
840,
|
| 977 |
+
1
|
| 978 |
+
],
|
| 979 |
+
[
|
| 980 |
+
833,
|
| 981 |
+
833,
|
| 982 |
+
2
|
| 983 |
+
],
|
| 984 |
+
[
|
| 985 |
+
841,
|
| 986 |
+
841,
|
| 987 |
+
1
|
| 988 |
+
],
|
| 989 |
+
[
|
| 990 |
+
843,
|
| 991 |
+
843,
|
| 992 |
+
2
|
| 993 |
+
],
|
| 994 |
+
[
|
| 995 |
+
117,
|
| 996 |
+
117,
|
| 997 |
+
1
|
| 998 |
+
],
|
| 999 |
+
[
|
| 1000 |
+
125,
|
| 1001 |
+
125,
|
| 1002 |
+
1
|
| 1003 |
+
],
|
| 1004 |
+
[
|
| 1005 |
+
836,
|
| 1006 |
+
836,
|
| 1007 |
+
2
|
| 1008 |
+
],
|
| 1009 |
+
[
|
| 1010 |
+
846,
|
| 1011 |
+
846,
|
| 1012 |
+
2
|
| 1013 |
+
],
|
| 1014 |
+
[
|
| 1015 |
+
852,
|
| 1016 |
+
852,
|
| 1017 |
+
2
|
| 1018 |
+
],
|
| 1019 |
+
[
|
| 1020 |
+
1024,
|
| 1021 |
+
824,
|
| 1022 |
+
2
|
| 1023 |
+
],
|
| 1024 |
+
[
|
| 1025 |
+
853,
|
| 1026 |
+
853,
|
| 1027 |
+
2
|
| 1028 |
+
],
|
| 1029 |
+
[
|
| 1030 |
+
825,
|
| 1031 |
+
825,
|
| 1032 |
+
1
|
| 1033 |
+
],
|
| 1034 |
+
[
|
| 1035 |
+
845,
|
| 1036 |
+
845,
|
| 1037 |
+
2
|
| 1038 |
+
],
|
| 1039 |
+
[
|
| 1040 |
+
826,
|
| 1041 |
+
826,
|
| 1042 |
+
1
|
| 1043 |
+
],
|
| 1044 |
+
[
|
| 1045 |
+
820,
|
| 1046 |
+
820,
|
| 1047 |
+
1
|
| 1048 |
+
],
|
| 1049 |
+
[
|
| 1050 |
+
818,
|
| 1051 |
+
818,
|
| 1052 |
+
2
|
| 1053 |
+
],
|
| 1054 |
+
[
|
| 1055 |
+
200,
|
| 1056 |
+
200,
|
| 1057 |
+
1
|
| 1058 |
+
],
|
| 1059 |
+
[
|
| 1060 |
+
206,
|
| 1061 |
+
206,
|
| 1062 |
+
1
|
| 1063 |
+
],
|
| 1064 |
+
[
|
| 1065 |
+
202,
|
| 1066 |
+
202,
|
| 1067 |
+
1
|
| 1068 |
+
],
|
| 1069 |
+
[
|
| 1070 |
+
37,
|
| 1071 |
+
37,
|
| 1072 |
+
1
|
| 1073 |
+
],
|
| 1074 |
+
[
|
| 1075 |
+
861,
|
| 1076 |
+
861,
|
| 1077 |
+
2
|
| 1078 |
+
],
|
| 1079 |
+
[
|
| 1080 |
+
724,
|
| 1081 |
+
724,
|
| 1082 |
+
1
|
| 1083 |
+
],
|
| 1084 |
+
[
|
| 1085 |
+
205,
|
| 1086 |
+
205,
|
| 1087 |
+
1
|
| 1088 |
+
],
|
| 1089 |
+
[
|
| 1090 |
+
43,
|
| 1091 |
+
43,
|
| 1092 |
+
1
|
| 1093 |
+
],
|
| 1094 |
+
[
|
| 1095 |
+
862,
|
| 1096 |
+
862,
|
| 1097 |
+
1
|
| 1098 |
+
],
|
| 1099 |
+
[
|
| 1100 |
+
827,
|
| 1101 |
+
827,
|
| 1102 |
+
2
|
| 1103 |
+
],
|
| 1104 |
+
[
|
| 1105 |
+
42,
|
| 1106 |
+
42,
|
| 1107 |
+
1
|
| 1108 |
+
],
|
| 1109 |
+
[
|
| 1110 |
+
118,
|
| 1111 |
+
118,
|
| 1112 |
+
1
|
| 1113 |
+
],
|
| 1114 |
+
[
|
| 1115 |
+
863,
|
| 1116 |
+
863,
|
| 1117 |
+
1
|
| 1118 |
+
],
|
| 1119 |
+
[
|
| 1120 |
+
38,
|
| 1121 |
+
38,
|
| 1122 |
+
1
|
| 1123 |
+
],
|
| 1124 |
+
[
|
| 1125 |
+
1028,
|
| 1126 |
+
1028,
|
| 1127 |
+
2
|
| 1128 |
+
],
|
| 1129 |
+
[
|
| 1130 |
+
36,
|
| 1131 |
+
36,
|
| 1132 |
+
1
|
| 1133 |
+
],
|
| 1134 |
+
[
|
| 1135 |
+
40,
|
| 1136 |
+
40,
|
| 1137 |
+
2
|
| 1138 |
+
],
|
| 1139 |
+
[
|
| 1140 |
+
1029,
|
| 1141 |
+
1029,
|
| 1142 |
+
2
|
| 1143 |
+
],
|
| 1144 |
+
[
|
| 1145 |
+
528,
|
| 1146 |
+
527,
|
| 1147 |
+
1
|
| 1148 |
+
],
|
| 1149 |
+
[
|
| 1150 |
+
124,
|
| 1151 |
+
124,
|
| 1152 |
+
1
|
| 1153 |
+
],
|
| 1154 |
+
[
|
| 1155 |
+
201,
|
| 1156 |
+
201,
|
| 1157 |
+
1
|
| 1158 |
+
],
|
| 1159 |
+
[
|
| 1160 |
+
121,
|
| 1161 |
+
121,
|
| 1162 |
+
1
|
| 1163 |
+
],
|
| 1164 |
+
[
|
| 1165 |
+
119,
|
| 1166 |
+
119,
|
| 1167 |
+
1
|
| 1168 |
+
],
|
| 1169 |
+
[
|
| 1170 |
+
526,
|
| 1171 |
+
527,
|
| 1172 |
+
1
|
| 1173 |
+
],
|
| 1174 |
+
[
|
| 1175 |
+
120,
|
| 1176 |
+
120,
|
| 1177 |
+
1
|
| 1178 |
+
],
|
| 1179 |
+
[
|
| 1180 |
+
204,
|
| 1181 |
+
204,
|
| 1182 |
+
1
|
| 1183 |
+
],
|
| 1184 |
+
[
|
| 1185 |
+
858,
|
| 1186 |
+
858,
|
| 1187 |
+
2
|
| 1188 |
+
],
|
| 1189 |
+
[
|
| 1190 |
+
534,
|
| 1191 |
+
534,
|
| 1192 |
+
1
|
| 1193 |
+
],
|
| 1194 |
+
[
|
| 1195 |
+
123,
|
| 1196 |
+
123,
|
| 1197 |
+
1
|
| 1198 |
+
],
|
| 1199 |
+
[
|
| 1200 |
+
530,
|
| 1201 |
+
529,
|
| 1202 |
+
1
|
| 1203 |
+
],
|
| 1204 |
+
[
|
| 1205 |
+
122,
|
| 1206 |
+
122,
|
| 1207 |
+
1
|
| 1208 |
+
],
|
| 1209 |
+
[
|
| 1210 |
+
44,
|
| 1211 |
+
44,
|
| 1212 |
+
1
|
| 1213 |
+
],
|
| 1214 |
+
[
|
| 1215 |
+
824,
|
| 1216 |
+
824,
|
| 1217 |
+
2
|
| 1218 |
+
],
|
| 1219 |
+
[
|
| 1220 |
+
851,
|
| 1221 |
+
851,
|
| 1222 |
+
2
|
| 1223 |
+
],
|
| 1224 |
+
[
|
| 1225 |
+
199,
|
| 1226 |
+
199,
|
| 1227 |
+
1
|
| 1228 |
+
],
|
| 1229 |
+
[
|
| 1230 |
+
718,
|
| 1231 |
+
718,
|
| 1232 |
+
1
|
| 1233 |
+
],
|
| 1234 |
+
[
|
| 1235 |
+
717,
|
| 1236 |
+
717,
|
| 1237 |
+
1
|
| 1238 |
+
],
|
| 1239 |
+
[
|
| 1240 |
+
203,
|
| 1241 |
+
203,
|
| 1242 |
+
1
|
| 1243 |
+
],
|
| 1244 |
+
[
|
| 1245 |
+
723,
|
| 1246 |
+
723,
|
| 1247 |
+
1
|
| 1248 |
+
],
|
| 1249 |
+
[
|
| 1250 |
+
828,
|
| 1251 |
+
828,
|
| 1252 |
+
2
|
| 1253 |
+
],
|
| 1254 |
+
[
|
| 1255 |
+
207,
|
| 1256 |
+
207,
|
| 1257 |
+
1
|
| 1258 |
+
],
|
| 1259 |
+
[
|
| 1260 |
+
530,
|
| 1261 |
+
531,
|
| 1262 |
+
1
|
| 1263 |
+
],
|
| 1264 |
+
[
|
| 1265 |
+
528,
|
| 1266 |
+
529,
|
| 1267 |
+
1
|
| 1268 |
+
],
|
| 1269 |
+
[
|
| 1270 |
+
524,
|
| 1271 |
+
524,
|
| 1272 |
+
1
|
| 1273 |
+
],
|
| 1274 |
+
[
|
| 1275 |
+
39,
|
| 1276 |
+
39,
|
| 1277 |
+
1
|
| 1278 |
+
],
|
| 1279 |
+
[
|
| 1280 |
+
526,
|
| 1281 |
+
525,
|
| 1282 |
+
1
|
| 1283 |
+
],
|
| 1284 |
+
[
|
| 1285 |
+
41,
|
| 1286 |
+
41,
|
| 1287 |
+
1
|
| 1288 |
+
],
|
| 1289 |
+
[
|
| 1290 |
+
720,
|
| 1291 |
+
720,
|
| 1292 |
+
1
|
| 1293 |
+
],
|
| 1294 |
+
[
|
| 1295 |
+
855,
|
| 1296 |
+
855,
|
| 1297 |
+
2
|
| 1298 |
+
],
|
| 1299 |
+
[
|
| 1300 |
+
870,
|
| 1301 |
+
870,
|
| 1302 |
+
2
|
| 1303 |
+
],
|
| 1304 |
+
[
|
| 1305 |
+
716,
|
| 1306 |
+
716,
|
| 1307 |
+
1
|
| 1308 |
+
],
|
| 1309 |
+
[
|
| 1310 |
+
532,
|
| 1311 |
+
531,
|
| 1312 |
+
1
|
| 1313 |
+
],
|
| 1314 |
+
[
|
| 1315 |
+
35,
|
| 1316 |
+
35,
|
| 1317 |
+
1
|
| 1318 |
+
],
|
| 1319 |
+
[
|
| 1320 |
+
880,
|
| 1321 |
+
880,
|
| 1322 |
+
1
|
| 1323 |
+
],
|
| 1324 |
+
[
|
| 1325 |
+
197,
|
| 1326 |
+
197,
|
| 1327 |
+
1
|
| 1328 |
+
],
|
| 1329 |
+
[
|
| 1330 |
+
831,
|
| 1331 |
+
831,
|
| 1332 |
+
3
|
| 1333 |
+
],
|
| 1334 |
+
[
|
| 1335 |
+
45,
|
| 1336 |
+
45,
|
| 1337 |
+
1
|
| 1338 |
+
],
|
| 1339 |
+
[
|
| 1340 |
+
116,
|
| 1341 |
+
116,
|
| 1342 |
+
1
|
| 1343 |
+
],
|
| 1344 |
+
[
|
| 1345 |
+
881,
|
| 1346 |
+
881,
|
| 1347 |
+
1
|
| 1348 |
+
],
|
| 1349 |
+
[
|
| 1350 |
+
126,
|
| 1351 |
+
126,
|
| 1352 |
+
1
|
| 1353 |
+
],
|
| 1354 |
+
[
|
| 1355 |
+
823,
|
| 1356 |
+
823,
|
| 1357 |
+
1
|
| 1358 |
+
],
|
| 1359 |
+
[
|
| 1360 |
+
719,
|
| 1361 |
+
719,
|
| 1362 |
+
1
|
| 1363 |
+
],
|
| 1364 |
+
[
|
| 1365 |
+
867,
|
| 1366 |
+
867,
|
| 1367 |
+
2
|
| 1368 |
+
],
|
| 1369 |
+
[
|
| 1370 |
+
532,
|
| 1371 |
+
533,
|
| 1372 |
+
1
|
| 1373 |
+
],
|
| 1374 |
+
[
|
| 1375 |
+
721,
|
| 1376 |
+
721,
|
| 1377 |
+
1
|
| 1378 |
+
],
|
| 1379 |
+
[
|
| 1380 |
+
722,
|
| 1381 |
+
722,
|
| 1382 |
+
1
|
| 1383 |
+
],
|
| 1384 |
+
[
|
| 1385 |
+
875,
|
| 1386 |
+
875,
|
| 1387 |
+
1
|
| 1388 |
+
],
|
| 1389 |
+
[
|
| 1390 |
+
856,
|
| 1391 |
+
834,
|
| 1392 |
+
1
|
| 1393 |
+
],
|
| 1394 |
+
[
|
| 1395 |
+
877,
|
| 1396 |
+
877,
|
| 1397 |
+
1
|
| 1398 |
+
],
|
| 1399 |
+
[
|
| 1400 |
+
848,
|
| 1401 |
+
862,
|
| 1402 |
+
1
|
| 1403 |
+
],
|
| 1404 |
+
[
|
| 1405 |
+
876,
|
| 1406 |
+
876,
|
| 1407 |
+
2
|
| 1408 |
+
],
|
| 1409 |
+
[
|
| 1410 |
+
848,
|
| 1411 |
+
840,
|
| 1412 |
+
1
|
| 1413 |
+
],
|
| 1414 |
+
[
|
| 1415 |
+
849,
|
| 1416 |
+
863,
|
| 1417 |
+
1
|
| 1418 |
+
],
|
| 1419 |
+
[
|
| 1420 |
+
878,
|
| 1421 |
+
878,
|
| 1422 |
+
2
|
| 1423 |
+
],
|
| 1424 |
+
[
|
| 1425 |
+
857,
|
| 1426 |
+
837,
|
| 1427 |
+
1
|
| 1428 |
+
],
|
| 1429 |
+
[
|
| 1430 |
+
829,
|
| 1431 |
+
829,
|
| 1432 |
+
3
|
| 1433 |
+
],
|
| 1434 |
+
[
|
| 1435 |
+
849,
|
| 1436 |
+
841,
|
| 1437 |
+
1
|
| 1438 |
+
],
|
| 1439 |
+
[
|
| 1440 |
+
724,
|
| 1441 |
+
725,
|
| 1442 |
+
1
|
| 1443 |
+
],
|
| 1444 |
+
[
|
| 1445 |
+
534,
|
| 1446 |
+
533,
|
| 1447 |
+
1
|
| 1448 |
+
],
|
| 1449 |
+
[
|
| 1450 |
+
715,
|
| 1451 |
+
715,
|
| 1452 |
+
1
|
| 1453 |
+
],
|
| 1454 |
+
[
|
| 1455 |
+
833,
|
| 1456 |
+
833,
|
| 1457 |
+
1
|
| 1458 |
+
],
|
| 1459 |
+
[
|
| 1460 |
+
836,
|
| 1461 |
+
836,
|
| 1462 |
+
1
|
| 1463 |
+
],
|
| 1464 |
+
[
|
| 1465 |
+
1026,
|
| 1466 |
+
828,
|
| 1467 |
+
2
|
| 1468 |
+
],
|
| 1469 |
+
[
|
| 1470 |
+
832,
|
| 1471 |
+
832,
|
| 1472 |
+
3
|
| 1473 |
+
],
|
| 1474 |
+
[
|
| 1475 |
+
1033,
|
| 1476 |
+
1033,
|
| 1477 |
+
4
|
| 1478 |
+
],
|
| 1479 |
+
[
|
| 1480 |
+
854,
|
| 1481 |
+
854,
|
| 1482 |
+
2
|
| 1483 |
+
],
|
| 1484 |
+
[
|
| 1485 |
+
830,
|
| 1486 |
+
830,
|
| 1487 |
+
2
|
| 1488 |
+
],
|
| 1489 |
+
[
|
| 1490 |
+
835,
|
| 1491 |
+
835,
|
| 1492 |
+
3
|
| 1493 |
+
],
|
| 1494 |
+
[
|
| 1495 |
+
535,
|
| 1496 |
+
535,
|
| 1497 |
+
1
|
| 1498 |
+
],
|
| 1499 |
+
[
|
| 1500 |
+
524,
|
| 1501 |
+
525,
|
| 1502 |
+
1
|
| 1503 |
+
],
|
| 1504 |
+
[
|
| 1505 |
+
885,
|
| 1506 |
+
885,
|
| 1507 |
+
2
|
| 1508 |
+
],
|
| 1509 |
+
[
|
| 1510 |
+
887,
|
| 1511 |
+
887,
|
| 1512 |
+
2
|
| 1513 |
+
],
|
| 1514 |
+
[
|
| 1515 |
+
825,
|
| 1516 |
+
840,
|
| 1517 |
+
1
|
| 1518 |
+
],
|
| 1519 |
+
[
|
| 1520 |
+
826,
|
| 1521 |
+
841,
|
| 1522 |
+
1
|
| 1523 |
+
],
|
| 1524 |
+
[
|
| 1525 |
+
523,
|
| 1526 |
+
523,
|
| 1527 |
+
1
|
| 1528 |
+
],
|
| 1529 |
+
[
|
| 1530 |
+
838,
|
| 1531 |
+
838,
|
| 1532 |
+
3
|
| 1533 |
+
],
|
| 1534 |
+
[
|
| 1535 |
+
843,
|
| 1536 |
+
827,
|
| 1537 |
+
2
|
| 1538 |
+
],
|
| 1539 |
+
[
|
| 1540 |
+
208,
|
| 1541 |
+
208,
|
| 1542 |
+
1
|
| 1543 |
+
],
|
| 1544 |
+
[
|
| 1545 |
+
845,
|
| 1546 |
+
827,
|
| 1547 |
+
2
|
| 1548 |
+
],
|
| 1549 |
+
[
|
| 1550 |
+
873,
|
| 1551 |
+
873,
|
| 1552 |
+
2
|
| 1553 |
+
],
|
| 1554 |
+
[
|
| 1555 |
+
894,
|
| 1556 |
+
894,
|
| 1557 |
+
2
|
| 1558 |
+
],
|
| 1559 |
+
[
|
| 1560 |
+
871,
|
| 1561 |
+
871,
|
| 1562 |
+
1
|
| 1563 |
+
],
|
| 1564 |
+
[
|
| 1565 |
+
725,
|
| 1566 |
+
725,
|
| 1567 |
+
1
|
| 1568 |
+
],
|
| 1569 |
+
[
|
| 1570 |
+
897,
|
| 1571 |
+
897,
|
| 1572 |
+
2
|
| 1573 |
+
],
|
| 1574 |
+
[
|
| 1575 |
+
872,
|
| 1576 |
+
872,
|
| 1577 |
+
1
|
| 1578 |
+
],
|
| 1579 |
+
[
|
| 1580 |
+
856,
|
| 1581 |
+
880,
|
| 1582 |
+
1
|
| 1583 |
+
],
|
| 1584 |
+
[
|
| 1585 |
+
34,
|
| 1586 |
+
34,
|
| 1587 |
+
1
|
| 1588 |
+
],
|
| 1589 |
+
[
|
| 1590 |
+
874,
|
| 1591 |
+
874,
|
| 1592 |
+
1
|
| 1593 |
+
],
|
| 1594 |
+
[
|
| 1595 |
+
857,
|
| 1596 |
+
881,
|
| 1597 |
+
1
|
| 1598 |
+
],
|
| 1599 |
+
[
|
| 1600 |
+
900,
|
| 1601 |
+
900,
|
| 1602 |
+
1
|
| 1603 |
+
],
|
| 1604 |
+
[
|
| 1605 |
+
903,
|
| 1606 |
+
903,
|
| 1607 |
+
1
|
| 1608 |
+
],
|
| 1609 |
+
[
|
| 1610 |
+
818,
|
| 1611 |
+
818,
|
| 1612 |
+
1
|
| 1613 |
+
],
|
| 1614 |
+
[
|
| 1615 |
+
898,
|
| 1616 |
+
898,
|
| 1617 |
+
2
|
| 1618 |
+
],
|
| 1619 |
+
[
|
| 1620 |
+
899,
|
| 1621 |
+
899,
|
| 1622 |
+
2
|
| 1623 |
+
],
|
| 1624 |
+
[
|
| 1625 |
+
206,
|
| 1626 |
+
205,
|
| 1627 |
+
1
|
| 1628 |
+
],
|
| 1629 |
+
[
|
| 1630 |
+
1031,
|
| 1631 |
+
527,
|
| 1632 |
+
2
|
| 1633 |
+
],
|
| 1634 |
+
[
|
| 1635 |
+
831,
|
| 1636 |
+
831,
|
| 1637 |
+
1
|
| 1638 |
+
],
|
| 1639 |
+
[
|
| 1640 |
+
829,
|
| 1641 |
+
829,
|
| 1642 |
+
1
|
| 1643 |
+
],
|
| 1644 |
+
[
|
| 1645 |
+
825,
|
| 1646 |
+
820,
|
| 1647 |
+
1
|
| 1648 |
+
],
|
| 1649 |
+
[
|
| 1650 |
+
826,
|
| 1651 |
+
820,
|
| 1652 |
+
1
|
| 1653 |
+
],
|
| 1654 |
+
[
|
| 1655 |
+
837,
|
| 1656 |
+
837,
|
| 1657 |
+
2
|
| 1658 |
+
],
|
| 1659 |
+
[
|
| 1660 |
+
869,
|
| 1661 |
+
869,
|
| 1662 |
+
2
|
| 1663 |
+
],
|
| 1664 |
+
[
|
| 1665 |
+
875,
|
| 1666 |
+
862,
|
| 1667 |
+
1
|
| 1668 |
+
],
|
| 1669 |
+
[
|
| 1670 |
+
202,
|
| 1671 |
+
201,
|
| 1672 |
+
1
|
| 1673 |
+
],
|
| 1674 |
+
[
|
| 1675 |
+
534,
|
| 1676 |
+
535,
|
| 1677 |
+
1
|
| 1678 |
+
],
|
| 1679 |
+
[
|
| 1680 |
+
206,
|
| 1681 |
+
207,
|
| 1682 |
+
1
|
| 1683 |
+
],
|
| 1684 |
+
[
|
| 1685 |
+
838,
|
| 1686 |
+
838,
|
| 1687 |
+
1
|
| 1688 |
+
],
|
| 1689 |
+
[
|
| 1690 |
+
127,
|
| 1691 |
+
127,
|
| 1692 |
+
1
|
| 1693 |
+
],
|
| 1694 |
+
[
|
| 1695 |
+
877,
|
| 1696 |
+
863,
|
| 1697 |
+
1
|
| 1698 |
+
],
|
| 1699 |
+
[
|
| 1700 |
+
905,
|
| 1701 |
+
905,
|
| 1702 |
+
2
|
| 1703 |
+
],
|
| 1704 |
+
[
|
| 1705 |
+
904,
|
| 1706 |
+
904,
|
| 1707 |
+
2
|
| 1708 |
+
],
|
| 1709 |
+
[
|
| 1710 |
+
46,
|
| 1711 |
+
46,
|
| 1712 |
+
1
|
| 1713 |
+
],
|
| 1714 |
+
[
|
| 1715 |
+
115,
|
| 1716 |
+
115,
|
| 1717 |
+
1
|
| 1718 |
+
],
|
| 1719 |
+
[
|
| 1720 |
+
717,
|
| 1721 |
+
716,
|
| 1722 |
+
1
|
| 1723 |
+
],
|
| 1724 |
+
[
|
| 1725 |
+
910,
|
| 1726 |
+
910,
|
| 1727 |
+
2
|
| 1728 |
+
],
|
| 1729 |
+
[
|
| 1730 |
+
833,
|
| 1731 |
+
833,
|
| 1732 |
+
3
|
| 1733 |
+
],
|
| 1734 |
+
[
|
| 1735 |
+
916,
|
| 1736 |
+
916,
|
| 1737 |
+
1
|
| 1738 |
+
],
|
| 1739 |
+
[
|
| 1740 |
+
843,
|
| 1741 |
+
843,
|
| 1742 |
+
1
|
| 1743 |
+
],
|
| 1744 |
+
[
|
| 1745 |
+
724,
|
| 1746 |
+
723,
|
| 1747 |
+
1
|
| 1748 |
+
],
|
| 1749 |
+
[
|
| 1750 |
+
834,
|
| 1751 |
+
834,
|
| 1752 |
+
2
|
| 1753 |
+
],
|
| 1754 |
+
[
|
| 1755 |
+
717,
|
| 1756 |
+
718,
|
| 1757 |
+
1
|
| 1758 |
+
],
|
| 1759 |
+
[
|
| 1760 |
+
200,
|
| 1761 |
+
199,
|
| 1762 |
+
1
|
| 1763 |
+
],
|
| 1764 |
+
[
|
| 1765 |
+
836,
|
| 1766 |
+
836,
|
| 1767 |
+
3
|
| 1768 |
+
],
|
| 1769 |
+
[
|
| 1770 |
+
886,
|
| 1771 |
+
886,
|
| 1772 |
+
1
|
| 1773 |
+
],
|
| 1774 |
+
[
|
| 1775 |
+
868,
|
| 1776 |
+
868,
|
| 1777 |
+
2
|
| 1778 |
+
],
|
| 1779 |
+
[
|
| 1780 |
+
918,
|
| 1781 |
+
918,
|
| 1782 |
+
1
|
| 1783 |
+
],
|
| 1784 |
+
[
|
| 1785 |
+
197,
|
| 1786 |
+
198,
|
| 1787 |
+
1
|
| 1788 |
+
],
|
| 1789 |
+
[
|
| 1790 |
+
196,
|
| 1791 |
+
196,
|
| 1792 |
+
1
|
| 1793 |
+
],
|
| 1794 |
+
[
|
| 1795 |
+
845,
|
| 1796 |
+
845,
|
| 1797 |
+
1
|
| 1798 |
+
],
|
| 1799 |
+
[
|
| 1800 |
+
117,
|
| 1801 |
+
116,
|
| 1802 |
+
1
|
| 1803 |
+
],
|
| 1804 |
+
[
|
| 1805 |
+
851,
|
| 1806 |
+
851,
|
| 1807 |
+
1
|
| 1808 |
+
],
|
| 1809 |
+
[
|
| 1810 |
+
715,
|
| 1811 |
+
714,
|
| 1812 |
+
1
|
| 1813 |
+
],
|
| 1814 |
+
[
|
| 1815 |
+
890,
|
| 1816 |
+
890,
|
| 1817 |
+
1
|
| 1818 |
+
],
|
| 1819 |
+
[
|
| 1820 |
+
889,
|
| 1821 |
+
889,
|
| 1822 |
+
1
|
| 1823 |
+
],
|
| 1824 |
+
[
|
| 1825 |
+
715,
|
| 1826 |
+
716,
|
| 1827 |
+
1
|
| 1828 |
+
],
|
| 1829 |
+
[
|
| 1830 |
+
37,
|
| 1831 |
+
36,
|
| 1832 |
+
1
|
| 1833 |
+
],
|
| 1834 |
+
[
|
| 1835 |
+
125,
|
| 1836 |
+
126,
|
| 1837 |
+
1
|
| 1838 |
+
],
|
| 1839 |
+
[
|
| 1840 |
+
524,
|
| 1841 |
+
523,
|
| 1842 |
+
1
|
| 1843 |
+
],
|
| 1844 |
+
[
|
| 1845 |
+
847,
|
| 1846 |
+
835,
|
| 1847 |
+
2
|
| 1848 |
+
],
|
| 1849 |
+
[
|
| 1850 |
+
37,
|
| 1851 |
+
38,
|
| 1852 |
+
1
|
| 1853 |
+
],
|
| 1854 |
+
[
|
| 1855 |
+
43,
|
| 1856 |
+
42,
|
| 1857 |
+
1
|
| 1858 |
+
],
|
| 1859 |
+
[
|
| 1860 |
+
200,
|
| 1861 |
+
201,
|
| 1862 |
+
1
|
| 1863 |
+
],
|
| 1864 |
+
[
|
| 1865 |
+
536,
|
| 1866 |
+
536,
|
| 1867 |
+
1
|
| 1868 |
+
],
|
| 1869 |
+
[
|
| 1870 |
+
726,
|
| 1871 |
+
726,
|
| 1872 |
+
1
|
| 1873 |
+
],
|
| 1874 |
+
[
|
| 1875 |
+
204,
|
| 1876 |
+
203,
|
| 1877 |
+
1
|
| 1878 |
+
],
|
| 1879 |
+
[
|
| 1880 |
+
858,
|
| 1881 |
+
858,
|
| 1882 |
+
1
|
| 1883 |
+
],
|
| 1884 |
+
[
|
| 1885 |
+
855,
|
| 1886 |
+
855,
|
| 1887 |
+
1
|
| 1888 |
+
],
|
| 1889 |
+
[
|
| 1890 |
+
531,
|
| 1891 |
+
530,
|
| 1892 |
+
1
|
| 1893 |
+
],
|
| 1894 |
+
[
|
| 1895 |
+
529,
|
| 1896 |
+
530,
|
| 1897 |
+
1
|
| 1898 |
+
],
|
| 1899 |
+
[
|
| 1900 |
+
529,
|
| 1901 |
+
528,
|
| 1902 |
+
1
|
| 1903 |
+
],
|
| 1904 |
+
[
|
| 1905 |
+
117,
|
| 1906 |
+
118,
|
| 1907 |
+
1
|
| 1908 |
+
],
|
| 1909 |
+
[
|
| 1910 |
+
202,
|
| 1911 |
+
203,
|
| 1912 |
+
1
|
| 1913 |
+
],
|
| 1914 |
+
[
|
| 1915 |
+
204,
|
| 1916 |
+
205,
|
| 1917 |
+
1
|
| 1918 |
+
],
|
| 1919 |
+
[
|
| 1920 |
+
35,
|
| 1921 |
+
36,
|
| 1922 |
+
1
|
| 1923 |
+
],
|
| 1924 |
+
[
|
| 1925 |
+
125,
|
| 1926 |
+
124,
|
| 1927 |
+
1
|
| 1928 |
+
],
|
| 1929 |
+
[
|
| 1930 |
+
824,
|
| 1931 |
+
1024,
|
| 1932 |
+
2
|
| 1933 |
+
],
|
| 1934 |
+
[
|
| 1935 |
+
895,
|
| 1936 |
+
895,
|
| 1937 |
+
1
|
| 1938 |
+
],
|
| 1939 |
+
[
|
| 1940 |
+
832,
|
| 1941 |
+
832,
|
| 1942 |
+
1
|
| 1943 |
+
],
|
| 1944 |
+
[
|
| 1945 |
+
121,
|
| 1946 |
+
120,
|
| 1947 |
+
1
|
| 1948 |
+
],
|
| 1949 |
+
[
|
| 1950 |
+
43,
|
| 1951 |
+
44,
|
| 1952 |
+
1
|
| 1953 |
+
],
|
| 1954 |
+
[
|
| 1955 |
+
119,
|
| 1956 |
+
118,
|
| 1957 |
+
1
|
| 1958 |
+
],
|
| 1959 |
+
[
|
| 1960 |
+
888,
|
| 1961 |
+
888,
|
| 1962 |
+
1
|
| 1963 |
+
],
|
| 1964 |
+
[
|
| 1965 |
+
527,
|
| 1966 |
+
528,
|
| 1967 |
+
1
|
| 1968 |
+
],
|
| 1969 |
+
[
|
| 1970 |
+
522,
|
| 1971 |
+
522,
|
| 1972 |
+
1
|
| 1973 |
+
],
|
| 1974 |
+
[
|
| 1975 |
+
121,
|
| 1976 |
+
122,
|
| 1977 |
+
1
|
| 1978 |
+
],
|
| 1979 |
+
[
|
| 1980 |
+
853,
|
| 1981 |
+
835,
|
| 1982 |
+
2
|
| 1983 |
+
],
|
| 1984 |
+
[
|
| 1985 |
+
831,
|
| 1986 |
+
831,
|
| 1987 |
+
4
|
| 1988 |
+
],
|
| 1989 |
+
[
|
| 1990 |
+
847,
|
| 1991 |
+
829,
|
| 1992 |
+
2
|
| 1993 |
+
],
|
| 1994 |
+
[
|
| 1995 |
+
891,
|
| 1996 |
+
891,
|
| 1997 |
+
1
|
| 1998 |
+
],
|
| 1999 |
+
[
|
| 2000 |
+
526,
|
| 2001 |
+
1050,
|
| 2002 |
+
1
|
| 2003 |
+
],
|
| 2004 |
+
[
|
| 2005 |
+
123,
|
| 2006 |
+
124,
|
| 2007 |
+
1
|
| 2008 |
+
],
|
| 2009 |
+
[
|
| 2010 |
+
45,
|
| 2011 |
+
44,
|
| 2012 |
+
1
|
| 2013 |
+
],
|
| 2014 |
+
[
|
| 2015 |
+
719,
|
| 2016 |
+
718,
|
| 2017 |
+
1
|
| 2018 |
+
],
|
| 2019 |
+
[
|
| 2020 |
+
911,
|
| 2021 |
+
911,
|
| 2022 |
+
1
|
| 2023 |
+
],
|
| 2024 |
+
[
|
| 2025 |
+
722,
|
| 2026 |
+
723,
|
| 2027 |
+
1
|
| 2028 |
+
],
|
| 2029 |
+
[
|
| 2030 |
+
1051,
|
| 2031 |
+
198,
|
| 2032 |
+
2
|
| 2033 |
+
],
|
| 2034 |
+
[
|
| 2035 |
+
919,
|
| 2036 |
+
919,
|
| 2037 |
+
2
|
| 2038 |
+
],
|
| 2039 |
+
[
|
| 2040 |
+
39,
|
| 2041 |
+
38,
|
| 2042 |
+
1
|
| 2043 |
+
],
|
| 2044 |
+
[
|
| 2045 |
+
824,
|
| 2046 |
+
824,
|
| 2047 |
+
3
|
| 2048 |
+
],
|
| 2049 |
+
[
|
| 2050 |
+
846,
|
| 2051 |
+
846,
|
| 2052 |
+
3
|
| 2053 |
+
],
|
| 2054 |
+
[
|
| 2055 |
+
119,
|
| 2056 |
+
120,
|
| 2057 |
+
1
|
| 2058 |
+
],
|
| 2059 |
+
[
|
| 2060 |
+
41,
|
| 2061 |
+
42,
|
| 2062 |
+
1
|
| 2063 |
+
],
|
| 2064 |
+
[
|
| 2065 |
+
930,
|
| 2066 |
+
930,
|
| 2067 |
+
1
|
| 2068 |
+
],
|
| 2069 |
+
[
|
| 2070 |
+
932,
|
| 2071 |
+
932,
|
| 2072 |
+
1
|
| 2073 |
+
],
|
| 2074 |
+
[
|
| 2075 |
+
864,
|
| 2076 |
+
864,
|
| 2077 |
+
2
|
| 2078 |
+
],
|
| 2079 |
+
[
|
| 2080 |
+
1039,
|
| 2081 |
+
531,
|
| 2082 |
+
2
|
| 2083 |
+
],
|
| 2084 |
+
[
|
| 2085 |
+
720,
|
| 2086 |
+
721,
|
| 2087 |
+
1
|
| 2088 |
+
],
|
| 2089 |
+
[
|
| 2090 |
+
913,
|
| 2091 |
+
913,
|
| 2092 |
+
1
|
| 2093 |
+
],
|
| 2094 |
+
[
|
| 2095 |
+
875,
|
| 2096 |
+
900,
|
| 2097 |
+
1
|
| 2098 |
+
],
|
| 2099 |
+
[
|
| 2100 |
+
123,
|
| 2101 |
+
122,
|
| 2102 |
+
1
|
| 2103 |
+
],
|
| 2104 |
+
[
|
| 2105 |
+
877,
|
| 2106 |
+
903,
|
| 2107 |
+
1
|
| 2108 |
+
],
|
| 2109 |
+
[
|
| 2110 |
+
1028,
|
| 2111 |
+
816,
|
| 2112 |
+
2
|
| 2113 |
+
],
|
| 2114 |
+
[
|
| 2115 |
+
852,
|
| 2116 |
+
852,
|
| 2117 |
+
3
|
| 2118 |
+
],
|
| 2119 |
+
[
|
| 2120 |
+
714,
|
| 2121 |
+
714,
|
| 2122 |
+
1
|
| 2123 |
+
],
|
| 2124 |
+
[
|
| 2125 |
+
867,
|
| 2126 |
+
867,
|
| 2127 |
+
1
|
| 2128 |
+
],
|
| 2129 |
+
[
|
| 2130 |
+
1037,
|
| 2131 |
+
835,
|
| 2132 |
+
4
|
| 2133 |
+
],
|
| 2134 |
+
[
|
| 2135 |
+
1046,
|
| 2136 |
+
533,
|
| 2137 |
+
2
|
| 2138 |
+
],
|
| 2139 |
+
[
|
| 2140 |
+
208,
|
| 2141 |
+
207,
|
| 2142 |
+
1
|
| 2143 |
+
],
|
| 2144 |
+
[
|
| 2145 |
+
209,
|
| 2146 |
+
209,
|
| 2147 |
+
1
|
| 2148 |
+
],
|
| 2149 |
+
[
|
| 2150 |
+
907,
|
| 2151 |
+
907,
|
| 2152 |
+
2
|
| 2153 |
+
],
|
| 2154 |
+
[
|
| 2155 |
+
879,
|
| 2156 |
+
879,
|
| 2157 |
+
1
|
| 2158 |
+
],
|
| 2159 |
+
[
|
| 2160 |
+
722,
|
| 2161 |
+
721,
|
| 2162 |
+
1
|
| 2163 |
+
],
|
| 2164 |
+
[
|
| 2165 |
+
33,
|
| 2166 |
+
33,
|
| 2167 |
+
1
|
| 2168 |
+
],
|
| 2169 |
+
[
|
| 2170 |
+
854,
|
| 2171 |
+
854,
|
| 2172 |
+
1
|
| 2173 |
+
],
|
| 2174 |
+
[
|
| 2175 |
+
199,
|
| 2176 |
+
198,
|
| 2177 |
+
1
|
| 2178 |
+
],
|
| 2179 |
+
[
|
| 2180 |
+
526,
|
| 2181 |
+
1031,
|
| 2182 |
+
1
|
| 2183 |
+
],
|
| 2184 |
+
[
|
| 2185 |
+
908,
|
| 2186 |
+
908,
|
| 2187 |
+
2
|
| 2188 |
+
],
|
| 2189 |
+
[
|
| 2190 |
+
828,
|
| 2191 |
+
828,
|
| 2192 |
+
3
|
| 2193 |
+
],
|
| 2194 |
+
[
|
| 2195 |
+
1086,
|
| 2196 |
+
1086,
|
| 2197 |
+
4
|
| 2198 |
+
],
|
| 2199 |
+
[
|
| 2200 |
+
1030,
|
| 2201 |
+
831,
|
| 2202 |
+
4
|
| 2203 |
+
],
|
| 2204 |
+
[
|
| 2205 |
+
827,
|
| 2206 |
+
843,
|
| 2207 |
+
2
|
| 2208 |
+
],
|
| 2209 |
+
[
|
| 2210 |
+
823,
|
| 2211 |
+
837,
|
| 2212 |
+
1
|
| 2213 |
+
],
|
| 2214 |
+
[
|
| 2215 |
+
726,
|
| 2216 |
+
725,
|
| 2217 |
+
1
|
| 2218 |
+
],
|
| 2219 |
+
[
|
| 2220 |
+
892,
|
| 2221 |
+
892,
|
| 2222 |
+
1
|
| 2223 |
+
],
|
| 2224 |
+
[
|
| 2225 |
+
833,
|
| 2226 |
+
833,
|
| 2227 |
+
4
|
| 2228 |
+
],
|
| 2229 |
+
[
|
| 2230 |
+
861,
|
| 2231 |
+
861,
|
| 2232 |
+
1
|
| 2233 |
+
],
|
| 2234 |
+
[
|
| 2235 |
+
1043,
|
| 2236 |
+
834,
|
| 2237 |
+
2
|
| 2238 |
+
],
|
| 2239 |
+
[
|
| 2240 |
+
943,
|
| 2241 |
+
943,
|
| 2242 |
+
2
|
| 2243 |
+
],
|
| 2244 |
+
[
|
| 2245 |
+
882,
|
| 2246 |
+
882,
|
| 2247 |
+
2
|
| 2248 |
+
],
|
| 2249 |
+
[
|
| 2250 |
+
827,
|
| 2251 |
+
845,
|
| 2252 |
+
2
|
| 2253 |
+
],
|
| 2254 |
+
[
|
| 2255 |
+
893,
|
| 2256 |
+
893,
|
| 2257 |
+
1
|
| 2258 |
+
],
|
| 2259 |
+
[
|
| 2260 |
+
902,
|
| 2261 |
+
902,
|
| 2262 |
+
1
|
| 2263 |
+
],
|
| 2264 |
+
[
|
| 2265 |
+
836,
|
| 2266 |
+
836,
|
| 2267 |
+
4
|
| 2268 |
+
],
|
| 2269 |
+
[
|
| 2270 |
+
832,
|
| 2271 |
+
832,
|
| 2272 |
+
4
|
| 2273 |
+
],
|
| 2274 |
+
[
|
| 2275 |
+
848,
|
| 2276 |
+
1077,
|
| 2277 |
+
1
|
| 2278 |
+
],
|
| 2279 |
+
[
|
| 2280 |
+
838,
|
| 2281 |
+
838,
|
| 2282 |
+
4
|
| 2283 |
+
],
|
| 2284 |
+
[
|
| 2285 |
+
1025,
|
| 2286 |
+
1024,
|
| 2287 |
+
4
|
| 2288 |
+
],
|
| 2289 |
+
[
|
| 2290 |
+
35,
|
| 2291 |
+
34,
|
| 2292 |
+
1
|
| 2293 |
+
],
|
| 2294 |
+
[
|
| 2295 |
+
945,
|
| 2296 |
+
945,
|
| 2297 |
+
2
|
| 2298 |
+
],
|
| 2299 |
+
[
|
| 2300 |
+
536,
|
| 2301 |
+
535,
|
| 2302 |
+
1
|
| 2303 |
+
],
|
| 2304 |
+
[
|
| 2305 |
+
916,
|
| 2306 |
+
880,
|
| 2307 |
+
1
|
| 2308 |
+
],
|
| 2309 |
+
[
|
| 2310 |
+
883,
|
| 2311 |
+
883,
|
| 2312 |
+
2
|
| 2313 |
+
],
|
| 2314 |
+
[
|
| 2315 |
+
720,
|
| 2316 |
+
719,
|
| 2317 |
+
1
|
| 2318 |
+
],
|
| 2319 |
+
[
|
| 2320 |
+
531,
|
| 2321 |
+
532,
|
| 2322 |
+
1
|
| 2323 |
+
],
|
| 2324 |
+
[
|
| 2325 |
+
849,
|
| 2326 |
+
1081,
|
| 2327 |
+
1
|
| 2328 |
+
],
|
| 2329 |
+
[
|
| 2330 |
+
128,
|
| 2331 |
+
128,
|
| 2332 |
+
1
|
| 2333 |
+
],
|
| 2334 |
+
[
|
| 2335 |
+
906,
|
| 2336 |
+
906,
|
| 2337 |
+
1
|
| 2338 |
+
],
|
| 2339 |
+
[
|
| 2340 |
+
47,
|
| 2341 |
+
47,
|
| 2342 |
+
1
|
| 2343 |
+
],
|
| 2344 |
+
[
|
| 2345 |
+
918,
|
| 2346 |
+
881,
|
| 2347 |
+
1
|
| 2348 |
+
],
|
| 2349 |
+
[
|
| 2350 |
+
40,
|
| 2351 |
+
40,
|
| 2352 |
+
1
|
| 2353 |
+
],
|
| 2354 |
+
[
|
| 2355 |
+
823,
|
| 2356 |
+
834,
|
| 2357 |
+
1
|
| 2358 |
+
],
|
| 2359 |
+
[
|
| 2360 |
+
1025,
|
| 2361 |
+
824,
|
| 2362 |
+
4
|
| 2363 |
+
],
|
| 2364 |
+
[
|
| 2365 |
+
942,
|
| 2366 |
+
942,
|
| 2367 |
+
2
|
| 2368 |
+
],
|
| 2369 |
+
[
|
| 2370 |
+
847,
|
| 2371 |
+
847,
|
| 2372 |
+
3
|
| 2373 |
+
],
|
| 2374 |
+
[
|
| 2375 |
+
536,
|
| 2376 |
+
537,
|
| 2377 |
+
1
|
| 2378 |
+
],
|
| 2379 |
+
[
|
| 2380 |
+
873,
|
| 2381 |
+
873,
|
| 2382 |
+
1
|
| 2383 |
+
],
|
| 2384 |
+
[
|
| 2385 |
+
528,
|
| 2386 |
+
528,
|
| 2387 |
+
2
|
| 2388 |
+
],
|
| 2389 |
+
[
|
| 2390 |
+
846,
|
| 2391 |
+
846,
|
| 2392 |
+
1
|
| 2393 |
+
],
|
| 2394 |
+
[
|
| 2395 |
+
527,
|
| 2396 |
+
526,
|
| 2397 |
+
1
|
| 2398 |
+
],
|
| 2399 |
+
[
|
| 2400 |
+
1053,
|
| 2401 |
+
840,
|
| 2402 |
+
2
|
| 2403 |
+
],
|
| 2404 |
+
[
|
| 2405 |
+
522,
|
| 2406 |
+
523,
|
| 2407 |
+
1
|
| 2408 |
+
],
|
| 2409 |
+
[
|
| 2410 |
+
828,
|
| 2411 |
+
1026,
|
| 2412 |
+
2
|
| 2413 |
+
],
|
| 2414 |
+
[
|
| 2415 |
+
525,
|
| 2416 |
+
526,
|
| 2417 |
+
1
|
| 2418 |
+
],
|
| 2419 |
+
[
|
| 2420 |
+
1055,
|
| 2421 |
+
841,
|
| 2422 |
+
2
|
| 2423 |
+
],
|
| 2424 |
+
[
|
| 2425 |
+
949,
|
| 2426 |
+
949,
|
| 2427 |
+
2
|
| 2428 |
+
],
|
| 2429 |
+
[
|
| 2430 |
+
853,
|
| 2431 |
+
853,
|
| 2432 |
+
3
|
| 2433 |
+
],
|
| 2434 |
+
[
|
| 2435 |
+
114,
|
| 2436 |
+
114,
|
| 2437 |
+
1
|
| 2438 |
+
],
|
| 2439 |
+
[
|
| 2440 |
+
726,
|
| 2441 |
+
727,
|
| 2442 |
+
1
|
| 2443 |
+
],
|
| 2444 |
+
[
|
| 2445 |
+
1067,
|
| 2446 |
+
820,
|
| 2447 |
+
2
|
| 2448 |
+
],
|
| 2449 |
+
[
|
| 2450 |
+
1047,
|
| 2451 |
+
837,
|
| 2452 |
+
2
|
| 2453 |
+
],
|
| 2454 |
+
[
|
| 2455 |
+
980,
|
| 2456 |
+
980,
|
| 2457 |
+
1
|
| 2458 |
+
],
|
| 2459 |
+
[
|
| 2460 |
+
852,
|
| 2461 |
+
852,
|
| 2462 |
+
1
|
| 2463 |
+
],
|
| 2464 |
+
[
|
| 2465 |
+
825,
|
| 2466 |
+
825,
|
| 2467 |
+
2
|
| 2468 |
+
],
|
| 2469 |
+
[
|
| 2470 |
+
830,
|
| 2471 |
+
830,
|
| 2472 |
+
1
|
| 2473 |
+
],
|
| 2474 |
+
[
|
| 2475 |
+
870,
|
| 2476 |
+
870,
|
| 2477 |
+
1
|
| 2478 |
+
],
|
| 2479 |
+
[
|
| 2480 |
+
1083,
|
| 2481 |
+
1083,
|
| 2482 |
+
4
|
| 2483 |
+
],
|
| 2484 |
+
[
|
| 2485 |
+
874,
|
| 2486 |
+
874,
|
| 2487 |
+
2
|
| 2488 |
+
],
|
| 2489 |
+
[
|
| 2490 |
+
1032,
|
| 2491 |
+
829,
|
| 2492 |
+
4
|
| 2493 |
+
],
|
| 2494 |
+
[
|
| 2495 |
+
826,
|
| 2496 |
+
826,
|
| 2497 |
+
2
|
| 2498 |
+
],
|
| 2499 |
+
[
|
| 2500 |
+
982,
|
| 2501 |
+
982,
|
| 2502 |
+
1
|
| 2503 |
+
],
|
| 2504 |
+
[
|
| 2505 |
+
713,
|
| 2506 |
+
713,
|
| 2507 |
+
1
|
| 2508 |
+
],
|
| 2509 |
+
[
|
| 2510 |
+
869,
|
| 2511 |
+
869,
|
| 2512 |
+
1
|
| 2513 |
+
],
|
| 2514 |
+
[
|
| 2515 |
+
1029,
|
| 2516 |
+
817,
|
| 2517 |
+
2
|
| 2518 |
+
],
|
| 2519 |
+
[
|
| 2520 |
+
528,
|
| 2521 |
+
1031,
|
| 2522 |
+
1
|
| 2523 |
+
],
|
| 2524 |
+
[
|
| 2525 |
+
1042,
|
| 2526 |
+
529,
|
| 2527 |
+
2
|
| 2528 |
+
],
|
| 2529 |
+
[
|
| 2530 |
+
864,
|
| 2531 |
+
827,
|
| 2532 |
+
2
|
| 2533 |
+
],
|
| 2534 |
+
[
|
| 2535 |
+
819,
|
| 2536 |
+
819,
|
| 2537 |
+
2
|
| 2538 |
+
],
|
| 2539 |
+
[
|
| 2540 |
+
115,
|
| 2541 |
+
116,
|
| 2542 |
+
1
|
| 2543 |
+
],
|
| 2544 |
+
[
|
| 2545 |
+
127,
|
| 2546 |
+
126,
|
| 2547 |
+
1
|
| 2548 |
+
],
|
| 2549 |
+
[
|
| 2550 |
+
1079,
|
| 2551 |
+
42,
|
| 2552 |
+
2
|
| 2553 |
+
],
|
| 2554 |
+
[
|
| 2555 |
+
524,
|
| 2556 |
+
1050,
|
| 2557 |
+
1
|
| 2558 |
+
],
|
| 2559 |
+
[
|
| 2560 |
+
878,
|
| 2561 |
+
835,
|
| 2562 |
+
2
|
| 2563 |
+
],
|
| 2564 |
+
[
|
| 2565 |
+
45,
|
| 2566 |
+
46,
|
| 2567 |
+
1
|
| 2568 |
+
],
|
| 2569 |
+
[
|
| 2570 |
+
1082,
|
| 2571 |
+
38,
|
| 2572 |
+
2
|
| 2573 |
+
],
|
| 2574 |
+
[
|
| 2575 |
+
1080,
|
| 2576 |
+
118,
|
| 2577 |
+
2
|
| 2578 |
+
],
|
| 2579 |
+
[
|
| 2580 |
+
843,
|
| 2581 |
+
843,
|
| 2582 |
+
3
|
| 2583 |
+
],
|
| 2584 |
+
[
|
| 2585 |
+
871,
|
| 2586 |
+
871,
|
| 2587 |
+
2
|
| 2588 |
+
],
|
| 2589 |
+
[
|
| 2590 |
+
533,
|
| 2591 |
+
532,
|
| 2592 |
+
1
|
| 2593 |
+
],
|
| 2594 |
+
[
|
| 2595 |
+
839,
|
| 2596 |
+
839,
|
| 2597 |
+
2
|
| 2598 |
+
],
|
| 2599 |
+
[
|
| 2600 |
+
958,
|
| 2601 |
+
958,
|
| 2602 |
+
2
|
| 2603 |
+
],
|
| 2604 |
+
[
|
| 2605 |
+
522,
|
| 2606 |
+
521,
|
| 2607 |
+
1
|
| 2608 |
+
],
|
| 2609 |
+
[
|
| 2610 |
+
872,
|
| 2611 |
+
872,
|
| 2612 |
+
2
|
| 2613 |
+
],
|
| 2614 |
+
[
|
| 2615 |
+
1084,
|
| 2616 |
+
36,
|
| 2617 |
+
2
|
| 2618 |
+
],
|
| 2619 |
+
[
|
| 2620 |
+
838,
|
| 2621 |
+
851,
|
| 2622 |
+
2
|
| 2623 |
+
],
|
| 2624 |
+
[
|
| 2625 |
+
197,
|
| 2626 |
+
196,
|
| 2627 |
+
1
|
| 2628 |
+
],
|
| 2629 |
+
[
|
| 2630 |
+
1088,
|
| 2631 |
+
124,
|
| 2632 |
+
2
|
| 2633 |
+
],
|
| 2634 |
+
[
|
| 2635 |
+
1075,
|
| 2636 |
+
205,
|
| 2637 |
+
2
|
| 2638 |
+
],
|
| 2639 |
+
[
|
| 2640 |
+
971,
|
| 2641 |
+
971,
|
| 2642 |
+
1
|
| 2643 |
+
],
|
| 2644 |
+
[
|
| 2645 |
+
1093,
|
| 2646 |
+
120,
|
| 2647 |
+
2
|
| 2648 |
+
],
|
| 2649 |
+
[
|
| 2650 |
+
1035,
|
| 2651 |
+
832,
|
| 2652 |
+
4
|
| 2653 |
+
],
|
| 2654 |
+
[
|
| 2655 |
+
195,
|
| 2656 |
+
195,
|
| 2657 |
+
1
|
| 2658 |
+
],
|
| 2659 |
+
[
|
| 2660 |
+
973,
|
| 2661 |
+
973,
|
| 2662 |
+
1
|
| 2663 |
+
],
|
| 2664 |
+
[
|
| 2665 |
+
856,
|
| 2666 |
+
1043,
|
| 2667 |
+
1
|
| 2668 |
+
],
|
| 2669 |
+
[
|
| 2670 |
+
965,
|
| 2671 |
+
965,
|
| 2672 |
+
2
|
| 2673 |
+
],
|
| 2674 |
+
[
|
| 2675 |
+
915,
|
| 2676 |
+
909,
|
| 2677 |
+
1
|
| 2678 |
+
],
|
| 2679 |
+
[
|
| 2680 |
+
829,
|
| 2681 |
+
829,
|
| 2682 |
+
4
|
| 2683 |
+
],
|
| 2684 |
+
[
|
| 2685 |
+
533,
|
| 2686 |
+
534,
|
| 2687 |
+
1
|
| 2688 |
+
],
|
| 2689 |
+
[
|
| 2690 |
+
858,
|
| 2691 |
+
827,
|
| 2692 |
+
2
|
| 2693 |
+
],
|
| 2694 |
+
[
|
| 2695 |
+
1030,
|
| 2696 |
+
1030,
|
| 2697 |
+
4
|
| 2698 |
+
],
|
| 2699 |
+
[
|
| 2700 |
+
967,
|
| 2701 |
+
967,
|
| 2702 |
+
2
|
| 2703 |
+
],
|
| 2704 |
+
[
|
| 2705 |
+
1034,
|
| 2706 |
+
527,
|
| 2707 |
+
2
|
| 2708 |
+
],
|
| 2709 |
+
[
|
| 2710 |
+
930,
|
| 2711 |
+
903,
|
| 2712 |
+
1
|
| 2713 |
+
],
|
| 2714 |
+
[
|
| 2715 |
+
838,
|
| 2716 |
+
855,
|
| 2717 |
+
2
|
| 2718 |
+
],
|
| 2719 |
+
[
|
| 2720 |
+
854,
|
| 2721 |
+
854,
|
| 2722 |
+
3
|
| 2723 |
+
],
|
| 2724 |
+
[
|
| 2725 |
+
932,
|
| 2726 |
+
900,
|
| 2727 |
+
1
|
| 2728 |
+
],
|
| 2729 |
+
[
|
| 2730 |
+
1099,
|
| 2731 |
+
122,
|
| 2732 |
+
2
|
| 2733 |
+
],
|
| 2734 |
+
[
|
| 2735 |
+
972,
|
| 2736 |
+
972,
|
| 2737 |
+
2
|
| 2738 |
+
],
|
| 2739 |
+
[
|
| 2740 |
+
1089,
|
| 2741 |
+
201,
|
| 2742 |
+
2
|
| 2743 |
+
],
|
| 2744 |
+
[
|
| 2745 |
+
1038,
|
| 2746 |
+
530,
|
| 2747 |
+
2
|
| 2748 |
+
],
|
| 2749 |
+
[
|
| 2750 |
+
969,
|
| 2751 |
+
969,
|
| 2752 |
+
2
|
| 2753 |
+
],
|
| 2754 |
+
[
|
| 2755 |
+
1085,
|
| 2756 |
+
41,
|
| 2757 |
+
1
|
| 2758 |
+
],
|
| 2759 |
+
[
|
| 2760 |
+
845,
|
| 2761 |
+
845,
|
| 2762 |
+
3
|
| 2763 |
+
],
|
| 2764 |
+
[
|
| 2765 |
+
925,
|
| 2766 |
+
925,
|
| 2767 |
+
1
|
| 2768 |
+
],
|
| 2769 |
+
[
|
| 2770 |
+
974,
|
| 2771 |
+
974,
|
| 2772 |
+
2
|
| 2773 |
+
],
|
| 2774 |
+
[
|
| 2775 |
+
837,
|
| 2776 |
+
857,
|
| 2777 |
+
1
|
| 2778 |
+
],
|
| 2779 |
+
[
|
| 2780 |
+
912,
|
| 2781 |
+
912,
|
| 2782 |
+
2
|
| 2783 |
+
],
|
| 2784 |
+
[
|
| 2785 |
+
901,
|
| 2786 |
+
901,
|
| 2787 |
+
2
|
| 2788 |
+
],
|
| 2789 |
+
[
|
| 2790 |
+
853,
|
| 2791 |
+
853,
|
| 2792 |
+
1
|
| 2793 |
+
],
|
| 2794 |
+
[
|
| 2795 |
+
928,
|
| 2796 |
+
928,
|
| 2797 |
+
1
|
| 2798 |
+
],
|
| 2799 |
+
[
|
| 2800 |
+
841,
|
| 2801 |
+
826,
|
| 2802 |
+
1
|
| 2803 |
+
],
|
| 2804 |
+
[
|
| 2805 |
+
206,
|
| 2806 |
+
1109,
|
| 2807 |
+
1
|
| 2808 |
+
],
|
| 2809 |
+
[
|
| 2810 |
+
1100,
|
| 2811 |
+
44,
|
| 2812 |
+
2
|
| 2813 |
+
],
|
| 2814 |
+
[
|
| 2815 |
+
117,
|
| 2816 |
+
1126,
|
| 2817 |
+
1
|
| 2818 |
+
],
|
| 2819 |
+
[
|
| 2820 |
+
840,
|
| 2821 |
+
825,
|
| 2822 |
+
1
|
| 2823 |
+
],
|
| 2824 |
+
[
|
| 2825 |
+
856,
|
| 2826 |
+
1122,
|
| 2827 |
+
1
|
| 2828 |
+
],
|
| 2829 |
+
[
|
| 2830 |
+
840,
|
| 2831 |
+
848,
|
| 2832 |
+
1
|
| 2833 |
+
],
|
| 2834 |
+
[
|
| 2835 |
+
1085,
|
| 2836 |
+
39,
|
| 2837 |
+
1
|
| 2838 |
+
],
|
| 2839 |
+
[
|
| 2840 |
+
1057,
|
| 2841 |
+
1057,
|
| 2842 |
+
2
|
| 2843 |
+
],
|
| 2844 |
+
[
|
| 2845 |
+
125,
|
| 2846 |
+
1128,
|
| 2847 |
+
1
|
| 2848 |
+
],
|
| 2849 |
+
[
|
| 2850 |
+
537,
|
| 2851 |
+
537,
|
| 2852 |
+
1
|
| 2853 |
+
],
|
| 2854 |
+
[
|
| 2855 |
+
868,
|
| 2856 |
+
834,
|
| 2857 |
+
1
|
| 2858 |
+
],
|
| 2859 |
+
[
|
| 2860 |
+
1045,
|
| 2861 |
+
847,
|
| 2862 |
+
4
|
| 2863 |
+
],
|
| 2864 |
+
[
|
| 2865 |
+
1058,
|
| 2866 |
+
1058,
|
| 2867 |
+
2
|
| 2868 |
+
],
|
| 2869 |
+
[
|
| 2870 |
+
931,
|
| 2871 |
+
931,
|
| 2872 |
+
1
|
| 2873 |
+
],
|
| 2874 |
+
[
|
| 2875 |
+
983,
|
| 2876 |
+
983,
|
| 2877 |
+
2
|
| 2878 |
+
],
|
| 2879 |
+
[
|
| 2880 |
+
532,
|
| 2881 |
+
1039,
|
| 2882 |
+
1
|
| 2883 |
+
],
|
| 2884 |
+
[
|
| 2885 |
+
841,
|
| 2886 |
+
849,
|
| 2887 |
+
1
|
| 2888 |
+
],
|
| 2889 |
+
[
|
| 2890 |
+
1034,
|
| 2891 |
+
525,
|
| 2892 |
+
2
|
| 2893 |
+
],
|
| 2894 |
+
[
|
| 2895 |
+
197,
|
| 2896 |
+
1051,
|
| 2897 |
+
1
|
| 2898 |
+
],
|
| 2899 |
+
[
|
| 2900 |
+
530,
|
| 2901 |
+
530,
|
| 2902 |
+
2
|
| 2903 |
+
],
|
| 2904 |
+
[
|
| 2905 |
+
833,
|
| 2906 |
+
833,
|
| 2907 |
+
5
|
| 2908 |
+
],
|
| 2909 |
+
[
|
| 2910 |
+
1024,
|
| 2911 |
+
824,
|
| 2912 |
+
3
|
| 2913 |
+
],
|
| 2914 |
+
[
|
| 2915 |
+
934,
|
| 2916 |
+
934,
|
| 2917 |
+
1
|
| 2918 |
+
],
|
| 2919 |
+
[
|
| 2920 |
+
979,
|
| 2921 |
+
979,
|
| 2922 |
+
2
|
| 2923 |
+
],
|
| 2924 |
+
[
|
| 2925 |
+
1025,
|
| 2926 |
+
1062,
|
| 2927 |
+
4
|
| 2928 |
+
],
|
| 2929 |
+
[
|
| 2930 |
+
939,
|
| 2931 |
+
939,
|
| 2932 |
+
1
|
| 2933 |
+
],
|
| 2934 |
+
[
|
| 2935 |
+
329,
|
| 2936 |
+
329,
|
| 2937 |
+
1
|
| 2938 |
+
],
|
| 2939 |
+
[
|
| 2940 |
+
836,
|
| 2941 |
+
836,
|
| 2942 |
+
5
|
| 2943 |
+
],
|
| 2944 |
+
[
|
| 2945 |
+
1106,
|
| 2946 |
+
203,
|
| 2947 |
+
2
|
| 2948 |
+
],
|
| 2949 |
+
[
|
| 2950 |
+
941,
|
| 2951 |
+
941,
|
| 2952 |
+
1
|
| 2953 |
+
],
|
| 2954 |
+
[
|
| 2955 |
+
987,
|
| 2956 |
+
987,
|
| 2957 |
+
2
|
| 2958 |
+
],
|
| 2959 |
+
[
|
| 2960 |
+
890,
|
| 2961 |
+
890,
|
| 2962 |
+
2
|
| 2963 |
+
],
|
| 2964 |
+
[
|
| 2965 |
+
889,
|
| 2966 |
+
879,
|
| 2967 |
+
1
|
| 2968 |
+
],
|
| 2969 |
+
[
|
| 2970 |
+
876,
|
| 2971 |
+
876,
|
| 2972 |
+
1
|
| 2973 |
+
],
|
| 2974 |
+
[
|
| 2975 |
+
1040,
|
| 2976 |
+
862,
|
| 2977 |
+
2
|
| 2978 |
+
],
|
| 2979 |
+
[
|
| 2980 |
+
728,
|
| 2981 |
+
727,
|
| 2982 |
+
1
|
| 2983 |
+
],
|
| 2984 |
+
[
|
| 2985 |
+
978,
|
| 2986 |
+
978,
|
| 2987 |
+
2
|
| 2988 |
+
],
|
| 2989 |
+
[
|
| 2990 |
+
32,
|
| 2991 |
+
32,
|
| 2992 |
+
1
|
| 2993 |
+
],
|
| 2994 |
+
[
|
| 2995 |
+
1036,
|
| 2996 |
+
528,
|
| 2997 |
+
2
|
| 2998 |
+
],
|
| 2999 |
+
[
|
| 3000 |
+
818,
|
| 3001 |
+
818,
|
| 3002 |
+
3
|
| 3003 |
+
],
|
| 3004 |
+
[
|
| 3005 |
+
208,
|
| 3006 |
+
209,
|
| 3007 |
+
1
|
| 3008 |
+
],
|
| 3009 |
+
[
|
| 3010 |
+
1090,
|
| 3011 |
+
1090,
|
| 3012 |
+
2
|
| 3013 |
+
],
|
| 3014 |
+
[
|
| 3015 |
+
922,
|
| 3016 |
+
922,
|
| 3017 |
+
1
|
| 3018 |
+
],
|
| 3019 |
+
[
|
| 3020 |
+
921,
|
| 3021 |
+
921,
|
| 3022 |
+
1
|
| 3023 |
+
],
|
| 3024 |
+
[
|
| 3025 |
+
850,
|
| 3026 |
+
850,
|
| 3027 |
+
2
|
| 3028 |
+
],
|
| 3029 |
+
[
|
| 3030 |
+
1041,
|
| 3031 |
+
863,
|
| 3032 |
+
2
|
| 3033 |
+
],
|
| 3034 |
+
[
|
| 3035 |
+
946,
|
| 3036 |
+
946,
|
| 3037 |
+
1
|
| 3038 |
+
],
|
| 3039 |
+
[
|
| 3040 |
+
999,
|
| 3041 |
+
999,
|
| 3042 |
+
1
|
| 3043 |
+
],
|
| 3044 |
+
[
|
| 3045 |
+
1036,
|
| 3046 |
+
527,
|
| 3047 |
+
2
|
| 3048 |
+
],
|
| 3049 |
+
[
|
| 3050 |
+
917,
|
| 3051 |
+
917,
|
| 3052 |
+
2
|
| 3053 |
+
],
|
| 3054 |
+
[
|
| 3055 |
+
39,
|
| 3056 |
+
40,
|
| 3057 |
+
1
|
| 3058 |
+
],
|
| 3059 |
+
[
|
| 3060 |
+
41,
|
| 3061 |
+
40,
|
| 3062 |
+
1
|
| 3063 |
+
],
|
| 3064 |
+
[
|
| 3065 |
+
520,
|
| 3066 |
+
521,
|
| 3067 |
+
1
|
| 3068 |
+
],
|
| 3069 |
+
[
|
| 3070 |
+
851,
|
| 3071 |
+
851,
|
| 3072 |
+
3
|
| 3073 |
+
],
|
| 3074 |
+
[
|
| 3075 |
+
725,
|
| 3076 |
+
724,
|
| 3077 |
+
1
|
| 3078 |
+
],
|
| 3079 |
+
[
|
| 3080 |
+
1034,
|
| 3081 |
+
526,
|
| 3082 |
+
2
|
| 3083 |
+
],
|
| 3084 |
+
[
|
| 3085 |
+
713,
|
| 3086 |
+
714,
|
| 3087 |
+
1
|
| 3088 |
+
],
|
| 3089 |
+
[
|
| 3090 |
+
210,
|
| 3091 |
+
210,
|
| 3092 |
+
1
|
| 3093 |
+
],
|
| 3094 |
+
[
|
| 3095 |
+
1104,
|
| 3096 |
+
718,
|
| 3097 |
+
2
|
| 3098 |
+
],
|
| 3099 |
+
[
|
| 3100 |
+
847,
|
| 3101 |
+
887,
|
| 3102 |
+
2
|
| 3103 |
+
],
|
| 3104 |
+
[
|
| 3105 |
+
1048,
|
| 3106 |
+
834,
|
| 3107 |
+
2
|
| 3108 |
+
],
|
| 3109 |
+
[
|
| 3110 |
+
868,
|
| 3111 |
+
837,
|
| 3112 |
+
1
|
| 3113 |
+
],
|
| 3114 |
+
[
|
| 3115 |
+
1040,
|
| 3116 |
+
848,
|
| 3117 |
+
2
|
| 3118 |
+
],
|
| 3119 |
+
[
|
| 3120 |
+
857,
|
| 3121 |
+
1127,
|
| 3122 |
+
1
|
| 3123 |
+
],
|
| 3124 |
+
[
|
| 3125 |
+
858,
|
| 3126 |
+
873,
|
| 3127 |
+
2
|
| 3128 |
+
],
|
| 3129 |
+
[
|
| 3130 |
+
200,
|
| 3131 |
+
1103,
|
| 3132 |
+
1
|
| 3133 |
+
],
|
| 3134 |
+
[
|
| 3135 |
+
895,
|
| 3136 |
+
895,
|
| 3137 |
+
2
|
| 3138 |
+
],
|
| 3139 |
+
[
|
| 3140 |
+
953,
|
| 3141 |
+
953,
|
| 3142 |
+
1
|
| 3143 |
+
],
|
| 3144 |
+
[
|
| 3145 |
+
875,
|
| 3146 |
+
1077,
|
| 3147 |
+
1
|
| 3148 |
+
],
|
| 3149 |
+
[
|
| 3150 |
+
957,
|
| 3151 |
+
957,
|
| 3152 |
+
1
|
| 3153 |
+
],
|
| 3154 |
+
[
|
| 3155 |
+
1041,
|
| 3156 |
+
849,
|
| 3157 |
+
2
|
| 3158 |
+
],
|
| 3159 |
+
[
|
| 3160 |
+
855,
|
| 3161 |
+
838,
|
| 3162 |
+
2
|
| 3163 |
+
],
|
| 3164 |
+
[
|
| 3165 |
+
530,
|
| 3166 |
+
1039,
|
| 3167 |
+
1
|
| 3168 |
+
],
|
| 3169 |
+
[
|
| 3170 |
+
853,
|
| 3171 |
+
831,
|
| 3172 |
+
2
|
| 3173 |
+
],
|
| 3174 |
+
[
|
| 3175 |
+
524,
|
| 3176 |
+
526,
|
| 3177 |
+
1
|
| 3178 |
+
],
|
| 3179 |
+
[
|
| 3180 |
+
830,
|
| 3181 |
+
830,
|
| 3182 |
+
3
|
| 3183 |
+
],
|
| 3184 |
+
[
|
| 3185 |
+
956,
|
| 3186 |
+
956,
|
| 3187 |
+
1
|
| 3188 |
+
],
|
| 3189 |
+
[
|
| 3190 |
+
842,
|
| 3191 |
+
842,
|
| 3192 |
+
2
|
| 3193 |
+
],
|
| 3194 |
+
[
|
| 3195 |
+
48,
|
| 3196 |
+
48,
|
| 3197 |
+
1
|
| 3198 |
+
],
|
| 3199 |
+
[
|
| 3200 |
+
877,
|
| 3201 |
+
1081,
|
| 3202 |
+
1
|
| 3203 |
+
],
|
| 3204 |
+
[
|
| 3205 |
+
888,
|
| 3206 |
+
888,
|
| 3207 |
+
2
|
| 3208 |
+
],
|
| 3209 |
+
[
|
| 3210 |
+
952,
|
| 3211 |
+
952,
|
| 3212 |
+
1
|
| 3213 |
+
],
|
| 3214 |
+
[
|
| 3215 |
+
827,
|
| 3216 |
+
827,
|
| 3217 |
+
3
|
| 3218 |
+
],
|
| 3219 |
+
[
|
| 3220 |
+
533,
|
| 3221 |
+
535,
|
| 3222 |
+
1
|
| 3223 |
+
],
|
| 3224 |
+
[
|
| 3225 |
+
1038,
|
| 3226 |
+
529,
|
| 3227 |
+
2
|
| 3228 |
+
],
|
| 3229 |
+
[
|
| 3230 |
+
846,
|
| 3231 |
+
829,
|
| 3232 |
+
2
|
| 3233 |
+
],
|
| 3234 |
+
[
|
| 3235 |
+
885,
|
| 3236 |
+
885,
|
| 3237 |
+
1
|
| 3238 |
+
],
|
| 3239 |
+
[
|
| 3240 |
+
1073,
|
| 3241 |
+
861,
|
| 3242 |
+
4
|
| 3243 |
+
],
|
| 3244 |
+
[
|
| 3245 |
+
886,
|
| 3246 |
+
886,
|
| 3247 |
+
2
|
| 3248 |
+
],
|
| 3249 |
+
[
|
| 3250 |
+
847,
|
| 3251 |
+
897,
|
| 3252 |
+
2
|
| 3253 |
+
],
|
| 3254 |
+
[
|
| 3255 |
+
1071,
|
| 3256 |
+
1071,
|
| 3257 |
+
2
|
| 3258 |
+
],
|
| 3259 |
+
[
|
| 3260 |
+
716,
|
| 3261 |
+
717,
|
| 3262 |
+
1
|
| 3263 |
+
],
|
| 3264 |
+
[
|
| 3265 |
+
1107,
|
| 3266 |
+
723,
|
| 3267 |
+
2
|
| 3268 |
+
],
|
| 3269 |
+
[
|
| 3270 |
+
896,
|
| 3271 |
+
896,
|
| 3272 |
+
2
|
| 3273 |
+
],
|
| 3274 |
+
[
|
| 3275 |
+
882,
|
| 3276 |
+
882,
|
| 3277 |
+
1
|
| 3278 |
+
],
|
| 3279 |
+
[
|
| 3280 |
+
538,
|
| 3281 |
+
538,
|
| 3282 |
+
1
|
| 3283 |
+
],
|
| 3284 |
+
[
|
| 3285 |
+
852,
|
| 3286 |
+
831,
|
| 3287 |
+
2
|
| 3288 |
+
],
|
| 3289 |
+
[
|
| 3290 |
+
532,
|
| 3291 |
+
1046,
|
| 3292 |
+
1
|
| 3293 |
+
],
|
| 3294 |
+
[
|
| 3295 |
+
891,
|
| 3296 |
+
891,
|
| 3297 |
+
2
|
| 3298 |
+
],
|
| 3299 |
+
[
|
| 3300 |
+
1026,
|
| 3301 |
+
828,
|
| 3302 |
+
3
|
| 3303 |
+
],
|
| 3304 |
+
[
|
| 3305 |
+
916,
|
| 3306 |
+
980,
|
| 3307 |
+
1
|
| 3308 |
+
],
|
| 3309 |
+
[
|
| 3310 |
+
857,
|
| 3311 |
+
1047,
|
| 3312 |
+
1
|
| 3313 |
+
],
|
| 3314 |
+
[
|
| 3315 |
+
33,
|
| 3316 |
+
34,
|
| 3317 |
+
1
|
| 3318 |
+
],
|
| 3319 |
+
[
|
| 3320 |
+
909,
|
| 3321 |
+
915,
|
| 3322 |
+
1
|
| 3323 |
+
],
|
| 3324 |
+
[
|
| 3325 |
+
862,
|
| 3326 |
+
848,
|
| 3327 |
+
1
|
| 3328 |
+
],
|
| 3329 |
+
[
|
| 3330 |
+
1072,
|
| 3331 |
+
1072,
|
| 3332 |
+
2
|
| 3333 |
+
],
|
| 3334 |
+
[
|
| 3335 |
+
883,
|
| 3336 |
+
883,
|
| 3337 |
+
1
|
| 3338 |
+
],
|
| 3339 |
+
[
|
| 3340 |
+
918,
|
| 3341 |
+
982,
|
| 3342 |
+
1
|
| 3343 |
+
],
|
| 3344 |
+
[
|
| 3345 |
+
1044,
|
| 3346 |
+
531,
|
| 3347 |
+
2
|
| 3348 |
+
],
|
| 3349 |
+
[
|
| 3350 |
+
1052,
|
| 3351 |
+
1052,
|
| 3352 |
+
2
|
| 3353 |
+
],
|
| 3354 |
+
[
|
| 3355 |
+
908,
|
| 3356 |
+
835,
|
| 3357 |
+
2
|
| 3358 |
+
],
|
| 3359 |
+
[
|
| 3360 |
+
913,
|
| 3361 |
+
198,
|
| 3362 |
+
1
|
| 3363 |
+
],
|
| 3364 |
+
[
|
| 3365 |
+
713,
|
| 3366 |
+
712,
|
| 3367 |
+
1
|
| 3368 |
+
],
|
| 3369 |
+
[
|
| 3370 |
+
1000,
|
| 3371 |
+
1000,
|
| 3372 |
+
1
|
| 3373 |
+
],
|
| 3374 |
+
[
|
| 3375 |
+
863,
|
| 3376 |
+
849,
|
| 3377 |
+
1
|
| 3378 |
+
],
|
| 3379 |
+
[
|
| 3380 |
+
846,
|
| 3381 |
+
832,
|
| 3382 |
+
2
|
| 3383 |
+
],
|
| 3384 |
+
[
|
| 3385 |
+
930,
|
| 3386 |
+
971,
|
| 3387 |
+
1
|
| 3388 |
+
],
|
| 3389 |
+
[
|
| 3390 |
+
1085,
|
| 3391 |
+
40,
|
| 3392 |
+
1
|
| 3393 |
+
],
|
| 3394 |
+
[
|
| 3395 |
+
836,
|
| 3396 |
+
838,
|
| 3397 |
+
1
|
| 3398 |
+
],
|
| 3399 |
+
[
|
| 3400 |
+
37,
|
| 3401 |
+
1084,
|
| 3402 |
+
1
|
| 3403 |
+
],
|
| 3404 |
+
[
|
| 3405 |
+
1048,
|
| 3406 |
+
880,
|
| 3407 |
+
2
|
| 3408 |
+
],
|
| 3409 |
+
[
|
| 3410 |
+
868,
|
| 3411 |
+
868,
|
| 3412 |
+
1
|
| 3413 |
+
],
|
| 3414 |
+
[
|
| 3415 |
+
932,
|
| 3416 |
+
973,
|
| 3417 |
+
1
|
| 3418 |
+
],
|
| 3419 |
+
[
|
| 3420 |
+
530,
|
| 3421 |
+
1042,
|
| 3422 |
+
1
|
| 3423 |
+
],
|
| 3424 |
+
[
|
| 3425 |
+
534,
|
| 3426 |
+
1046,
|
| 3427 |
+
1
|
| 3428 |
+
],
|
| 3429 |
+
[
|
| 3430 |
+
1036,
|
| 3431 |
+
529,
|
| 3432 |
+
2
|
| 3433 |
+
],
|
| 3434 |
+
[
|
| 3435 |
+
892,
|
| 3436 |
+
892,
|
| 3437 |
+
2
|
| 3438 |
+
],
|
| 3439 |
+
[
|
| 3440 |
+
878,
|
| 3441 |
+
897,
|
| 3442 |
+
2
|
| 3443 |
+
],
|
| 3444 |
+
[
|
| 3445 |
+
858,
|
| 3446 |
+
874,
|
| 3447 |
+
2
|
| 3448 |
+
],
|
| 3449 |
+
[
|
| 3450 |
+
818,
|
| 3451 |
+
835,
|
| 3452 |
+
2
|
| 3453 |
+
],
|
| 3454 |
+
[
|
| 3455 |
+
129,
|
| 3456 |
+
129,
|
| 3457 |
+
1
|
| 3458 |
+
],
|
| 3459 |
+
[
|
| 3460 |
+
894,
|
| 3461 |
+
894,
|
| 3462 |
+
1
|
| 3463 |
+
],
|
| 3464 |
+
[
|
| 3465 |
+
1076,
|
| 3466 |
+
1076,
|
| 3467 |
+
2
|
| 3468 |
+
],
|
| 3469 |
+
[
|
| 3470 |
+
893,
|
| 3471 |
+
893,
|
| 3472 |
+
2
|
| 3473 |
+
],
|
| 3474 |
+
[
|
| 3475 |
+
852,
|
| 3476 |
+
832,
|
| 3477 |
+
2
|
| 3478 |
+
],
|
| 3479 |
+
[
|
| 3480 |
+
113,
|
| 3481 |
+
113,
|
| 3482 |
+
1
|
| 3483 |
+
],
|
| 3484 |
+
[
|
| 3485 |
+
727,
|
| 3486 |
+
727,
|
| 3487 |
+
1
|
| 3488 |
+
],
|
| 3489 |
+
[
|
| 3490 |
+
966,
|
| 3491 |
+
966,
|
| 3492 |
+
1
|
| 3493 |
+
],
|
| 3494 |
+
[
|
| 3495 |
+
1013,
|
| 3496 |
+
1013,
|
| 3497 |
+
1
|
| 3498 |
+
],
|
| 3499 |
+
[
|
| 3500 |
+
838,
|
| 3501 |
+
836,
|
| 3502 |
+
1
|
| 3503 |
+
],
|
| 3504 |
+
[
|
| 3505 |
+
843,
|
| 3506 |
+
833,
|
| 3507 |
+
2
|
| 3508 |
+
],
|
| 3509 |
+
[
|
| 3510 |
+
833,
|
| 3511 |
+
843,
|
| 3512 |
+
1
|
| 3513 |
+
],
|
| 3514 |
+
[
|
| 3515 |
+
825,
|
| 3516 |
+
825,
|
| 3517 |
+
3
|
| 3518 |
+
],
|
| 3519 |
+
[
|
| 3520 |
+
1021,
|
| 3521 |
+
1021,
|
| 3522 |
+
1
|
| 3523 |
+
],
|
| 3524 |
+
[
|
| 3525 |
+
1038,
|
| 3526 |
+
531,
|
| 3527 |
+
2
|
| 3528 |
+
],
|
| 3529 |
+
[
|
| 3530 |
+
332,
|
| 3531 |
+
332,
|
| 3532 |
+
1
|
| 3533 |
+
],
|
| 3534 |
+
[
|
| 3535 |
+
854,
|
| 3536 |
+
854,
|
| 3537 |
+
4
|
| 3538 |
+
],
|
| 3539 |
+
[
|
| 3540 |
+
47,
|
| 3541 |
+
46,
|
| 3542 |
+
1
|
| 3543 |
+
],
|
| 3544 |
+
[
|
| 3545 |
+
527,
|
| 3546 |
+
525,
|
| 3547 |
+
1
|
| 3548 |
+
],
|
| 3549 |
+
[
|
| 3550 |
+
202,
|
| 3551 |
+
1089,
|
| 3552 |
+
1
|
| 3553 |
+
],
|
| 3554 |
+
[
|
| 3555 |
+
1035,
|
| 3556 |
+
1035,
|
| 3557 |
+
4
|
| 3558 |
+
],
|
| 3559 |
+
[
|
| 3560 |
+
855,
|
| 3561 |
+
855,
|
| 3562 |
+
3
|
| 3563 |
+
],
|
| 3564 |
+
[
|
| 3565 |
+
344,
|
| 3566 |
+
344,
|
| 3567 |
+
1
|
| 3568 |
+
],
|
| 3569 |
+
[
|
| 3570 |
+
826,
|
| 3571 |
+
826,
|
| 3572 |
+
3
|
| 3573 |
+
],
|
| 3574 |
+
[
|
| 3575 |
+
43,
|
| 3576 |
+
1079,
|
| 3577 |
+
1
|
| 3578 |
+
],
|
| 3579 |
+
[
|
| 3580 |
+
898,
|
| 3581 |
+
898,
|
| 3582 |
+
1
|
| 3583 |
+
],
|
| 3584 |
+
[
|
| 3585 |
+
907,
|
| 3586 |
+
834,
|
| 3587 |
+
1
|
| 3588 |
+
],
|
| 3589 |
+
[
|
| 3590 |
+
914,
|
| 3591 |
+
914,
|
| 3592 |
+
2
|
| 3593 |
+
],
|
| 3594 |
+
[
|
| 3595 |
+
521,
|
| 3596 |
+
521,
|
| 3597 |
+
1
|
| 3598 |
+
],
|
| 3599 |
+
[
|
| 3600 |
+
926,
|
| 3601 |
+
926,
|
| 3602 |
+
2
|
| 3603 |
+
],
|
| 3604 |
+
[
|
| 3605 |
+
528,
|
| 3606 |
+
1042,
|
| 3607 |
+
1
|
| 3608 |
+
],
|
| 3609 |
+
[
|
| 3610 |
+
1032,
|
| 3611 |
+
1032,
|
| 3612 |
+
4
|
| 3613 |
+
],
|
| 3614 |
+
[
|
| 3615 |
+
836,
|
| 3616 |
+
845,
|
| 3617 |
+
1
|
| 3618 |
+
],
|
| 3619 |
+
[
|
| 3620 |
+
920,
|
| 3621 |
+
920,
|
| 3622 |
+
1
|
| 3623 |
+
],
|
| 3624 |
+
[
|
| 3625 |
+
843,
|
| 3626 |
+
833,
|
| 3627 |
+
1
|
| 3628 |
+
],
|
| 3629 |
+
[
|
| 3630 |
+
837,
|
| 3631 |
+
868,
|
| 3632 |
+
1
|
| 3633 |
+
],
|
| 3634 |
+
[
|
| 3635 |
+
1040,
|
| 3636 |
+
840,
|
| 3637 |
+
2
|
| 3638 |
+
],
|
| 3639 |
+
[
|
| 3640 |
+
1091,
|
| 3641 |
+
1091,
|
| 3642 |
+
2
|
| 3643 |
+
],
|
| 3644 |
+
[
|
| 3645 |
+
960,
|
| 3646 |
+
960,
|
| 3647 |
+
1
|
| 3648 |
+
],
|
| 3649 |
+
[
|
| 3650 |
+
208,
|
| 3651 |
+
1109,
|
| 3652 |
+
1
|
| 3653 |
+
],
|
| 3654 |
+
[
|
| 3655 |
+
959,
|
| 3656 |
+
959,
|
| 3657 |
+
1
|
| 3658 |
+
],
|
| 3659 |
+
[
|
| 3660 |
+
199,
|
| 3661 |
+
200,
|
| 3662 |
+
1
|
| 3663 |
+
],
|
| 3664 |
+
[
|
| 3665 |
+
115,
|
| 3666 |
+
114,
|
| 3667 |
+
1
|
| 3668 |
+
],
|
| 3669 |
+
[
|
| 3670 |
+
716,
|
| 3671 |
+
715,
|
| 3672 |
+
1
|
| 3673 |
+
],
|
| 3674 |
+
[
|
| 3675 |
+
879,
|
| 3676 |
+
879,
|
| 3677 |
+
2
|
| 3678 |
+
],
|
| 3679 |
+
[
|
| 3680 |
+
899,
|
| 3681 |
+
899,
|
| 3682 |
+
1
|
| 3683 |
+
],
|
| 3684 |
+
[
|
| 3685 |
+
206,
|
| 3686 |
+
1075,
|
| 3687 |
+
1
|
| 3688 |
+
],
|
| 3689 |
+
[
|
| 3690 |
+
985,
|
| 3691 |
+
985,
|
| 3692 |
+
1
|
| 3693 |
+
],
|
| 3694 |
+
[
|
| 3695 |
+
861,
|
| 3696 |
+
829,
|
| 3697 |
+
2
|
| 3698 |
+
],
|
| 3699 |
+
[
|
| 3700 |
+
948,
|
| 3701 |
+
948,
|
| 3702 |
+
1
|
| 3703 |
+
],
|
| 3704 |
+
[
|
| 3705 |
+
718,
|
| 3706 |
+
717,
|
| 3707 |
+
1
|
| 3708 |
+
],
|
| 3709 |
+
[
|
| 3710 |
+
127,
|
| 3711 |
+
128,
|
| 3712 |
+
1
|
| 3713 |
+
],
|
| 3714 |
+
[
|
| 3715 |
+
875,
|
| 3716 |
+
848,
|
| 3717 |
+
1
|
| 3718 |
+
],
|
| 3719 |
+
[
|
| 3720 |
+
724,
|
| 3721 |
+
931,
|
| 3722 |
+
1
|
| 3723 |
+
],
|
| 3724 |
+
[
|
| 3725 |
+
1085,
|
| 3726 |
+
1085,
|
| 3727 |
+
1
|
| 3728 |
+
],
|
| 3729 |
+
[
|
| 3730 |
+
845,
|
| 3731 |
+
836,
|
| 3732 |
+
1
|
| 3733 |
+
],
|
| 3734 |
+
[
|
| 3735 |
+
1041,
|
| 3736 |
+
841,
|
| 3737 |
+
2
|
| 3738 |
+
],
|
| 3739 |
+
[
|
| 3740 |
+
845,
|
| 3741 |
+
836,
|
| 3742 |
+
2
|
| 3743 |
+
],
|
| 3744 |
+
[
|
| 3745 |
+
1037,
|
| 3746 |
+
1037,
|
| 3747 |
+
4
|
| 3748 |
+
],
|
| 3749 |
+
[
|
| 3750 |
+
929,
|
| 3751 |
+
929,
|
| 3752 |
+
1
|
| 3753 |
+
],
|
| 3754 |
+
[
|
| 3755 |
+
904,
|
| 3756 |
+
904,
|
| 3757 |
+
1
|
| 3758 |
+
],
|
| 3759 |
+
[
|
| 3760 |
+
877,
|
| 3761 |
+
849,
|
| 3762 |
+
1
|
| 3763 |
+
],
|
| 3764 |
+
[
|
| 3765 |
+
37,
|
| 3766 |
+
1082,
|
| 3767 |
+
1
|
| 3768 |
+
],
|
| 3769 |
+
[
|
| 3770 |
+
119,
|
| 3771 |
+
1080,
|
| 3772 |
+
1
|
| 3773 |
+
],
|
| 3774 |
+
[
|
| 3775 |
+
831,
|
| 3776 |
+
831,
|
| 3777 |
+
5
|
| 3778 |
+
],
|
| 3779 |
+
[
|
| 3780 |
+
905,
|
| 3781 |
+
905,
|
| 3782 |
+
1
|
| 3783 |
+
],
|
| 3784 |
+
[
|
| 3785 |
+
838,
|
| 3786 |
+
855,
|
| 3787 |
+
1
|
| 3788 |
+
],
|
| 3789 |
+
[
|
| 3790 |
+
910,
|
| 3791 |
+
910,
|
| 3792 |
+
1
|
| 3793 |
+
],
|
| 3794 |
+
[
|
| 3795 |
+
951,
|
| 3796 |
+
951,
|
| 3797 |
+
1
|
| 3798 |
+
],
|
| 3799 |
+
[
|
| 3800 |
+
1050,
|
| 3801 |
+
525,
|
| 3802 |
+
2
|
| 3803 |
+
],
|
| 3804 |
+
[
|
| 3805 |
+
827,
|
| 3806 |
+
864,
|
| 3807 |
+
2
|
| 3808 |
+
],
|
| 3809 |
+
[
|
| 3810 |
+
824,
|
| 3811 |
+
909,
|
| 3812 |
+
1
|
| 3813 |
+
],
|
| 3814 |
+
[
|
| 3815 |
+
837,
|
| 3816 |
+
823,
|
| 3817 |
+
1
|
| 3818 |
+
],
|
| 3819 |
+
[
|
| 3820 |
+
1133,
|
| 3821 |
+
721,
|
| 3822 |
+
2
|
| 3823 |
+
],
|
| 3824 |
+
[
|
| 3825 |
+
1097,
|
| 3826 |
+
1097,
|
| 3827 |
+
2
|
| 3828 |
+
],
|
| 3829 |
+
[
|
| 3830 |
+
329,
|
| 3831 |
+
328,
|
| 3832 |
+
1
|
| 3833 |
+
],
|
| 3834 |
+
[
|
| 3835 |
+
819,
|
| 3836 |
+
819,
|
| 3837 |
+
1
|
| 3838 |
+
],
|
| 3839 |
+
[
|
| 3840 |
+
204,
|
| 3841 |
+
1106,
|
| 3842 |
+
1
|
| 3843 |
+
],
|
| 3844 |
+
[
|
| 3845 |
+
723,
|
| 3846 |
+
724,
|
| 3847 |
+
1
|
| 3848 |
+
],
|
| 3849 |
+
[
|
| 3850 |
+
835,
|
| 3851 |
+
847,
|
| 3852 |
+
2
|
| 3853 |
+
],
|
| 3854 |
+
[
|
| 3855 |
+
861,
|
| 3856 |
+
887,
|
| 3857 |
+
2
|
| 3858 |
+
],
|
| 3859 |
+
[
|
| 3860 |
+
195,
|
| 3861 |
+
196,
|
| 3862 |
+
1
|
| 3863 |
+
],
|
| 3864 |
+
[
|
| 3865 |
+
838,
|
| 3866 |
+
851,
|
| 3867 |
+
1
|
| 3868 |
+
],
|
| 3869 |
+
[
|
| 3870 |
+
834,
|
| 3871 |
+
868,
|
| 3872 |
+
1
|
| 3873 |
+
],
|
| 3874 |
+
[
|
| 3875 |
+
348,
|
| 3876 |
+
348,
|
| 3877 |
+
1
|
| 3878 |
+
],
|
| 3879 |
+
[
|
| 3880 |
+
1113,
|
| 3881 |
+
1113,
|
| 3882 |
+
2
|
| 3883 |
+
],
|
| 3884 |
+
[
|
| 3885 |
+
911,
|
| 3886 |
+
911,
|
| 3887 |
+
2
|
| 3888 |
+
],
|
| 3889 |
+
[
|
| 3890 |
+
902,
|
| 3891 |
+
902,
|
| 3892 |
+
2
|
| 3893 |
+
],
|
| 3894 |
+
[
|
| 3895 |
+
1116,
|
| 3896 |
+
1116,
|
| 3897 |
+
2
|
| 3898 |
+
],
|
| 3899 |
+
[
|
| 3900 |
+
1115,
|
| 3901 |
+
1115,
|
| 3902 |
+
2
|
| 3903 |
+
],
|
| 3904 |
+
[
|
| 3905 |
+
117,
|
| 3906 |
+
1080,
|
| 3907 |
+
1
|
| 3908 |
+
],
|
| 3909 |
+
[
|
| 3910 |
+
534,
|
| 3911 |
+
532,
|
| 3912 |
+
1
|
| 3913 |
+
],
|
| 3914 |
+
[
|
| 3915 |
+
838,
|
| 3916 |
+
833,
|
| 3917 |
+
1
|
| 3918 |
+
],
|
| 3919 |
+
[
|
| 3920 |
+
529,
|
| 3921 |
+
529,
|
| 3922 |
+
2
|
| 3923 |
+
],
|
| 3924 |
+
[
|
| 3925 |
+
923,
|
| 3926 |
+
923,
|
| 3927 |
+
2
|
| 3928 |
+
],
|
| 3929 |
+
[
|
| 3930 |
+
35,
|
| 3931 |
+
1175,
|
| 3932 |
+
1
|
| 3933 |
+
],
|
| 3934 |
+
[
|
| 3935 |
+
1052,
|
| 3936 |
+
881,
|
| 3937 |
+
2
|
| 3938 |
+
],
|
| 3939 |
+
[
|
| 3940 |
+
1069,
|
| 3941 |
+
1069,
|
| 3942 |
+
2
|
| 3943 |
+
],
|
| 3944 |
+
[
|
| 3945 |
+
728,
|
| 3946 |
+
729,
|
| 3947 |
+
1
|
| 3948 |
+
],
|
| 3949 |
+
[
|
| 3950 |
+
872,
|
| 3951 |
+
928,
|
| 3952 |
+
1
|
| 3953 |
+
],
|
| 3954 |
+
[
|
| 3955 |
+
889,
|
| 3956 |
+
956,
|
| 3957 |
+
1
|
| 3958 |
+
],
|
| 3959 |
+
[
|
| 3960 |
+
1044,
|
| 3961 |
+
532,
|
| 3962 |
+
2
|
| 3963 |
+
],
|
| 3964 |
+
[
|
| 3965 |
+
818,
|
| 3966 |
+
829,
|
| 3967 |
+
2
|
| 3968 |
+
],
|
| 3969 |
+
[
|
| 3970 |
+
121,
|
| 3971 |
+
1093,
|
| 3972 |
+
1
|
| 3973 |
+
],
|
| 3974 |
+
[
|
| 3975 |
+
125,
|
| 3976 |
+
1088,
|
| 3977 |
+
1
|
| 3978 |
+
],
|
| 3979 |
+
[
|
| 3980 |
+
123,
|
| 3981 |
+
1088,
|
| 3982 |
+
1
|
| 3983 |
+
],
|
| 3984 |
+
[
|
| 3985 |
+
45,
|
| 3986 |
+
1100,
|
| 3987 |
+
1
|
| 3988 |
+
],
|
| 3989 |
+
[
|
| 3990 |
+
39,
|
| 3991 |
+
1082,
|
| 3992 |
+
1
|
| 3993 |
+
],
|
| 3994 |
+
[
|
| 3995 |
+
878,
|
| 3996 |
+
945,
|
| 3997 |
+
2
|
| 3998 |
+
],
|
| 3999 |
+
[
|
| 4000 |
+
824,
|
| 4001 |
+
924,
|
| 4002 |
+
1
|
| 4003 |
+
],
|
| 4004 |
+
[
|
| 4005 |
+
927,
|
| 4006 |
+
927,
|
| 4007 |
+
2
|
| 4008 |
+
],
|
| 4009 |
+
[
|
| 4010 |
+
906,
|
| 4011 |
+
906,
|
| 4012 |
+
2
|
| 4013 |
+
],
|
| 4014 |
+
[
|
| 4015 |
+
886,
|
| 4016 |
+
869,
|
| 4017 |
+
1
|
| 4018 |
+
],
|
| 4019 |
+
[
|
| 4020 |
+
1070,
|
| 4021 |
+
205,
|
| 4022 |
+
2
|
| 4023 |
+
],
|
| 4024 |
+
[
|
| 4025 |
+
719,
|
| 4026 |
+
720,
|
| 4027 |
+
1
|
| 4028 |
+
],
|
| 4029 |
+
[
|
| 4030 |
+
871,
|
| 4031 |
+
931,
|
| 4032 |
+
1
|
| 4033 |
+
],
|
| 4034 |
+
[
|
| 4035 |
+
43,
|
| 4036 |
+
1100,
|
| 4037 |
+
1
|
| 4038 |
+
],
|
| 4039 |
+
[
|
| 4040 |
+
818,
|
| 4041 |
+
831,
|
| 4042 |
+
2
|
| 4043 |
+
],
|
| 4044 |
+
[
|
| 4045 |
+
871,
|
| 4046 |
+
833,
|
| 4047 |
+
1
|
| 4048 |
+
],
|
| 4049 |
+
[
|
| 4050 |
+
1112,
|
| 4051 |
+
525,
|
| 4052 |
+
2
|
| 4053 |
+
],
|
| 4054 |
+
[
|
| 4055 |
+
1123,
|
| 4056 |
+
198,
|
| 4057 |
+
2
|
| 4058 |
+
],
|
| 4059 |
+
[
|
| 4060 |
+
851,
|
| 4061 |
+
838,
|
| 4062 |
+
2
|
| 4063 |
+
],
|
| 4064 |
+
[
|
| 4065 |
+
1048,
|
| 4066 |
+
1048,
|
| 4067 |
+
2
|
| 4068 |
+
],
|
| 4069 |
+
[
|
| 4070 |
+
31,
|
| 4071 |
+
31,
|
| 4072 |
+
1
|
| 4073 |
+
],
|
| 4074 |
+
[
|
| 4075 |
+
968,
|
| 4076 |
+
924,
|
| 4077 |
+
1
|
| 4078 |
+
],
|
| 4079 |
+
[
|
| 4080 |
+
121,
|
| 4081 |
+
1099,
|
| 4082 |
+
1
|
| 4083 |
+
],
|
| 4084 |
+
[
|
| 4085 |
+
843,
|
| 4086 |
+
864,
|
| 4087 |
+
2
|
| 4088 |
+
],
|
| 4089 |
+
[
|
| 4090 |
+
914,
|
| 4091 |
+
829,
|
| 4092 |
+
2
|
| 4093 |
+
],
|
| 4094 |
+
[
|
| 4095 |
+
1083,
|
| 4096 |
+
816,
|
| 4097 |
+
4
|
| 4098 |
+
],
|
| 4099 |
+
[
|
| 4100 |
+
1051,
|
| 4101 |
+
1051,
|
| 4102 |
+
2
|
| 4103 |
+
],
|
| 4104 |
+
[
|
| 4105 |
+
524,
|
| 4106 |
+
1163,
|
| 4107 |
+
1
|
| 4108 |
+
],
|
| 4109 |
+
[
|
| 4110 |
+
726,
|
| 4111 |
+
724,
|
| 4112 |
+
1
|
| 4113 |
+
],
|
| 4114 |
+
[
|
| 4115 |
+
1033,
|
| 4116 |
+
1026,
|
| 4117 |
+
4
|
| 4118 |
+
],
|
| 4119 |
+
[
|
| 4120 |
+
119,
|
| 4121 |
+
1093,
|
| 4122 |
+
1
|
| 4123 |
+
],
|
| 4124 |
+
[
|
| 4125 |
+
206,
|
| 4126 |
+
1000,
|
| 4127 |
+
1
|
| 4128 |
+
],
|
| 4129 |
+
[
|
| 4130 |
+
890,
|
| 4131 |
+
867,
|
| 4132 |
+
1
|
| 4133 |
+
],
|
| 4134 |
+
[
|
| 4135 |
+
711,
|
| 4136 |
+
712,
|
| 4137 |
+
1
|
| 4138 |
+
],
|
| 4139 |
+
[
|
| 4140 |
+
1063,
|
| 4141 |
+
853,
|
| 4142 |
+
4
|
| 4143 |
+
],
|
| 4144 |
+
[
|
| 4145 |
+
1064,
|
| 4146 |
+
825,
|
| 4147 |
+
2
|
| 4148 |
+
],
|
| 4149 |
+
[
|
| 4150 |
+
538,
|
| 4151 |
+
537,
|
| 4152 |
+
1
|
| 4153 |
+
],
|
| 4154 |
+
[
|
| 4155 |
+
827,
|
| 4156 |
+
833,
|
| 4157 |
+
1
|
| 4158 |
+
],
|
| 4159 |
+
[
|
| 4160 |
+
533,
|
| 4161 |
+
1096,
|
| 4162 |
+
1
|
| 4163 |
+
],
|
| 4164 |
+
[
|
| 4165 |
+
520,
|
| 4166 |
+
519,
|
| 4167 |
+
1
|
| 4168 |
+
],
|
| 4169 |
+
[
|
| 4170 |
+
941,
|
| 4171 |
+
198,
|
| 4172 |
+
1
|
| 4173 |
+
],
|
| 4174 |
+
[
|
| 4175 |
+
127,
|
| 4176 |
+
1128,
|
| 4177 |
+
1
|
| 4178 |
+
],
|
| 4179 |
+
[
|
| 4180 |
+
936,
|
| 4181 |
+
936,
|
| 4182 |
+
2
|
| 4183 |
+
],
|
| 4184 |
+
[
|
| 4185 |
+
820,
|
| 4186 |
+
825,
|
| 4187 |
+
1
|
| 4188 |
+
],
|
| 4189 |
+
[
|
| 4190 |
+
820,
|
| 4191 |
+
826,
|
| 4192 |
+
1
|
| 4193 |
+
],
|
| 4194 |
+
[
|
| 4195 |
+
823,
|
| 4196 |
+
981,
|
| 4197 |
+
1
|
| 4198 |
+
],
|
| 4199 |
+
[
|
| 4200 |
+
1034,
|
| 4201 |
+
1034,
|
| 4202 |
+
2
|
| 4203 |
+
],
|
| 4204 |
+
[
|
| 4205 |
+
1066,
|
| 4206 |
+
826,
|
| 4207 |
+
2
|
| 4208 |
+
],
|
| 4209 |
+
[
|
| 4210 |
+
835,
|
| 4211 |
+
853,
|
| 4212 |
+
2
|
| 4213 |
+
],
|
| 4214 |
+
[
|
| 4215 |
+
115,
|
| 4216 |
+
1126,
|
| 4217 |
+
1
|
| 4218 |
+
],
|
| 4219 |
+
[
|
| 4220 |
+
856,
|
| 4221 |
+
1017,
|
| 4222 |
+
1
|
| 4223 |
+
],
|
| 4224 |
+
[
|
| 4225 |
+
123,
|
| 4226 |
+
1099,
|
| 4227 |
+
1
|
| 4228 |
+
],
|
| 4229 |
+
[
|
| 4230 |
+
845,
|
| 4231 |
+
864,
|
| 4232 |
+
2
|
| 4233 |
+
],
|
| 4234 |
+
[
|
| 4235 |
+
194,
|
| 4236 |
+
194,
|
| 4237 |
+
1
|
| 4238 |
+
],
|
| 4239 |
+
[
|
| 4240 |
+
833,
|
| 4241 |
+
827,
|
| 4242 |
+
1
|
| 4243 |
+
],
|
| 4244 |
+
[
|
| 4245 |
+
919,
|
| 4246 |
+
919,
|
| 4247 |
+
1
|
| 4248 |
+
],
|
| 4249 |
+
[
|
| 4250 |
+
856,
|
| 4251 |
+
907,
|
| 4252 |
+
1
|
| 4253 |
+
],
|
| 4254 |
+
[
|
| 4255 |
+
869,
|
| 4256 |
+
886,
|
| 4257 |
+
1
|
| 4258 |
+
],
|
| 4259 |
+
[
|
| 4260 |
+
872,
|
| 4261 |
+
836,
|
| 4262 |
+
1
|
| 4263 |
+
],
|
| 4264 |
+
[
|
| 4265 |
+
870,
|
| 4266 |
+
870,
|
| 4267 |
+
3
|
| 4268 |
+
],
|
| 4269 |
+
[
|
| 4270 |
+
858,
|
| 4271 |
+
873,
|
| 4272 |
+
1
|
| 4273 |
+
],
|
| 4274 |
+
[
|
| 4275 |
+
831,
|
| 4276 |
+
832,
|
| 4277 |
+
1
|
| 4278 |
+
],
|
| 4279 |
+
[
|
| 4280 |
+
1061,
|
| 4281 |
+
852,
|
| 4282 |
+
4
|
| 4283 |
+
],
|
| 4284 |
+
[
|
| 4285 |
+
832,
|
| 4286 |
+
831,
|
| 4287 |
+
1
|
| 4288 |
+
],
|
| 4289 |
+
[
|
| 4290 |
+
1033,
|
| 4291 |
+
828,
|
| 4292 |
+
4
|
| 4293 |
+
],
|
| 4294 |
+
[
|
| 4295 |
+
534,
|
| 4296 |
+
1157,
|
| 4297 |
+
1
|
| 4298 |
+
],
|
| 4299 |
+
[
|
| 4300 |
+
989,
|
| 4301 |
+
989,
|
| 4302 |
+
1
|
| 4303 |
+
],
|
| 4304 |
+
[
|
| 4305 |
+
35,
|
| 4306 |
+
1084,
|
| 4307 |
+
1
|
| 4308 |
+
],
|
| 4309 |
+
[
|
| 4310 |
+
41,
|
| 4311 |
+
1079,
|
| 4312 |
+
1
|
| 4313 |
+
],
|
| 4314 |
+
[
|
| 4315 |
+
523,
|
| 4316 |
+
525,
|
| 4317 |
+
1
|
| 4318 |
+
],
|
| 4319 |
+
[
|
| 4320 |
+
990,
|
| 4321 |
+
990,
|
| 4322 |
+
1
|
| 4323 |
+
],
|
| 4324 |
+
[
|
| 4325 |
+
1045,
|
| 4326 |
+
1045,
|
| 4327 |
+
4
|
| 4328 |
+
],
|
| 4329 |
+
[
|
| 4330 |
+
895,
|
| 4331 |
+
867,
|
| 4332 |
+
1
|
| 4333 |
+
],
|
| 4334 |
+
[
|
| 4335 |
+
884,
|
| 4336 |
+
884,
|
| 4337 |
+
2
|
| 4338 |
+
],
|
| 4339 |
+
[
|
| 4340 |
+
1036,
|
| 4341 |
+
1036,
|
| 4342 |
+
2
|
| 4343 |
+
],
|
| 4344 |
+
[
|
| 4345 |
+
204,
|
| 4346 |
+
1075,
|
| 4347 |
+
1
|
| 4348 |
+
],
|
| 4349 |
+
[
|
| 4350 |
+
858,
|
| 4351 |
+
874,
|
| 4352 |
+
1
|
| 4353 |
+
],
|
| 4354 |
+
[
|
| 4355 |
+
716,
|
| 4356 |
+
714,
|
| 4357 |
+
1
|
| 4358 |
+
],
|
| 4359 |
+
[
|
| 4360 |
+
916,
|
| 4361 |
+
1122,
|
| 4362 |
+
1
|
| 4363 |
+
],
|
| 4364 |
+
[
|
| 4365 |
+
871,
|
| 4366 |
+
888,
|
| 4367 |
+
1
|
| 4368 |
+
],
|
| 4369 |
+
[
|
| 4370 |
+
824,
|
| 4371 |
+
854,
|
| 4372 |
+
1
|
| 4373 |
+
],
|
| 4374 |
+
[
|
| 4375 |
+
1060,
|
| 4376 |
+
846,
|
| 4377 |
+
4
|
| 4378 |
+
],
|
| 4379 |
+
[
|
| 4380 |
+
835,
|
| 4381 |
+
835,
|
| 4382 |
+
4
|
| 4383 |
+
],
|
| 4384 |
+
[
|
| 4385 |
+
210,
|
| 4386 |
+
209,
|
| 4387 |
+
1
|
| 4388 |
+
],
|
| 4389 |
+
[
|
| 4390 |
+
717,
|
| 4391 |
+
1119,
|
| 4392 |
+
1
|
| 4393 |
+
],
|
| 4394 |
+
[
|
| 4395 |
+
872,
|
| 4396 |
+
891,
|
| 4397 |
+
1
|
| 4398 |
+
],
|
| 4399 |
+
[
|
| 4400 |
+
912,
|
| 4401 |
+
912,
|
| 4402 |
+
1
|
| 4403 |
+
],
|
| 4404 |
+
[
|
| 4405 |
+
1094,
|
| 4406 |
+
1094,
|
| 4407 |
+
2
|
| 4408 |
+
],
|
| 4409 |
+
[
|
| 4410 |
+
851,
|
| 4411 |
+
838,
|
| 4412 |
+
1
|
| 4413 |
+
],
|
| 4414 |
+
[
|
| 4415 |
+
824,
|
| 4416 |
+
938,
|
| 4417 |
+
1
|
| 4418 |
+
],
|
| 4419 |
+
[
|
| 4420 |
+
33,
|
| 4421 |
+
32,
|
| 4422 |
+
1
|
| 4423 |
+
],
|
| 4424 |
+
[
|
| 4425 |
+
843,
|
| 4426 |
+
827,
|
| 4427 |
+
1
|
| 4428 |
+
],
|
| 4429 |
+
[
|
| 4430 |
+
538,
|
| 4431 |
+
539,
|
| 4432 |
+
1
|
| 4433 |
+
],
|
| 4434 |
+
[
|
| 4435 |
+
1067,
|
| 4436 |
+
1067,
|
| 4437 |
+
2
|
| 4438 |
+
],
|
| 4439 |
+
[
|
| 4440 |
+
824,
|
| 4441 |
+
935,
|
| 4442 |
+
1
|
| 4443 |
+
],
|
| 4444 |
+
[
|
| 4445 |
+
913,
|
| 4446 |
+
953,
|
| 4447 |
+
1
|
| 4448 |
+
],
|
| 4449 |
+
[
|
| 4450 |
+
925,
|
| 4451 |
+
873,
|
| 4452 |
+
1
|
| 4453 |
+
],
|
| 4454 |
+
[
|
| 4455 |
+
839,
|
| 4456 |
+
839,
|
| 4457 |
+
1
|
| 4458 |
+
],
|
| 4459 |
+
[
|
| 4460 |
+
49,
|
| 4461 |
+
49,
|
| 4462 |
+
1
|
| 4463 |
+
],
|
| 4464 |
+
[
|
| 4465 |
+
824,
|
| 4466 |
+
937,
|
| 4467 |
+
1
|
| 4468 |
+
],
|
| 4469 |
+
[
|
| 4470 |
+
879,
|
| 4471 |
+
889,
|
| 4472 |
+
1
|
| 4473 |
+
],
|
| 4474 |
+
[
|
| 4475 |
+
829,
|
| 4476 |
+
847,
|
| 4477 |
+
2
|
| 4478 |
+
],
|
| 4479 |
+
[
|
| 4480 |
+
827,
|
| 4481 |
+
827,
|
| 4482 |
+
1
|
| 4483 |
+
],
|
| 4484 |
+
[
|
| 4485 |
+
533,
|
| 4486 |
+
531,
|
| 4487 |
+
1
|
| 4488 |
+
],
|
| 4489 |
+
[
|
| 4490 |
+
832,
|
| 4491 |
+
846,
|
| 4492 |
+
2
|
| 4493 |
+
],
|
| 4494 |
+
[
|
| 4495 |
+
824,
|
| 4496 |
+
940,
|
| 4497 |
+
1
|
| 4498 |
+
],
|
| 4499 |
+
[
|
| 4500 |
+
918,
|
| 4501 |
+
1127,
|
| 4502 |
+
1
|
| 4503 |
+
],
|
| 4504 |
+
[
|
| 4505 |
+
867,
|
| 4506 |
+
890,
|
| 4507 |
+
1
|
| 4508 |
+
],
|
| 4509 |
+
[
|
| 4510 |
+
969,
|
| 4511 |
+
887,
|
| 4512 |
+
2
|
| 4513 |
+
],
|
| 4514 |
+
[
|
| 4515 |
+
840,
|
| 4516 |
+
862,
|
| 4517 |
+
1
|
| 4518 |
+
],
|
| 4519 |
+
[
|
| 4520 |
+
841,
|
| 4521 |
+
863,
|
| 4522 |
+
1
|
| 4523 |
+
],
|
| 4524 |
+
[
|
| 4525 |
+
949,
|
| 4526 |
+
887,
|
| 4527 |
+
2
|
| 4528 |
+
],
|
| 4529 |
+
[
|
| 4530 |
+
1040,
|
| 4531 |
+
1040,
|
| 4532 |
+
2
|
| 4533 |
+
],
|
| 4534 |
+
[
|
| 4535 |
+
824,
|
| 4536 |
+
915,
|
| 4537 |
+
1
|
| 4538 |
+
],
|
| 4539 |
+
[
|
| 4540 |
+
831,
|
| 4541 |
+
853,
|
| 4542 |
+
2
|
| 4543 |
+
],
|
| 4544 |
+
[
|
| 4545 |
+
1070,
|
| 4546 |
+
207,
|
| 4547 |
+
2
|
| 4548 |
+
],
|
| 4549 |
+
[
|
| 4550 |
+
210,
|
| 4551 |
+
211,
|
| 4552 |
+
1
|
| 4553 |
+
],
|
| 4554 |
+
[
|
| 4555 |
+
934,
|
| 4556 |
+
874,
|
| 4557 |
+
1
|
| 4558 |
+
],
|
| 4559 |
+
[
|
| 4560 |
+
851,
|
| 4561 |
+
836,
|
| 4562 |
+
1
|
| 4563 |
+
],
|
| 4564 |
+
[
|
| 4565 |
+
1057,
|
| 4566 |
+
116,
|
| 4567 |
+
2
|
| 4568 |
+
],
|
| 4569 |
+
[
|
| 4570 |
+
875,
|
| 4571 |
+
1178,
|
| 4572 |
+
1
|
| 4573 |
+
],
|
| 4574 |
+
[
|
| 4575 |
+
206,
|
| 4576 |
+
946,
|
| 4577 |
+
1
|
| 4578 |
+
],
|
| 4579 |
+
[
|
| 4580 |
+
1013,
|
| 4581 |
+
36,
|
| 4582 |
+
1
|
| 4583 |
+
],
|
| 4584 |
+
[
|
| 4585 |
+
117,
|
| 4586 |
+
943,
|
| 4587 |
+
1
|
| 4588 |
+
],
|
| 4589 |
+
[
|
| 4590 |
+
1069,
|
| 4591 |
+
200,
|
| 4592 |
+
2
|
| 4593 |
+
],
|
| 4594 |
+
[
|
| 4595 |
+
999,
|
| 4596 |
+
44,
|
| 4597 |
+
1
|
| 4598 |
+
],
|
| 4599 |
+
[
|
| 4600 |
+
827,
|
| 4601 |
+
843,
|
| 4602 |
+
1
|
| 4603 |
+
],
|
| 4604 |
+
[
|
| 4605 |
+
833,
|
| 4606 |
+
838,
|
| 4607 |
+
1
|
| 4608 |
+
],
|
| 4609 |
+
[
|
| 4610 |
+
955,
|
| 4611 |
+
955,
|
| 4612 |
+
2
|
| 4613 |
+
],
|
| 4614 |
+
[
|
| 4615 |
+
1058,
|
| 4616 |
+
126,
|
| 4617 |
+
2
|
| 4618 |
+
],
|
| 4619 |
+
[
|
| 4620 |
+
1121,
|
| 4621 |
+
1121,
|
| 4622 |
+
2
|
| 4623 |
+
],
|
| 4624 |
+
[
|
| 4625 |
+
718,
|
| 4626 |
+
719,
|
| 4627 |
+
1
|
| 4628 |
+
],
|
| 4629 |
+
[
|
| 4630 |
+
984,
|
| 4631 |
+
950,
|
| 4632 |
+
1
|
| 4633 |
+
],
|
| 4634 |
+
[
|
| 4635 |
+
877,
|
| 4636 |
+
1179,
|
| 4637 |
+
1
|
| 4638 |
+
],
|
| 4639 |
+
[
|
| 4640 |
+
939,
|
| 4641 |
+
533,
|
| 4642 |
+
1
|
| 4643 |
+
],
|
| 4644 |
+
[
|
| 4645 |
+
327,
|
| 4646 |
+
328,
|
| 4647 |
+
1
|
| 4648 |
+
],
|
| 4649 |
+
[
|
| 4650 |
+
1021,
|
| 4651 |
+
971,
|
| 4652 |
+
1
|
| 4653 |
+
],
|
| 4654 |
+
[
|
| 4655 |
+
908,
|
| 4656 |
+
837,
|
| 4657 |
+
1
|
| 4658 |
+
],
|
| 4659 |
+
[
|
| 4660 |
+
1052,
|
| 4661 |
+
837,
|
| 4662 |
+
2
|
| 4663 |
+
],
|
| 4664 |
+
[
|
| 4665 |
+
201,
|
| 4666 |
+
200,
|
| 4667 |
+
1
|
| 4668 |
+
],
|
| 4669 |
+
[
|
| 4670 |
+
867,
|
| 4671 |
+
895,
|
| 4672 |
+
1
|
| 4673 |
+
],
|
| 4674 |
+
[
|
| 4675 |
+
855,
|
| 4676 |
+
838,
|
| 4677 |
+
1
|
| 4678 |
+
],
|
| 4679 |
+
[
|
| 4680 |
+
830,
|
| 4681 |
+
830,
|
| 4682 |
+
4
|
| 4683 |
+
],
|
| 4684 |
+
[
|
| 4685 |
+
974,
|
| 4686 |
+
897,
|
| 4687 |
+
2
|
| 4688 |
+
],
|
| 4689 |
+
[
|
| 4690 |
+
1041,
|
| 4691 |
+
1041,
|
| 4692 |
+
2
|
| 4693 |
+
],
|
| 4694 |
+
[
|
| 4695 |
+
857,
|
| 4696 |
+
1010,
|
| 4697 |
+
1
|
| 4698 |
+
],
|
| 4699 |
+
[
|
| 4700 |
+
1103,
|
| 4701 |
+
199,
|
| 4702 |
+
2
|
| 4703 |
+
],
|
| 4704 |
+
[
|
| 4705 |
+
197,
|
| 4706 |
+
1216,
|
| 4707 |
+
1
|
| 4708 |
+
],
|
| 4709 |
+
[
|
| 4710 |
+
1014,
|
| 4711 |
+
1014,
|
| 4712 |
+
1
|
| 4713 |
+
],
|
| 4714 |
+
[
|
| 4715 |
+
861,
|
| 4716 |
+
885,
|
| 4717 |
+
2
|
| 4718 |
+
],
|
| 4719 |
+
[
|
| 4720 |
+
526,
|
| 4721 |
+
956,
|
| 4722 |
+
1
|
| 4723 |
+
],
|
| 4724 |
+
[
|
| 4725 |
+
1026,
|
| 4726 |
+
1108,
|
| 4727 |
+
2
|
| 4728 |
+
],
|
| 4729 |
+
[
|
| 4730 |
+
1074,
|
| 4731 |
+
724,
|
| 4732 |
+
2
|
| 4733 |
+
],
|
| 4734 |
+
[
|
| 4735 |
+
878,
|
| 4736 |
+
904,
|
| 4737 |
+
2
|
| 4738 |
+
],
|
| 4739 |
+
[
|
| 4740 |
+
985,
|
| 4741 |
+
531,
|
| 4742 |
+
1
|
| 4743 |
+
],
|
| 4744 |
+
[
|
| 4745 |
+
125,
|
| 4746 |
+
945,
|
| 4747 |
+
1
|
| 4748 |
+
],
|
| 4749 |
+
[
|
| 4750 |
+
855,
|
| 4751 |
+
833,
|
| 4752 |
+
1
|
| 4753 |
+
],
|
| 4754 |
+
[
|
| 4755 |
+
45,
|
| 4756 |
+
1200,
|
| 4757 |
+
1
|
| 4758 |
+
],
|
| 4759 |
+
[
|
| 4760 |
+
981,
|
| 4761 |
+
981,
|
| 4762 |
+
1
|
| 4763 |
+
],
|
| 4764 |
+
[
|
| 4765 |
+
828,
|
| 4766 |
+
828,
|
| 4767 |
+
4
|
| 4768 |
+
],
|
| 4769 |
+
[
|
| 4770 |
+
329,
|
| 4771 |
+
973,
|
| 4772 |
+
1
|
| 4773 |
+
],
|
| 4774 |
+
[
|
| 4775 |
+
876,
|
| 4776 |
+
876,
|
| 4777 |
+
3
|
| 4778 |
+
],
|
| 4779 |
+
[
|
| 4780 |
+
836,
|
| 4781 |
+
851,
|
| 4782 |
+
1
|
| 4783 |
+
],
|
| 4784 |
+
[
|
| 4785 |
+
961,
|
| 4786 |
+
961,
|
| 4787 |
+
2
|
| 4788 |
+
],
|
| 4789 |
+
[
|
| 4790 |
+
848,
|
| 4791 |
+
1053,
|
| 4792 |
+
1
|
| 4793 |
+
],
|
| 4794 |
+
[
|
| 4795 |
+
913,
|
| 4796 |
+
941,
|
| 4797 |
+
1
|
| 4798 |
+
],
|
| 4799 |
+
[
|
| 4800 |
+
1024,
|
| 4801 |
+
1024,
|
| 4802 |
+
3
|
| 4803 |
+
],
|
| 4804 |
+
[
|
| 4805 |
+
1038,
|
| 4806 |
+
1038,
|
| 4807 |
+
2
|
| 4808 |
+
],
|
| 4809 |
+
[
|
| 4810 |
+
849,
|
| 4811 |
+
1055,
|
| 4812 |
+
1
|
| 4813 |
+
],
|
| 4814 |
+
[
|
| 4815 |
+
901,
|
| 4816 |
+
901,
|
| 4817 |
+
1
|
| 4818 |
+
],
|
| 4819 |
+
[
|
| 4820 |
+
202,
|
| 4821 |
+
1106,
|
| 4822 |
+
1
|
| 4823 |
+
],
|
| 4824 |
+
[
|
| 4825 |
+
1160,
|
| 4826 |
+
887,
|
| 4827 |
+
4
|
| 4828 |
+
],
|
| 4829 |
+
[
|
| 4830 |
+
1044,
|
| 4831 |
+
1044,
|
| 4832 |
+
2
|
| 4833 |
+
],
|
| 4834 |
+
[
|
| 4835 |
+
964,
|
| 4836 |
+
964,
|
| 4837 |
+
2
|
| 4838 |
+
],
|
| 4839 |
+
[
|
| 4840 |
+
528,
|
| 4841 |
+
528,
|
| 4842 |
+
3
|
| 4843 |
+
],
|
| 4844 |
+
[
|
| 4845 |
+
527,
|
| 4846 |
+
527,
|
| 4847 |
+
2
|
| 4848 |
+
],
|
| 4849 |
+
[
|
| 4850 |
+
933,
|
| 4851 |
+
933,
|
| 4852 |
+
2
|
| 4853 |
+
],
|
| 4854 |
+
[
|
| 4855 |
+
827,
|
| 4856 |
+
836,
|
| 4857 |
+
1
|
| 4858 |
+
],
|
| 4859 |
+
[
|
| 4860 |
+
834,
|
| 4861 |
+
823,
|
| 4862 |
+
1
|
| 4863 |
+
],
|
| 4864 |
+
[
|
| 4865 |
+
848,
|
| 4866 |
+
988,
|
| 4867 |
+
1
|
| 4868 |
+
],
|
| 4869 |
+
[
|
| 4870 |
+
832,
|
| 4871 |
+
852,
|
| 4872 |
+
2
|
| 4873 |
+
],
|
| 4874 |
+
[
|
| 4875 |
+
825,
|
| 4876 |
+
854,
|
| 4877 |
+
1
|
| 4878 |
+
],
|
| 4879 |
+
[
|
| 4880 |
+
833,
|
| 4881 |
+
855,
|
| 4882 |
+
1
|
| 4883 |
+
],
|
| 4884 |
+
[
|
| 4885 |
+
827,
|
| 4886 |
+
845,
|
| 4887 |
+
1
|
| 4888 |
+
],
|
| 4889 |
+
[
|
| 4890 |
+
998,
|
| 4891 |
+
840,
|
| 4892 |
+
1
|
| 4893 |
+
],
|
| 4894 |
+
[
|
| 4895 |
+
1109,
|
| 4896 |
+
207,
|
| 4897 |
+
2
|
| 4898 |
+
],
|
| 4899 |
+
[
|
| 4900 |
+
332,
|
| 4901 |
+
980,
|
| 4902 |
+
1
|
| 4903 |
+
],
|
| 4904 |
+
[
|
| 4905 |
+
1125,
|
| 4906 |
+
44,
|
| 4907 |
+
2
|
| 4908 |
+
],
|
| 4909 |
+
[
|
| 4910 |
+
848,
|
| 4911 |
+
992,
|
| 4912 |
+
1
|
| 4913 |
+
],
|
| 4914 |
+
[
|
| 4915 |
+
849,
|
| 4916 |
+
993,
|
| 4917 |
+
1
|
| 4918 |
+
],
|
| 4919 |
+
[
|
| 4920 |
+
872,
|
| 4921 |
+
902,
|
| 4922 |
+
1
|
| 4923 |
+
],
|
| 4924 |
+
[
|
| 4925 |
+
826,
|
| 4926 |
+
854,
|
| 4927 |
+
1
|
| 4928 |
+
],
|
| 4929 |
+
[
|
| 4930 |
+
344,
|
| 4931 |
+
982,
|
| 4932 |
+
1
|
| 4933 |
+
],
|
| 4934 |
+
[
|
| 4935 |
+
1043,
|
| 4936 |
+
1043,
|
| 4937 |
+
2
|
| 4938 |
+
],
|
| 4939 |
+
[
|
| 4940 |
+
532,
|
| 4941 |
+
530,
|
| 4942 |
+
1
|
| 4943 |
+
],
|
| 4944 |
+
[
|
| 4945 |
+
831,
|
| 4946 |
+
835,
|
| 4947 |
+
2
|
| 4948 |
+
],
|
| 4949 |
+
[
|
| 4950 |
+
712,
|
| 4951 |
+
712,
|
| 4952 |
+
1
|
| 4953 |
+
],
|
| 4954 |
+
[
|
| 4955 |
+
1009,
|
| 4956 |
+
1009,
|
| 4957 |
+
1
|
| 4958 |
+
],
|
| 4959 |
+
[
|
| 4960 |
+
1071,
|
| 4961 |
+
201,
|
| 4962 |
+
2
|
| 4963 |
+
],
|
| 4964 |
+
[
|
| 4965 |
+
1007,
|
| 4966 |
+
1007,
|
| 4967 |
+
1
|
| 4968 |
+
],
|
| 4969 |
+
[
|
| 4970 |
+
1072,
|
| 4971 |
+
36,
|
| 4972 |
+
2
|
| 4973 |
+
],
|
| 4974 |
+
[
|
| 4975 |
+
206,
|
| 4976 |
+
957,
|
| 4977 |
+
1
|
| 4978 |
+
],
|
| 4979 |
+
[
|
| 4980 |
+
845,
|
| 4981 |
+
827,
|
| 4982 |
+
1
|
| 4983 |
+
],
|
| 4984 |
+
[
|
| 4985 |
+
722,
|
| 4986 |
+
1107,
|
| 4987 |
+
1
|
| 4988 |
+
],
|
| 4989 |
+
[
|
| 4990 |
+
952,
|
| 4991 |
+
205,
|
| 4992 |
+
1
|
| 4993 |
+
],
|
| 4994 |
+
[
|
| 4995 |
+
976,
|
| 4996 |
+
976,
|
| 4997 |
+
2
|
| 4998 |
+
],
|
| 4999 |
+
[
|
| 5000 |
+
1002,
|
| 5001 |
+
841,
|
| 5002 |
+
1
|
| 5003 |
+
],
|
| 5004 |
+
[
|
| 5005 |
+
949,
|
| 5006 |
+
897,
|
| 5007 |
+
2
|
| 5008 |
+
],
|
| 5009 |
+
[
|
| 5010 |
+
1031,
|
| 5011 |
+
1031,
|
| 5012 |
+
2
|
| 5013 |
+
],
|
| 5014 |
+
[
|
| 5015 |
+
857,
|
| 5016 |
+
1023,
|
| 5017 |
+
1
|
| 5018 |
+
],
|
| 5019 |
+
[
|
| 5020 |
+
1005,
|
| 5021 |
+
1005,
|
| 5022 |
+
1
|
| 5023 |
+
],
|
| 5024 |
+
[
|
| 5025 |
+
1006,
|
| 5026 |
+
1006,
|
| 5027 |
+
1
|
| 5028 |
+
],
|
| 5029 |
+
[
|
| 5030 |
+
871,
|
| 5031 |
+
906,
|
| 5032 |
+
1
|
| 5033 |
+
],
|
| 5034 |
+
[
|
| 5035 |
+
889,
|
| 5036 |
+
948,
|
| 5037 |
+
1
|
| 5038 |
+
],
|
| 5039 |
+
[
|
| 5040 |
+
831,
|
| 5041 |
+
852,
|
| 5042 |
+
2
|
| 5043 |
+
],
|
| 5044 |
+
[
|
| 5045 |
+
130,
|
| 5046 |
+
130,
|
| 5047 |
+
1
|
| 5048 |
+
],
|
| 5049 |
+
[
|
| 5050 |
+
117,
|
| 5051 |
+
972,
|
| 5052 |
+
1
|
| 5053 |
+
],
|
| 5054 |
+
[
|
| 5055 |
+
1018,
|
| 5056 |
+
1018,
|
| 5057 |
+
1
|
| 5058 |
+
],
|
| 5059 |
+
[
|
| 5060 |
+
829,
|
| 5061 |
+
846,
|
| 5062 |
+
2
|
| 5063 |
+
],
|
| 5064 |
+
[
|
| 5065 |
+
953,
|
| 5066 |
+
198,
|
| 5067 |
+
1
|
| 5068 |
+
],
|
| 5069 |
+
[
|
| 5070 |
+
529,
|
| 5071 |
+
531,
|
| 5072 |
+
1
|
| 5073 |
+
],
|
| 5074 |
+
[
|
| 5075 |
+
892,
|
| 5076 |
+
882,
|
| 5077 |
+
1
|
| 5078 |
+
],
|
| 5079 |
+
[
|
| 5080 |
+
836,
|
| 5081 |
+
827,
|
| 5082 |
+
1
|
| 5083 |
+
],
|
| 5084 |
+
[
|
| 5085 |
+
889,
|
| 5086 |
+
951,
|
| 5087 |
+
1
|
| 5088 |
+
],
|
| 5089 |
+
[
|
| 5090 |
+
1068,
|
| 5091 |
+
818,
|
| 5092 |
+
1
|
| 5093 |
+
],
|
| 5094 |
+
[
|
| 5095 |
+
925,
|
| 5096 |
+
723,
|
| 5097 |
+
1
|
| 5098 |
+
],
|
| 5099 |
+
[
|
| 5100 |
+
348,
|
| 5101 |
+
349,
|
| 5102 |
+
1
|
| 5103 |
+
],
|
| 5104 |
+
[
|
| 5105 |
+
849,
|
| 5106 |
+
1001,
|
| 5107 |
+
1
|
| 5108 |
+
],
|
| 5109 |
+
[
|
| 5110 |
+
1076,
|
| 5111 |
+
42,
|
| 5112 |
+
2
|
| 5113 |
+
],
|
| 5114 |
+
[
|
| 5115 |
+
960,
|
| 5116 |
+
888,
|
| 5117 |
+
1
|
| 5118 |
+
],
|
| 5119 |
+
[
|
| 5120 |
+
834,
|
| 5121 |
+
856,
|
| 5122 |
+
1
|
| 5123 |
+
],
|
| 5124 |
+
[
|
| 5125 |
+
869,
|
| 5126 |
+
854,
|
| 5127 |
+
1
|
| 5128 |
+
],
|
| 5129 |
+
[
|
| 5130 |
+
345,
|
| 5131 |
+
345,
|
| 5132 |
+
1
|
| 5133 |
+
],
|
| 5134 |
+
[
|
| 5135 |
+
891,
|
| 5136 |
+
872,
|
| 5137 |
+
1
|
| 5138 |
+
],
|
| 5139 |
+
[
|
| 5140 |
+
842,
|
| 5141 |
+
842,
|
| 5142 |
+
1
|
| 5143 |
+
],
|
| 5144 |
+
[
|
| 5145 |
+
893,
|
| 5146 |
+
883,
|
| 5147 |
+
1
|
| 5148 |
+
],
|
| 5149 |
+
[
|
| 5150 |
+
1074,
|
| 5151 |
+
723,
|
| 5152 |
+
2
|
| 5153 |
+
],
|
| 5154 |
+
[
|
| 5155 |
+
1086,
|
| 5156 |
+
817,
|
| 5157 |
+
4
|
| 5158 |
+
],
|
| 5159 |
+
[
|
| 5160 |
+
839,
|
| 5161 |
+
839,
|
| 5162 |
+
3
|
| 5163 |
+
],
|
| 5164 |
+
[
|
| 5165 |
+
1083,
|
| 5166 |
+
1028,
|
| 5167 |
+
4
|
| 5168 |
+
],
|
| 5169 |
+
[
|
| 5170 |
+
47,
|
| 5171 |
+
48,
|
| 5172 |
+
1
|
| 5173 |
+
],
|
| 5174 |
+
[
|
| 5175 |
+
1070,
|
| 5176 |
+
1070,
|
| 5177 |
+
2
|
| 5178 |
+
],
|
| 5179 |
+
[
|
| 5180 |
+
886,
|
| 5181 |
+
922,
|
| 5182 |
+
1
|
| 5183 |
+
],
|
| 5184 |
+
[
|
| 5185 |
+
527,
|
| 5186 |
+
1034,
|
| 5187 |
+
1
|
| 5188 |
+
],
|
| 5189 |
+
[
|
| 5190 |
+
837,
|
| 5191 |
+
908,
|
| 5192 |
+
1
|
| 5193 |
+
],
|
| 5194 |
+
[
|
| 5195 |
+
331,
|
| 5196 |
+
331,
|
| 5197 |
+
1
|
| 5198 |
+
],
|
| 5199 |
+
[
|
| 5200 |
+
528,
|
| 5201 |
+
864,
|
| 5202 |
+
1
|
| 5203 |
+
],
|
| 5204 |
+
[
|
| 5205 |
+
1090,
|
| 5206 |
+
120,
|
| 5207 |
+
2
|
| 5208 |
+
],
|
| 5209 |
+
[
|
| 5210 |
+
715,
|
| 5211 |
+
1119,
|
| 5212 |
+
1
|
| 5213 |
+
],
|
| 5214 |
+
[
|
| 5215 |
+
913,
|
| 5216 |
+
858,
|
| 5217 |
+
1
|
| 5218 |
+
],
|
| 5219 |
+
[
|
| 5220 |
+
942,
|
| 5221 |
+
942,
|
| 5222 |
+
1
|
| 5223 |
+
],
|
| 5224 |
+
[
|
| 5225 |
+
833,
|
| 5226 |
+
843,
|
| 5227 |
+
2
|
| 5228 |
+
],
|
| 5229 |
+
[
|
| 5230 |
+
834,
|
| 5231 |
+
907,
|
| 5232 |
+
1
|
| 5233 |
+
],
|
| 5234 |
+
[
|
| 5235 |
+
823,
|
| 5236 |
+
823,
|
| 5237 |
+
2
|
| 5238 |
+
],
|
| 5239 |
+
[
|
| 5240 |
+
725,
|
| 5241 |
+
723,
|
| 5242 |
+
1
|
| 5243 |
+
],
|
| 5244 |
+
[
|
| 5245 |
+
886,
|
| 5246 |
+
921,
|
| 5247 |
+
1
|
| 5248 |
+
],
|
| 5249 |
+
[
|
| 5250 |
+
925,
|
| 5251 |
+
888,
|
| 5252 |
+
1
|
| 5253 |
+
],
|
| 5254 |
+
[
|
| 5255 |
+
1135,
|
| 5256 |
+
862,
|
| 5257 |
+
2
|
| 5258 |
+
],
|
| 5259 |
+
[
|
| 5260 |
+
844,
|
| 5261 |
+
844,
|
| 5262 |
+
2
|
| 5263 |
+
],
|
| 5264 |
+
[
|
| 5265 |
+
892,
|
| 5266 |
+
921,
|
| 5267 |
+
1
|
| 5268 |
+
],
|
| 5269 |
+
[
|
| 5270 |
+
728,
|
| 5271 |
+
728,
|
| 5272 |
+
1
|
| 5273 |
+
],
|
| 5274 |
+
[
|
| 5275 |
+
835,
|
| 5276 |
+
829,
|
| 5277 |
+
1
|
| 5278 |
+
],
|
| 5279 |
+
[
|
| 5280 |
+
835,
|
| 5281 |
+
831,
|
| 5282 |
+
1
|
| 5283 |
+
],
|
| 5284 |
+
[
|
| 5285 |
+
1061,
|
| 5286 |
+
1061,
|
| 5287 |
+
4
|
| 5288 |
+
],
|
| 5289 |
+
[
|
| 5290 |
+
1072,
|
| 5291 |
+
38,
|
| 5292 |
+
2
|
| 5293 |
+
],
|
| 5294 |
+
[
|
| 5295 |
+
725,
|
| 5296 |
+
727,
|
| 5297 |
+
1
|
| 5298 |
+
],
|
| 5299 |
+
[
|
| 5300 |
+
1130,
|
| 5301 |
+
719,
|
| 5302 |
+
2
|
| 5303 |
+
],
|
| 5304 |
+
[
|
| 5305 |
+
886,
|
| 5306 |
+
939,
|
| 5307 |
+
1
|
| 5308 |
+
],
|
| 5309 |
+
[
|
| 5310 |
+
1077,
|
| 5311 |
+
862,
|
| 5312 |
+
2
|
| 5313 |
+
],
|
| 5314 |
+
[
|
| 5315 |
+
861,
|
| 5316 |
+
910,
|
| 5317 |
+
2
|
| 5318 |
+
],
|
| 5319 |
+
[
|
| 5320 |
+
715,
|
| 5321 |
+
717,
|
| 5322 |
+
1
|
| 5323 |
+
],
|
| 5324 |
+
[
|
| 5325 |
+
874,
|
| 5326 |
+
858,
|
| 5327 |
+
1
|
| 5328 |
+
],
|
| 5329 |
+
[
|
| 5330 |
+
889,
|
| 5331 |
+
826,
|
| 5332 |
+
1
|
| 5333 |
+
],
|
| 5334 |
+
[
|
| 5335 |
+
524,
|
| 5336 |
+
522,
|
| 5337 |
+
1
|
| 5338 |
+
],
|
| 5339 |
+
[
|
| 5340 |
+
893,
|
| 5341 |
+
922,
|
| 5342 |
+
1
|
| 5343 |
+
],
|
| 5344 |
+
[
|
| 5345 |
+
889,
|
| 5346 |
+
825,
|
| 5347 |
+
1
|
| 5348 |
+
],
|
| 5349 |
+
[
|
| 5350 |
+
1105,
|
| 5351 |
+
717,
|
| 5352 |
+
2
|
| 5353 |
+
],
|
| 5354 |
+
[
|
| 5355 |
+
112,
|
| 5356 |
+
112,
|
| 5357 |
+
1
|
| 5358 |
+
],
|
| 5359 |
+
[
|
| 5360 |
+
1064,
|
| 5361 |
+
840,
|
| 5362 |
+
2
|
| 5363 |
+
],
|
| 5364 |
+
[
|
| 5365 |
+
1137,
|
| 5366 |
+
863,
|
| 5367 |
+
2
|
| 5368 |
+
],
|
| 5369 |
+
[
|
| 5370 |
+
1131,
|
| 5371 |
+
867,
|
| 5372 |
+
1
|
| 5373 |
+
],
|
| 5374 |
+
[
|
| 5375 |
+
540,
|
| 5376 |
+
539,
|
| 5377 |
+
1
|
| 5378 |
+
],
|
| 5379 |
+
[
|
| 5380 |
+
966,
|
| 5381 |
+
205,
|
| 5382 |
+
1
|
| 5383 |
+
],
|
| 5384 |
+
[
|
| 5385 |
+
1039,
|
| 5386 |
+
1039,
|
| 5387 |
+
2
|
| 5388 |
+
],
|
| 5389 |
+
[
|
| 5390 |
+
520,
|
| 5391 |
+
520,
|
| 5392 |
+
1
|
| 5393 |
+
],
|
| 5394 |
+
[
|
| 5395 |
+
129,
|
| 5396 |
+
128,
|
| 5397 |
+
1
|
| 5398 |
+
],
|
| 5399 |
+
[
|
| 5400 |
+
970,
|
| 5401 |
+
970,
|
| 5402 |
+
2
|
| 5403 |
+
],
|
| 5404 |
+
[
|
| 5405 |
+
722,
|
| 5406 |
+
1133,
|
| 5407 |
+
1
|
| 5408 |
+
],
|
| 5409 |
+
[
|
| 5410 |
+
829,
|
| 5411 |
+
835,
|
| 5412 |
+
2
|
| 5413 |
+
],
|
| 5414 |
+
[
|
| 5415 |
+
117,
|
| 5416 |
+
983,
|
| 5417 |
+
1
|
| 5418 |
+
],
|
| 5419 |
+
[
|
| 5420 |
+
857,
|
| 5421 |
+
908,
|
| 5422 |
+
1
|
| 5423 |
+
],
|
| 5424 |
+
[
|
| 5425 |
+
1057,
|
| 5426 |
+
118,
|
| 5427 |
+
2
|
| 5428 |
+
],
|
| 5429 |
+
[
|
| 5430 |
+
1071,
|
| 5431 |
+
202,
|
| 5432 |
+
2
|
| 5433 |
+
],
|
| 5434 |
+
[
|
| 5435 |
+
1063,
|
| 5436 |
+
1063,
|
| 5437 |
+
4
|
| 5438 |
+
],
|
| 5439 |
+
[
|
| 5440 |
+
891,
|
| 5441 |
+
845,
|
| 5442 |
+
1
|
| 5443 |
+
],
|
| 5444 |
+
[
|
| 5445 |
+
991,
|
| 5446 |
+
991,
|
| 5447 |
+
2
|
| 5448 |
+
],
|
| 5449 |
+
[
|
| 5450 |
+
1081,
|
| 5451 |
+
863,
|
| 5452 |
+
2
|
| 5453 |
+
],
|
| 5454 |
+
[
|
| 5455 |
+
1090,
|
| 5456 |
+
122,
|
| 5457 |
+
2
|
| 5458 |
+
],
|
| 5459 |
+
[
|
| 5460 |
+
206,
|
| 5461 |
+
911,
|
| 5462 |
+
1
|
| 5463 |
+
],
|
| 5464 |
+
[
|
| 5465 |
+
988,
|
| 5466 |
+
988,
|
| 5467 |
+
1
|
| 5468 |
+
],
|
| 5469 |
+
[
|
| 5470 |
+
530,
|
| 5471 |
+
529,
|
| 5472 |
+
2
|
| 5473 |
+
],
|
| 5474 |
+
[
|
| 5475 |
+
113,
|
| 5476 |
+
114,
|
| 5477 |
+
1
|
| 5478 |
+
],
|
| 5479 |
+
[
|
| 5480 |
+
526,
|
| 5481 |
+
528,
|
| 5482 |
+
1
|
| 5483 |
+
],
|
| 5484 |
+
[
|
| 5485 |
+
525,
|
| 5486 |
+
523,
|
| 5487 |
+
1
|
| 5488 |
+
],
|
| 5489 |
+
[
|
| 5490 |
+
1058,
|
| 5491 |
+
124,
|
| 5492 |
+
2
|
| 5493 |
+
],
|
| 5494 |
+
[
|
| 5495 |
+
879,
|
| 5496 |
+
854,
|
| 5497 |
+
1
|
| 5498 |
+
],
|
| 5499 |
+
[
|
| 5500 |
+
212,
|
| 5501 |
+
211,
|
| 5502 |
+
1
|
| 5503 |
+
],
|
| 5504 |
+
[
|
| 5505 |
+
1166,
|
| 5506 |
+
207,
|
| 5507 |
+
2
|
| 5508 |
+
],
|
| 5509 |
+
[
|
| 5510 |
+
1060,
|
| 5511 |
+
1060,
|
| 5512 |
+
4
|
| 5513 |
+
],
|
| 5514 |
+
[
|
| 5515 |
+
125,
|
| 5516 |
+
987,
|
| 5517 |
+
1
|
| 5518 |
+
],
|
| 5519 |
+
[
|
| 5520 |
+
878,
|
| 5521 |
+
1037,
|
| 5522 |
+
2
|
| 5523 |
+
],
|
| 5524 |
+
[
|
| 5525 |
+
1091,
|
| 5526 |
+
118,
|
| 5527 |
+
2
|
| 5528 |
+
],
|
| 5529 |
+
[
|
| 5530 |
+
1094,
|
| 5531 |
+
203,
|
| 5532 |
+
2
|
| 5533 |
+
],
|
| 5534 |
+
[
|
| 5535 |
+
818,
|
| 5536 |
+
832,
|
| 5537 |
+
2
|
| 5538 |
+
],
|
| 5539 |
+
[
|
| 5540 |
+
861,
|
| 5541 |
+
1032,
|
| 5542 |
+
2
|
| 5543 |
+
],
|
| 5544 |
+
[
|
| 5545 |
+
847,
|
| 5546 |
+
1032,
|
| 5547 |
+
2
|
| 5548 |
+
],
|
| 5549 |
+
[
|
| 5550 |
+
1066,
|
| 5551 |
+
841,
|
| 5552 |
+
2
|
| 5553 |
+
],
|
| 5554 |
+
[
|
| 5555 |
+
1172,
|
| 5556 |
+
897,
|
| 5557 |
+
4
|
| 5558 |
+
],
|
| 5559 |
+
[
|
| 5560 |
+
530,
|
| 5561 |
+
528,
|
| 5562 |
+
1
|
| 5563 |
+
],
|
| 5564 |
+
[
|
| 5565 |
+
928,
|
| 5566 |
+
872,
|
| 5567 |
+
1
|
| 5568 |
+
],
|
| 5569 |
+
[
|
| 5570 |
+
846,
|
| 5571 |
+
885,
|
| 5572 |
+
2
|
| 5573 |
+
],
|
| 5574 |
+
[
|
| 5575 |
+
882,
|
| 5576 |
+
892,
|
| 5577 |
+
1
|
| 5578 |
+
],
|
| 5579 |
+
[
|
| 5580 |
+
1073,
|
| 5581 |
+
1073,
|
| 5582 |
+
4
|
| 5583 |
+
],
|
| 5584 |
+
[
|
| 5585 |
+
975,
|
| 5586 |
+
975,
|
| 5587 |
+
2
|
| 5588 |
+
],
|
| 5589 |
+
[
|
| 5590 |
+
831,
|
| 5591 |
+
835,
|
| 5592 |
+
1
|
| 5593 |
+
],
|
| 5594 |
+
[
|
| 5595 |
+
888,
|
| 5596 |
+
871,
|
| 5597 |
+
1
|
| 5598 |
+
],
|
| 5599 |
+
[
|
| 5600 |
+
1044,
|
| 5601 |
+
533,
|
| 5602 |
+
2
|
| 5603 |
+
],
|
| 5604 |
+
[
|
| 5605 |
+
1087,
|
| 5606 |
+
528,
|
| 5607 |
+
2
|
| 5608 |
+
],
|
| 5609 |
+
[
|
| 5610 |
+
1020,
|
| 5611 |
+
947,
|
| 5612 |
+
1
|
| 5613 |
+
],
|
| 5614 |
+
[
|
| 5615 |
+
825,
|
| 5616 |
+
1053,
|
| 5617 |
+
1
|
| 5618 |
+
],
|
| 5619 |
+
[
|
| 5620 |
+
890,
|
| 5621 |
+
946,
|
| 5622 |
+
1
|
| 5623 |
+
],
|
| 5624 |
+
[
|
| 5625 |
+
1030,
|
| 5626 |
+
1030,
|
| 5627 |
+
3
|
| 5628 |
+
],
|
| 5629 |
+
[
|
| 5630 |
+
1118,
|
| 5631 |
+
870,
|
| 5632 |
+
4
|
| 5633 |
+
],
|
| 5634 |
+
[
|
| 5635 |
+
534,
|
| 5636 |
+
536,
|
| 5637 |
+
1
|
| 5638 |
+
],
|
| 5639 |
+
[
|
| 5640 |
+
882,
|
| 5641 |
+
840,
|
| 5642 |
+
1
|
| 5643 |
+
],
|
| 5644 |
+
[
|
| 5645 |
+
883,
|
| 5646 |
+
893,
|
| 5647 |
+
1
|
| 5648 |
+
],
|
| 5649 |
+
[
|
| 5650 |
+
716,
|
| 5651 |
+
1148,
|
| 5652 |
+
1
|
| 5653 |
+
],
|
| 5654 |
+
[
|
| 5655 |
+
873,
|
| 5656 |
+
858,
|
| 5657 |
+
1
|
| 5658 |
+
],
|
| 5659 |
+
[
|
| 5660 |
+
1121,
|
| 5661 |
+
36,
|
| 5662 |
+
2
|
| 5663 |
+
],
|
| 5664 |
+
[
|
| 5665 |
+
900,
|
| 5666 |
+
862,
|
| 5667 |
+
1
|
| 5668 |
+
],
|
| 5669 |
+
[
|
| 5670 |
+
328,
|
| 5671 |
+
328,
|
| 5672 |
+
1
|
| 5673 |
+
],
|
| 5674 |
+
[
|
| 5675 |
+
868,
|
| 5676 |
+
828,
|
| 5677 |
+
1
|
| 5678 |
+
],
|
| 5679 |
+
[
|
| 5680 |
+
993,
|
| 5681 |
+
993,
|
| 5682 |
+
1
|
| 5683 |
+
],
|
| 5684 |
+
[
|
| 5685 |
+
831,
|
| 5686 |
+
832,
|
| 5687 |
+
2
|
| 5688 |
+
],
|
| 5689 |
+
[
|
| 5690 |
+
831,
|
| 5691 |
+
870,
|
| 5692 |
+
2
|
| 5693 |
+
],
|
| 5694 |
+
[
|
| 5695 |
+
867,
|
| 5696 |
+
911,
|
| 5697 |
+
1
|
| 5698 |
+
],
|
| 5699 |
+
[
|
| 5700 |
+
958,
|
| 5701 |
+
958,
|
| 5702 |
+
1
|
| 5703 |
+
],
|
| 5704 |
+
[
|
| 5705 |
+
721,
|
| 5706 |
+
720,
|
| 5707 |
+
1
|
| 5708 |
+
],
|
| 5709 |
+
[
|
| 5710 |
+
208,
|
| 5711 |
+
1287,
|
| 5712 |
+
1
|
| 5713 |
+
],
|
| 5714 |
+
[
|
| 5715 |
+
1000,
|
| 5716 |
+
207,
|
| 5717 |
+
1
|
| 5718 |
+
],
|
| 5719 |
+
[
|
| 5720 |
+
903,
|
| 5721 |
+
863,
|
| 5722 |
+
1
|
| 5723 |
+
],
|
| 5724 |
+
[
|
| 5725 |
+
1022,
|
| 5726 |
+
954,
|
| 5727 |
+
1
|
| 5728 |
+
],
|
| 5729 |
+
[
|
| 5730 |
+
824,
|
| 5731 |
+
901,
|
| 5732 |
+
1
|
| 5733 |
+
],
|
| 5734 |
+
[
|
| 5735 |
+
859,
|
| 5736 |
+
995,
|
| 5737 |
+
1
|
| 5738 |
+
],
|
| 5739 |
+
[
|
| 5740 |
+
1049,
|
| 5741 |
+
1049,
|
| 5742 |
+
4
|
| 5743 |
+
],
|
| 5744 |
+
[
|
| 5745 |
+
1076,
|
| 5746 |
+
44,
|
| 5747 |
+
2
|
| 5748 |
+
],
|
| 5749 |
+
[
|
| 5750 |
+
529,
|
| 5751 |
+
527,
|
| 5752 |
+
1
|
| 5753 |
+
],
|
| 5754 |
+
[
|
| 5755 |
+
869,
|
| 5756 |
+
854,
|
| 5757 |
+
2
|
| 5758 |
+
],
|
| 5759 |
+
[
|
| 5760 |
+
836,
|
| 5761 |
+
845,
|
| 5762 |
+
2
|
| 5763 |
+
],
|
| 5764 |
+
[
|
| 5765 |
+
711,
|
| 5766 |
+
710,
|
| 5767 |
+
1
|
| 5768 |
+
],
|
| 5769 |
+
[
|
| 5770 |
+
1096,
|
| 5771 |
+
535,
|
| 5772 |
+
2
|
| 5773 |
+
],
|
| 5774 |
+
[
|
| 5775 |
+
826,
|
| 5776 |
+
1055,
|
| 5777 |
+
1
|
| 5778 |
+
],
|
| 5779 |
+
[
|
| 5780 |
+
883,
|
| 5781 |
+
841,
|
| 5782 |
+
1
|
| 5783 |
+
],
|
| 5784 |
+
[
|
| 5785 |
+
726,
|
| 5786 |
+
1171,
|
| 5787 |
+
1
|
| 5788 |
+
],
|
| 5789 |
+
[
|
| 5790 |
+
31,
|
| 5791 |
+
30,
|
| 5792 |
+
1
|
| 5793 |
+
],
|
| 5794 |
+
[
|
| 5795 |
+
1014,
|
| 5796 |
+
198,
|
| 5797 |
+
1
|
| 5798 |
+
],
|
| 5799 |
+
[
|
| 5800 |
+
847,
|
| 5801 |
+
1037,
|
| 5802 |
+
2
|
| 5803 |
+
],
|
| 5804 |
+
[
|
| 5805 |
+
875,
|
| 5806 |
+
932,
|
| 5807 |
+
1
|
| 5808 |
+
],
|
| 5809 |
+
[
|
| 5810 |
+
836,
|
| 5811 |
+
851,
|
| 5812 |
+
2
|
| 5813 |
+
],
|
| 5814 |
+
[
|
| 5815 |
+
877,
|
| 5816 |
+
930,
|
| 5817 |
+
1
|
| 5818 |
+
],
|
| 5819 |
+
[
|
| 5820 |
+
1056,
|
| 5821 |
+
843,
|
| 5822 |
+
4
|
| 5823 |
+
],
|
| 5824 |
+
[
|
| 5825 |
+
848,
|
| 5826 |
+
998,
|
| 5827 |
+
1
|
| 5828 |
+
],
|
| 5829 |
+
[
|
| 5830 |
+
902,
|
| 5831 |
+
872,
|
| 5832 |
+
1
|
| 5833 |
+
],
|
| 5834 |
+
[
|
| 5835 |
+
1097,
|
| 5836 |
+
124,
|
| 5837 |
+
2
|
| 5838 |
+
],
|
| 5839 |
+
[
|
| 5840 |
+
829,
|
| 5841 |
+
835,
|
| 5842 |
+
1
|
| 5843 |
+
],
|
| 5844 |
+
[
|
| 5845 |
+
518,
|
| 5846 |
+
519,
|
| 5847 |
+
1
|
| 5848 |
+
],
|
| 5849 |
+
[
|
| 5850 |
+
924,
|
| 5851 |
+
968,
|
| 5852 |
+
1
|
| 5853 |
+
],
|
| 5854 |
+
[
|
| 5855 |
+
828,
|
| 5856 |
+
868,
|
| 5857 |
+
1
|
| 5858 |
+
],
|
| 5859 |
+
[
|
| 5860 |
+
833,
|
| 5861 |
+
855,
|
| 5862 |
+
2
|
| 5863 |
+
],
|
| 5864 |
+
[
|
| 5865 |
+
920,
|
| 5866 |
+
840,
|
| 5867 |
+
1
|
| 5868 |
+
],
|
| 5869 |
+
[
|
| 5870 |
+
818,
|
| 5871 |
+
829,
|
| 5872 |
+
1
|
| 5873 |
+
],
|
| 5874 |
+
[
|
| 5875 |
+
891,
|
| 5876 |
+
959,
|
| 5877 |
+
1
|
| 5878 |
+
],
|
| 5879 |
+
[
|
| 5880 |
+
730,
|
| 5881 |
+
729,
|
| 5882 |
+
1
|
| 5883 |
+
],
|
| 5884 |
+
[
|
| 5885 |
+
965,
|
| 5886 |
+
885,
|
| 5887 |
+
2
|
| 5888 |
+
],
|
| 5889 |
+
[
|
| 5890 |
+
894,
|
| 5891 |
+
894,
|
| 5892 |
+
3
|
| 5893 |
+
],
|
| 5894 |
+
[
|
| 5895 |
+
906,
|
| 5896 |
+
871,
|
| 5897 |
+
1
|
| 5898 |
+
],
|
| 5899 |
+
[
|
| 5900 |
+
824,
|
| 5901 |
+
986,
|
| 5902 |
+
1
|
| 5903 |
+
],
|
| 5904 |
+
[
|
| 5905 |
+
950,
|
| 5906 |
+
984,
|
| 5907 |
+
1
|
| 5908 |
+
],
|
| 5909 |
+
[
|
| 5910 |
+
944,
|
| 5911 |
+
944,
|
| 5912 |
+
2
|
| 5913 |
+
],
|
| 5914 |
+
[
|
| 5915 |
+
1105,
|
| 5916 |
+
718,
|
| 5917 |
+
2
|
| 5918 |
+
],
|
| 5919 |
+
[
|
| 5920 |
+
1047,
|
| 5921 |
+
1047,
|
| 5922 |
+
2
|
| 5923 |
+
],
|
| 5924 |
+
[
|
| 5925 |
+
865,
|
| 5926 |
+
865,
|
| 5927 |
+
2
|
| 5928 |
+
],
|
| 5929 |
+
[
|
| 5930 |
+
824,
|
| 5931 |
+
968,
|
| 5932 |
+
1
|
| 5933 |
+
],
|
| 5934 |
+
[
|
| 5935 |
+
537,
|
| 5936 |
+
535,
|
| 5937 |
+
1
|
| 5938 |
+
],
|
| 5939 |
+
[
|
| 5940 |
+
1125,
|
| 5941 |
+
1125,
|
| 5942 |
+
2
|
| 5943 |
+
],
|
| 5944 |
+
[
|
| 5945 |
+
1026,
|
| 5946 |
+
1026,
|
| 5947 |
+
3
|
| 5948 |
+
]
|
| 5949 |
+
]
|
| 5950 |
+
}
|
profiles/eef/config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "actioncodec2_unified",
|
| 3 |
+
"processor_class": "ActionCodec2Processor"
|
| 4 |
+
}
|
profiles/eef/config.yaml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_unified
|
| 2 |
+
config_version: 1
|
| 3 |
+
physical:
|
| 4 |
+
input_dim: 7
|
| 5 |
+
components:
|
| 6 |
+
translation_m:
|
| 7 |
+
dims:
|
| 8 |
+
start: 0
|
| 9 |
+
stop: 3
|
| 10 |
+
space: additive
|
| 11 |
+
input_order: 0
|
| 12 |
+
primitive_order: 1
|
| 13 |
+
input_transform:
|
| 14 |
+
scale: 1.0
|
| 15 |
+
clip: false
|
| 16 |
+
constraints:
|
| 17 |
+
order_0:
|
| 18 |
+
min: -2.0
|
| 19 |
+
max: 2.0
|
| 20 |
+
order_1:
|
| 21 |
+
min: -0.08
|
| 22 |
+
max: 0.08
|
| 23 |
+
distortion_limit: 0.001
|
| 24 |
+
primitive_selection: smooth
|
| 25 |
+
orientation_so3:
|
| 26 |
+
dims:
|
| 27 |
+
start: 3
|
| 28 |
+
stop: 6
|
| 29 |
+
space: so3
|
| 30 |
+
input_order: 0
|
| 31 |
+
primitive_order: 1
|
| 32 |
+
input_transform:
|
| 33 |
+
scale: 1.0
|
| 34 |
+
clip: false
|
| 35 |
+
constraints:
|
| 36 |
+
order_0:
|
| 37 |
+
min: -3.141592653589793
|
| 38 |
+
max: 3.141592653589793
|
| 39 |
+
order_1:
|
| 40 |
+
min: -0.25
|
| 41 |
+
max: 0.25
|
| 42 |
+
distortion_limit: 0.004
|
| 43 |
+
primitive_selection: smooth
|
| 44 |
+
representation: rotation_vector
|
| 45 |
+
multiplication: right
|
| 46 |
+
gripper_open_command:
|
| 47 |
+
dims:
|
| 48 |
+
- 6
|
| 49 |
+
space: additive
|
| 50 |
+
input_order: 0
|
| 51 |
+
primitive_order: 0
|
| 52 |
+
input_transform:
|
| 53 |
+
scale: 1.0
|
| 54 |
+
clip: true
|
| 55 |
+
constraints:
|
| 56 |
+
order_0:
|
| 57 |
+
min: 0.0
|
| 58 |
+
max: 1.0
|
| 59 |
+
distortion_limit: 0.5
|
| 60 |
+
primitive_selection: nearest
|
| 61 |
+
set_bpe:
|
| 62 |
+
lookahead: 8
|
| 63 |
+
allow_cross_component_merge: false
|
| 64 |
+
train:
|
| 65 |
+
primitives:
|
| 66 |
+
zero_primitive_components:
|
| 67 |
+
- translation_m
|
| 68 |
+
- orientation_so3
|
| 69 |
+
bpe:
|
| 70 |
+
spatial_vocab_size: 1024
|
| 71 |
+
vocab_size: 2048
|
| 72 |
+
quality_control:
|
| 73 |
+
hard_order_1_scale: 3.0
|
| 74 |
+
min_segment_length: 2
|
profiles/eef/primitives.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b393814f311daf6b609475c9cf943bfc9dde617b6b4249bb404df50d98b2526b
|
| 3 |
+
size 7406
|
profiles/eef/processor_config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"processor_class": "ActionCodec2Processor"
|
| 3 |
+
}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=77", "wheel", "pybind11>=2.12"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "actioncodec2-kernels"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Portable C++17 encode/decode kernels for ActionCodec2 tokenizer artifacts"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
license = "Apache-2.0"
|
| 11 |
+
|
| 12 |
+
[tool.setuptools]
|
| 13 |
+
packages = ["actioncodec2_kernels"]
|
| 14 |
+
|
| 15 |
+
[tool.setuptools.package-data]
|
| 16 |
+
actioncodec2_kernels = ["*.cpp", "*.hpp"]
|
release.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"edition": "Extended",
|
| 3 |
+
"encoding_order": 1,
|
| 4 |
+
"precision": "1mm",
|
| 5 |
+
"codebook_size": 2048,
|
| 6 |
+
"profiles": [
|
| 7 |
+
"eef"
|
| 8 |
+
],
|
| 9 |
+
"gripper": "binary",
|
| 10 |
+
"source_revision": "589f307",
|
| 11 |
+
"libero_evaluated": false
|
| 12 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
scipy
|
| 3 |
+
torch>=2.3
|
| 4 |
+
transformers>=4.57,<6
|
| 5 |
+
huggingface-hub
|
| 6 |
+
pyyaml
|
router_config.yaml
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_router
|
| 2 |
+
config_version: 3
|
| 3 |
+
profiles:
|
| 4 |
+
- name: eef
|
| 5 |
+
token_offset: 0
|
| 6 |
+
token_budget: 2048
|
| 7 |
+
artifact_fingerprint: 9272f9beecca4cd1bd2906f2b34c27d81f6f95ba547010843c8f2edfc7018af6
|
| 8 |
+
presets:
|
| 9 |
+
- name: single_eef_abs
|
| 10 |
+
input_dim: 7
|
| 11 |
+
input: absolute
|
| 12 |
+
source_hz: 15.0
|
| 13 |
+
target_hz: 15.0
|
| 14 |
+
interpolation: cubic
|
| 15 |
+
components:
|
| 16 |
+
arm:
|
| 17 |
+
space: eef
|
| 18 |
+
position_dims:
|
| 19 |
+
- 0
|
| 20 |
+
- 1
|
| 21 |
+
- 2
|
| 22 |
+
rotation_dims:
|
| 23 |
+
- 3
|
| 24 |
+
- 4
|
| 25 |
+
- 5
|
| 26 |
+
rotation_format: rotation_vector
|
| 27 |
+
gripper_dim: 6
|
| 28 |
+
gripper_threshold: 0.8
|
| 29 |
+
gripper_open_is_high: true
|
| 30 |
+
- name: single_eef_delta
|
| 31 |
+
input_dim: 7
|
| 32 |
+
input: delta
|
| 33 |
+
source_hz: 15.0
|
| 34 |
+
target_hz: 15.0
|
| 35 |
+
interpolation: cubic
|
| 36 |
+
components:
|
| 37 |
+
arm:
|
| 38 |
+
space: eef
|
| 39 |
+
position_dims:
|
| 40 |
+
- 0
|
| 41 |
+
- 1
|
| 42 |
+
- 2
|
| 43 |
+
rotation_dims:
|
| 44 |
+
- 3
|
| 45 |
+
- 4
|
| 46 |
+
- 5
|
| 47 |
+
rotation_format: rotation_vector
|
| 48 |
+
gripper_dim: 6
|
| 49 |
+
gripper_threshold: 0.8
|
| 50 |
+
gripper_open_is_high: true
|
| 51 |
+
- name: dual_eef_abs
|
| 52 |
+
input_dim: 14
|
| 53 |
+
input: absolute
|
| 54 |
+
source_hz: 15.0
|
| 55 |
+
target_hz: 15.0
|
| 56 |
+
interpolation: cubic
|
| 57 |
+
components:
|
| 58 |
+
left_arm:
|
| 59 |
+
space: eef
|
| 60 |
+
position_dims:
|
| 61 |
+
- 0
|
| 62 |
+
- 1
|
| 63 |
+
- 2
|
| 64 |
+
rotation_dims:
|
| 65 |
+
- 3
|
| 66 |
+
- 4
|
| 67 |
+
- 5
|
| 68 |
+
rotation_format: rotation_vector
|
| 69 |
+
gripper_dim: 6
|
| 70 |
+
gripper_threshold: 0.8
|
| 71 |
+
gripper_open_is_high: true
|
| 72 |
+
right_arm:
|
| 73 |
+
space: eef
|
| 74 |
+
position_dims:
|
| 75 |
+
- 7
|
| 76 |
+
- 8
|
| 77 |
+
- 9
|
| 78 |
+
rotation_dims:
|
| 79 |
+
- 10
|
| 80 |
+
- 11
|
| 81 |
+
- 12
|
| 82 |
+
rotation_format: rotation_vector
|
| 83 |
+
gripper_dim: 13
|
| 84 |
+
gripper_threshold: 0.8
|
| 85 |
+
gripper_open_is_high: true
|
| 86 |
+
- name: dual_eef_delta
|
| 87 |
+
input_dim: 14
|
| 88 |
+
input: delta
|
| 89 |
+
source_hz: 15.0
|
| 90 |
+
target_hz: 15.0
|
| 91 |
+
interpolation: cubic
|
| 92 |
+
components:
|
| 93 |
+
left_arm:
|
| 94 |
+
space: eef
|
| 95 |
+
position_dims:
|
| 96 |
+
- 0
|
| 97 |
+
- 1
|
| 98 |
+
- 2
|
| 99 |
+
rotation_dims:
|
| 100 |
+
- 3
|
| 101 |
+
- 4
|
| 102 |
+
- 5
|
| 103 |
+
rotation_format: rotation_vector
|
| 104 |
+
gripper_dim: 6
|
| 105 |
+
gripper_threshold: 0.8
|
| 106 |
+
gripper_open_is_high: true
|
| 107 |
+
right_arm:
|
| 108 |
+
space: eef
|
| 109 |
+
position_dims:
|
| 110 |
+
- 7
|
| 111 |
+
- 8
|
| 112 |
+
- 9
|
| 113 |
+
rotation_dims:
|
| 114 |
+
- 10
|
| 115 |
+
- 11
|
| 116 |
+
- 12
|
| 117 |
+
rotation_format: rotation_vector
|
| 118 |
+
gripper_dim: 13
|
| 119 |
+
gripper_threshold: 0.8
|
| 120 |
+
gripper_open_is_high: true
|
runtime/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Private inference runtime bundled with this ActionCodec2 artifact."""
|
runtime/backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal backend components for ActionCodec2."""
|
runtime/backend/native_backend.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Optional CPU accelerator; build with python setup.py build_ext --inplace."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
__all__ = ["native", "have_native", "require_native", "resolve_backend"]
|
| 8 |
+
|
| 9 |
+
try: # pragma: no cover - exercised by whichever branch the build state picks
|
| 10 |
+
from .. import _native as native
|
| 11 |
+
except ImportError: # pragma: no cover
|
| 12 |
+
native = None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def have_native() -> bool:
|
| 16 |
+
"""True when the compiled kernels are importable and not disabled."""
|
| 17 |
+
return native is not None and not os.environ.get("ACTIONCODEC2_NO_NATIVE")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def require_native():
|
| 21 |
+
"""Return the native module or explain how to get one."""
|
| 22 |
+
if not have_native():
|
| 23 |
+
raise RuntimeError(
|
| 24 |
+
"the actioncodec2 native extension is not available; build it from "
|
| 25 |
+
"the package native/ directory (python setup.py build_ext --inplace) or "
|
| 26 |
+
"select backend='python'"
|
| 27 |
+
)
|
| 28 |
+
return native
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def resolve_backend(backend: str) -> str:
|
| 32 |
+
"""Turn ``'auto'`` into a concrete backend name."""
|
| 33 |
+
if backend not in {"auto", "native", "python"}:
|
| 34 |
+
raise ValueError("backend must be 'auto', 'native' or 'python'")
|
| 35 |
+
if backend == "auto":
|
| 36 |
+
return "native" if have_native() else "python"
|
| 37 |
+
if backend == "native":
|
| 38 |
+
require_native()
|
| 39 |
+
return backend
|
runtime/common/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal common components for ActionCodec2."""
|
runtime/common/_frontend_base.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ActionCodec2-only tensor and token-row validation helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Sequence
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from ._types import ActionIdxBatch, TokenIds
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BaseActionTokenizer:
|
| 14 |
+
"""Shared action tensor methods, without policy-specific sentinels."""
|
| 15 |
+
|
| 16 |
+
@property
|
| 17 |
+
def new_action_tokens(self) -> list[str]:
|
| 18 |
+
"""Token strings in id order — downstream registers these into its VLM vocab."""
|
| 19 |
+
return list(self._new_action_tokens)
|
| 20 |
+
|
| 21 |
+
def to_token_strs(self, token_ids: TokenIds) -> list[str]:
|
| 22 |
+
"""Render one id row as its token strings, e.g. ``<right_arm_0>`` / ``<action0042>``.
|
| 23 |
+
|
| 24 |
+
This is exactly the text a VLM would see after registering
|
| 25 |
+
:attr:`new_action_tokens` — useful for eyeballing sequences and for
|
| 26 |
+
prompt-template debugging.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
token_ids: One row of action-token ids in ``[0, vocab_size)``.
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
list[str]: Token strings in row order.
|
| 33 |
+
"""
|
| 34 |
+
row = self._to_id_row(token_ids).tolist()
|
| 35 |
+
n = len(self._new_action_tokens)
|
| 36 |
+
bad = [i for i in row if not 0 <= i < n]
|
| 37 |
+
if bad:
|
| 38 |
+
raise ValueError(f"token ids {bad[:5]} out of range [0, {n})")
|
| 39 |
+
return [self._new_action_tokens[i] for i in row]
|
| 40 |
+
|
| 41 |
+
def _check_encode_kwargs(self, encode_kwargs: dict | None) -> dict:
|
| 42 |
+
"""Normalize ``encode_kwargs`` to a dict, rejecting keys outside :attr:`encode_kwargs`."""
|
| 43 |
+
kw = dict(encode_kwargs or {})
|
| 44 |
+
unknown = sorted(set(kw) - self.encode_kwargs)
|
| 45 |
+
if unknown:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
f"{type(self).__name__} does not consume encode_kwargs {unknown}; it declares {sorted(self.encode_kwargs)}"
|
| 48 |
+
)
|
| 49 |
+
return kw
|
| 50 |
+
|
| 51 |
+
@staticmethod
|
| 52 |
+
def _to_id_row(token_ids: TokenIds) -> torch.Tensor:
|
| 53 |
+
"""Validate and normalize one integer id row to a 1-D long tensor."""
|
| 54 |
+
if isinstance(token_ids, torch.Tensor):
|
| 55 |
+
row = token_ids.detach().cpu()
|
| 56 |
+
if (
|
| 57 |
+
row.dtype == torch.bool
|
| 58 |
+
or row.dtype.is_floating_point
|
| 59 |
+
or row.dtype.is_complex
|
| 60 |
+
):
|
| 61 |
+
raise ValueError("token ids must be integers")
|
| 62 |
+
row = row.to(torch.long)
|
| 63 |
+
else:
|
| 64 |
+
values = np.asarray(token_ids)
|
| 65 |
+
if values.dtype.kind not in {"i", "u"}:
|
| 66 |
+
raise ValueError("token ids must be integers")
|
| 67 |
+
row = torch.from_numpy(np.asarray(values, dtype=np.int64))
|
| 68 |
+
if row.ndim != 1:
|
| 69 |
+
raise ValueError(f"token id row must be 1D, got {tuple(row.shape)}")
|
| 70 |
+
return row
|
| 71 |
+
|
| 72 |
+
def _checked_decode_row(self, token_ids: TokenIds) -> torch.Tensor:
|
| 73 |
+
"""Return one validated row without repairing invalid model output."""
|
| 74 |
+
row = self._to_id_row(token_ids)
|
| 75 |
+
invalid = (row < 0) | (row >= int(self._action_vocab_size))
|
| 76 |
+
if torch.any(invalid):
|
| 77 |
+
bad = row[invalid][:5].tolist()
|
| 78 |
+
raise ValueError(
|
| 79 |
+
f"token ids {bad} out of range [0, {int(self._action_vocab_size)})"
|
| 80 |
+
)
|
| 81 |
+
return row
|
| 82 |
+
|
| 83 |
+
def _decode_token_ids_to_actions_batch(
|
| 84 |
+
self,
|
| 85 |
+
action_token_ids: Sequence[TokenIds],
|
| 86 |
+
*,
|
| 87 |
+
time_horizon: int | None = None,
|
| 88 |
+
action_dim: int | None = None,
|
| 89 |
+
decode_kwargs: dict | None = None,
|
| 90 |
+
) -> torch.Tensor:
|
| 91 |
+
"""Decode a batch of id rows to actions ``(B, T, D)``.
|
| 92 |
+
|
| 93 |
+
The decoder applies the configured action-space layout to each row.
|
| 94 |
+
"""
|
| 95 |
+
batch_action_idx: ActionIdxBatch = [
|
| 96 |
+
self._checked_decode_row(a).tolist() for a in action_token_ids
|
| 97 |
+
]
|
| 98 |
+
(actions_np, _) = self._decode_action_indices(
|
| 99 |
+
batch_action_idx,
|
| 100 |
+
time_horizon=time_horizon,
|
| 101 |
+
action_dim=action_dim,
|
| 102 |
+
decode_kwargs=decode_kwargs,
|
| 103 |
+
)
|
| 104 |
+
return torch.from_numpy(actions_np)
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def vocab_size(self) -> int:
|
| 108 |
+
return int(self._action_vocab_size)
|
runtime/common/_types.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared lightweight types for the frontend layer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
ActionIdxBatch = list[list[int]]
|
| 9 |
+
TokenIds = list[int] | np.ndarray | torch.Tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
__all__ = ["ActionIdxBatch", "TokenIds"]
|
runtime/common/_validation.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared validation primitives for ActionCodec2 public boundaries."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterable, Sequence
|
| 6 |
+
from typing import TypeVar
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
ErrorT = TypeVar("ErrorT", bound=Exception)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def active_dimensions(
|
| 14 |
+
active_dims: Sequence[int] | None,
|
| 15 |
+
dimension: int,
|
| 16 |
+
*,
|
| 17 |
+
subject: str = "active_dims",
|
| 18 |
+
) -> tuple[int, ...]:
|
| 19 |
+
"""Normalize and validate a non-empty increasing subset of dimensions."""
|
| 20 |
+
dims = (
|
| 21 |
+
tuple(range(dimension))
|
| 22 |
+
if active_dims is None
|
| 23 |
+
else tuple(int(item) for item in active_dims)
|
| 24 |
+
)
|
| 25 |
+
if not dims:
|
| 26 |
+
raise ValueError(f"{subject} must not be empty")
|
| 27 |
+
if any(left >= right for left, right in zip(dims, dims[1:])):
|
| 28 |
+
raise ValueError(f"{subject} must be strictly increasing")
|
| 29 |
+
if dims[0] < 0 or dims[-1] >= dimension:
|
| 30 |
+
raise ValueError(f"{subject} contains a dimension outside the vocabulary")
|
| 31 |
+
return dims
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def optional_finite_array(
|
| 35 |
+
value: np.ndarray | None,
|
| 36 |
+
*,
|
| 37 |
+
shape: tuple[int, ...],
|
| 38 |
+
name: str = "current_state",
|
| 39 |
+
) -> np.ndarray | None:
|
| 40 |
+
"""Convert an optional float32 array and enforce its exact shape/finiteness."""
|
| 41 |
+
if value is None:
|
| 42 |
+
return None
|
| 43 |
+
result = np.asarray(value, dtype=np.float32)
|
| 44 |
+
if result.shape != shape:
|
| 45 |
+
rendered = ", ".join(str(item) for item in shape)
|
| 46 |
+
raise ValueError(f"{name} must have shape [{rendered}]")
|
| 47 |
+
if not np.all(np.isfinite(result)):
|
| 48 |
+
raise ValueError(f"{name} contains a non-finite value")
|
| 49 |
+
return result
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def broadcast_vector(
|
| 53 |
+
value: object,
|
| 54 |
+
width: int,
|
| 55 |
+
*,
|
| 56 |
+
dtype: np.dtype,
|
| 57 |
+
name: str,
|
| 58 |
+
) -> np.ndarray:
|
| 59 |
+
"""Broadcast a scalar or width-sized value to one numerical vector."""
|
| 60 |
+
array = np.asarray(value, dtype=dtype)
|
| 61 |
+
try:
|
| 62 |
+
return np.broadcast_to(array, (width,)).copy()
|
| 63 |
+
except ValueError as error:
|
| 64 |
+
raise ValueError(f"{name} must be scalar or contain {width} values") from error
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def validate_dimension_partition(
|
| 68 |
+
groups: Iterable[tuple[str, Sequence[int]]],
|
| 69 |
+
dimension: int,
|
| 70 |
+
*,
|
| 71 |
+
label: str,
|
| 72 |
+
error_type: type[ErrorT] = ValueError,
|
| 73 |
+
) -> None:
|
| 74 |
+
"""Validate that named dimension groups form one exact non-overlapping partition."""
|
| 75 |
+
owners: dict[int, str] = {}
|
| 76 |
+
for name, dims in groups:
|
| 77 |
+
for index in dims:
|
| 78 |
+
if index < 0 or index >= dimension:
|
| 79 |
+
raise error_type(
|
| 80 |
+
f"{label} {index} used by {name!r} is outside input_dim"
|
| 81 |
+
)
|
| 82 |
+
if index in owners:
|
| 83 |
+
raise error_type(
|
| 84 |
+
f"{label} {index} belongs to both {owners[index]!r} and {name!r}"
|
| 85 |
+
)
|
| 86 |
+
owners[index] = name
|
| 87 |
+
missing = sorted(set(range(dimension)) - set(owners))
|
| 88 |
+
if missing:
|
| 89 |
+
raise error_type(f"{label}(s) {missing} are not assigned")
|
runtime/frontend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal frontend components for ActionCodec2."""
|
runtime/frontend/tokenizer.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Action-space-bound, arbitrary-horizon ActionCodec2 frontend."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import warnings
|
| 7 |
+
from collections.abc import Mapping, Sequence
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import TextIO
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from ..common._frontend_base import BaseActionTokenizer
|
| 15 |
+
from ..common._types import ActionIdxBatch, TokenIds
|
| 16 |
+
from ..routing.presets import PresetConfig
|
| 17 |
+
from ..routing.routing import ActionCodec2Router
|
| 18 |
+
from ..physical.semantic import resampled_horizon
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _numpy(value):
|
| 22 |
+
"""Convert a tensor to CPU float32 without changing NumPy input semantics."""
|
| 23 |
+
return (
|
| 24 |
+
value.detach().cpu().float().numpy()
|
| 25 |
+
if isinstance(value, torch.Tensor)
|
| 26 |
+
else value
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ActionCodec2Tokenizer(BaseActionTokenizer):
|
| 31 |
+
"""ActionCodec2 frontend bound to physical tensor semantics, not a horizon.
|
| 32 |
+
|
| 33 |
+
The selected action space fixes the meaning of every source column. Sequence
|
| 34 |
+
length remains unrestricted: encode and decode infer any positive ``T``
|
| 35 |
+
directly from the lossless token coverage.
|
| 36 |
+
|
| 37 |
+
Use :meth:`from_pretrained` for the normal one-call loading path. Built-in
|
| 38 |
+
and registered action spaces are discoverable through :meth:`print_action_spaces`
|
| 39 |
+
and are included in :func:`str` output.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
backend: Router containing the shared physical processors.
|
| 43 |
+
action_space: Registered action-space name to use for this frontend.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
encode_kwargs = frozenset({"fps", "current_state", "previous_first_order"})
|
| 47 |
+
|
| 48 |
+
def __init__(self, backend: ActionCodec2Router, *, action_space: str) -> None:
|
| 49 |
+
if not isinstance(backend, ActionCodec2Router):
|
| 50 |
+
raise TypeError(
|
| 51 |
+
f"ActionCodec2Tokenizer requires ActionCodec2Router, got {type(backend).__name__}"
|
| 52 |
+
)
|
| 53 |
+
preset = backend.preset(action_space)
|
| 54 |
+
self.action_tokenizer = backend
|
| 55 |
+
self._action_space = preset
|
| 56 |
+
self._action_vocab_size = int(backend.vocab_size)
|
| 57 |
+
self._new_action_tokens = list(backend.new_action_tokens)
|
| 58 |
+
self.action_dim = preset.input_dim
|
| 59 |
+
self._warned_default_fps = False
|
| 60 |
+
self.requires_previous_first_order = any(
|
| 61 |
+
physical.primitive_order == 2
|
| 62 |
+
and set(component.active_dims).intersection(physical.dims)
|
| 63 |
+
for component in preset.components
|
| 64 |
+
for physical in backend.profiles[
|
| 65 |
+
component.space
|
| 66 |
+
].processor.config.physical.components.values()
|
| 67 |
+
)
|
| 68 |
+
self._encoded_action_dim = sum(
|
| 69 |
+
len(component.active_dims) for component in preset.components
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
@classmethod
|
| 73 |
+
def from_pretrained(
|
| 74 |
+
cls,
|
| 75 |
+
pretrained_model_name_or_path: str | Path,
|
| 76 |
+
*,
|
| 77 |
+
action_space: str | Path | PresetConfig | Mapping[str, object],
|
| 78 |
+
**kwargs: object,
|
| 79 |
+
) -> ActionCodec2Tokenizer:
|
| 80 |
+
"""Load a local/Hub router and bind one explicit physical action space.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
pretrained_model_name_or_path: Local artifact directory or Hub repo ID.
|
| 84 |
+
action_space: Registered name, preset YAML path, ``PresetConfig``, or
|
| 85 |
+
strict preset mapping. Custom configs are registered before binding.
|
| 86 |
+
**kwargs: Standard Hub snapshot options such as ``revision``,
|
| 87 |
+
``cache_dir``, ``token``, and ``local_files_only``.
|
| 88 |
+
"""
|
| 89 |
+
router = ActionCodec2Router.from_pretrained(
|
| 90 |
+
pretrained_model_name_or_path, **kwargs
|
| 91 |
+
)
|
| 92 |
+
if isinstance(action_space, (PresetConfig, Mapping, Path)):
|
| 93 |
+
name = router.register_action_space(action_space).name
|
| 94 |
+
elif isinstance(action_space, str):
|
| 95 |
+
candidate = Path(action_space)
|
| 96 |
+
name = (
|
| 97 |
+
router.register_action_space(candidate).name
|
| 98 |
+
if candidate.is_file()
|
| 99 |
+
else action_space
|
| 100 |
+
)
|
| 101 |
+
else:
|
| 102 |
+
raise TypeError(
|
| 103 |
+
"action_space must be a registered name, PresetConfig, mapping, or YAML path"
|
| 104 |
+
)
|
| 105 |
+
return cls(router, action_space=name)
|
| 106 |
+
|
| 107 |
+
def __str__(self) -> str:
|
| 108 |
+
"""Show supported tensor layouts, physical units, and invocation requirements."""
|
| 109 |
+
return self._describe_action_spaces()
|
| 110 |
+
|
| 111 |
+
def _describe_action_spaces(self, action_dim: int | None = None) -> str:
|
| 112 |
+
return self.action_tokenizer.describe_action_spaces(
|
| 113 |
+
selected=self.action_space.name, action_dim=action_dim
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def action_space(self) -> PresetConfig:
|
| 118 |
+
"""The immutable tensor semantics selected for this tokenizer."""
|
| 119 |
+
return self._action_space
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def action_spaces(self) -> Mapping[str, PresetConfig]:
|
| 123 |
+
"""Every built-in or user-registered option in the loaded artifact."""
|
| 124 |
+
return self.action_tokenizer.action_spaces
|
| 125 |
+
|
| 126 |
+
def print_action_spaces(
|
| 127 |
+
self, *, action_dim: int | None = None, file: TextIO | None = None
|
| 128 |
+
) -> None:
|
| 129 |
+
"""Print all layouts or candidates matching ``action_dim`` without changing the binding."""
|
| 130 |
+
description = self._describe_action_spaces(action_dim)
|
| 131 |
+
print(description, file=sys.stdout if file is None else file)
|
| 132 |
+
|
| 133 |
+
def register_action_space(
|
| 134 |
+
self,
|
| 135 |
+
preset: PresetConfig | Mapping[str, object] | str | Path,
|
| 136 |
+
) -> PresetConfig:
|
| 137 |
+
"""Register a custom action space without changing this instance's binding."""
|
| 138 |
+
return self.action_tokenizer.register_action_space(preset)
|
| 139 |
+
|
| 140 |
+
def for_action_space(self, name: str) -> ActionCodec2Tokenizer:
|
| 141 |
+
"""Return a new tokenizer sharing weights but bound to another option."""
|
| 142 |
+
return type(self)(self.action_tokenizer, action_space=name)
|
| 143 |
+
|
| 144 |
+
def _source_fps(self, fps, preset=None):
|
| 145 |
+
preset = self.action_space if preset is None else preset
|
| 146 |
+
if fps is None:
|
| 147 |
+
if not self._warned_default_fps:
|
| 148 |
+
warnings.warn(
|
| 149 |
+
f"fps was omitted; assuming {preset.source_hz:g} Hz from action_space "
|
| 150 |
+
f"{preset.name!r}. Pass the actual fps explicitly to avoid changing action timing.",
|
| 151 |
+
UserWarning,
|
| 152 |
+
stacklevel=3,
|
| 153 |
+
)
|
| 154 |
+
self._warned_default_fps = True
|
| 155 |
+
return preset.source_hz
|
| 156 |
+
return fps
|
| 157 |
+
|
| 158 |
+
def first_order_template(self, batch_size: int) -> dict[str, np.ndarray]:
|
| 159 |
+
"""Explicit zero boundary templates, shaped {component: (B,D_active)}.
|
| 160 |
+
|
| 161 |
+
Zeros are valid only when the preceding canonical increment is known to
|
| 162 |
+
be zero. Otherwise replace them with measured/reconstructed values at the
|
| 163 |
+
action space's configured codec rate.
|
| 164 |
+
Only active second-order components are included; first order returns {}.
|
| 165 |
+
"""
|
| 166 |
+
if type(batch_size) is not int or batch_size < 1:
|
| 167 |
+
raise ValueError("batch_size must be a positive integer")
|
| 168 |
+
return {
|
| 169 |
+
c.name: np.zeros((batch_size, len(c.active_dims)), dtype=np.float32)
|
| 170 |
+
for c in self.action_space.components
|
| 171 |
+
if any(
|
| 172 |
+
p.primitive_order == 2 and set(c.active_dims).intersection(p.dims)
|
| 173 |
+
for p in self.action_tokenizer.profiles[
|
| 174 |
+
c.space
|
| 175 |
+
].processor.config.physical.components.values()
|
| 176 |
+
)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
def max_tokens_for_horizon(self, horizon: int, *, fps: float | None = None) -> int:
|
| 180 |
+
"""Return the token bound after converting a source chunk to the codec clock.
|
| 181 |
+
|
| 182 |
+
This helper describes encode input geometry. It does not store
|
| 183 |
+
``horizon`` in the token stream or pass it to decode.
|
| 184 |
+
"""
|
| 185 |
+
source_fps = self._source_fps(fps)
|
| 186 |
+
internal = resampled_horizon(
|
| 187 |
+
horizon, source_hz=source_fps, target_hz=self.action_space.target_hz
|
| 188 |
+
)
|
| 189 |
+
return internal * self._encoded_action_dim
|
| 190 |
+
|
| 191 |
+
def grammar(self, horizon: int, *, fps: float | None = None):
|
| 192 |
+
"""Build a grammar for the codec steps implied by source chunk geometry.
|
| 193 |
+
|
| 194 |
+
The grammar constrains token coverage only. When ``fps`` exceeds the
|
| 195 |
+
codec clock, several source row counts can map to the same codec
|
| 196 |
+
step count; decode therefore returns the unique canonical resampling
|
| 197 |
+
implied by the tokens and requested ``fps``, not a hidden source count.
|
| 198 |
+
"""
|
| 199 |
+
source_fps = self._source_fps(fps)
|
| 200 |
+
internal = resampled_horizon(
|
| 201 |
+
horizon, source_hz=source_fps, target_hz=self.action_space.target_hz
|
| 202 |
+
)
|
| 203 |
+
return self.action_tokenizer.grammar(
|
| 204 |
+
preset=self.action_space.name, horizon=internal
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
def __call__(
|
| 208 |
+
self,
|
| 209 |
+
action: torch.Tensor | np.ndarray,
|
| 210 |
+
*,
|
| 211 |
+
fps: float | None = None,
|
| 212 |
+
current_state: torch.Tensor | np.ndarray | None = None,
|
| 213 |
+
previous_first_order: Mapping[str, torch.Tensor | np.ndarray] | None = None,
|
| 214 |
+
encode_kwargs: dict | None = None,
|
| 215 |
+
) -> ActionIdxBatch:
|
| 216 |
+
"""Encode arbitrary-length ``(B,T,D)`` or ``(T,D)`` actions.
|
| 217 |
+
|
| 218 |
+
Second order requires ``previous_first_order`` mapping component names
|
| 219 |
+
to canonical ``(B,D_active)`` increments at the configured codec rate (``B=1`` for a single
|
| 220 |
+
trajectory). Absolute inputs also require ``current_state(B,D)``.
|
| 221 |
+
"""
|
| 222 |
+
kwargs = self._check_encode_kwargs(encode_kwargs)
|
| 223 |
+
direct = {
|
| 224 |
+
"fps": fps,
|
| 225 |
+
"current_state": current_state,
|
| 226 |
+
"previous_first_order": previous_first_order,
|
| 227 |
+
}
|
| 228 |
+
duplicates = sorted(
|
| 229 |
+
key for key, value in direct.items() if value is not None and key in kwargs
|
| 230 |
+
)
|
| 231 |
+
if duplicates:
|
| 232 |
+
raise ValueError(f"encode arguments provided twice: {duplicates}")
|
| 233 |
+
kwargs.update(
|
| 234 |
+
{key: value for key, value in direct.items() if value is not None}
|
| 235 |
+
)
|
| 236 |
+
reference = kwargs.get("current_state")
|
| 237 |
+
if self.action_space.input == "absolute" and reference is None:
|
| 238 |
+
raise ValueError(
|
| 239 |
+
f"action space {self.action_space.name!r} is absolute; encode requires "
|
| 240 |
+
"current_state with shape [B, D] to preserve the first transition"
|
| 241 |
+
)
|
| 242 |
+
if self.action_space.input == "delta" and reference is not None:
|
| 243 |
+
raise ValueError(
|
| 244 |
+
f"action space {self.action_space.name!r} is delta; omit current_state"
|
| 245 |
+
)
|
| 246 |
+
values = torch.as_tensor(action)
|
| 247 |
+
if values.ndim == 2:
|
| 248 |
+
values = values.unsqueeze(0)
|
| 249 |
+
return self._encode_action_indices(values, encode_kwargs=kwargs)
|
| 250 |
+
|
| 251 |
+
encode = __call__
|
| 252 |
+
|
| 253 |
+
def decode(
|
| 254 |
+
self,
|
| 255 |
+
action_token_ids: Sequence[TokenIds],
|
| 256 |
+
*,
|
| 257 |
+
fps: float | None = None,
|
| 258 |
+
current_state: torch.Tensor | np.ndarray | None = None,
|
| 259 |
+
previous_first_order: Mapping[str, torch.Tensor | np.ndarray] | None = None,
|
| 260 |
+
) -> torch.Tensor:
|
| 261 |
+
"""Decode a batch, deriving sequence length from tokens and output rate.
|
| 262 |
+
|
| 263 |
+
At the codec rate, token coverage determines ``T`` exactly. Other output rates
|
| 264 |
+
deterministically resample that decoded path. No source horizon is
|
| 265 |
+
accepted because downsampling cannot preserve an arbitrary original row
|
| 266 |
+
count without encoding extra metadata.
|
| 267 |
+
``previous_first_order`` maps component names to canonical
|
| 268 |
+
``(B,D_active)`` codec-rate increments; second order requires the same
|
| 269 |
+
boundary as encoding, independently of ``current_state(B,D)``.
|
| 270 |
+
"""
|
| 271 |
+
return self._decode_token_ids_to_actions_batch(
|
| 272 |
+
action_token_ids,
|
| 273 |
+
action_dim=self.action_dim,
|
| 274 |
+
decode_kwargs={
|
| 275 |
+
"fps": fps,
|
| 276 |
+
"current_state": current_state,
|
| 277 |
+
"previous_first_order": previous_first_order,
|
| 278 |
+
},
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
@staticmethod
|
| 282 |
+
def _numpy_boundary(previous_first_order):
|
| 283 |
+
if previous_first_order is None:
|
| 284 |
+
return None
|
| 285 |
+
if not isinstance(previous_first_order, Mapping):
|
| 286 |
+
raise ValueError(
|
| 287 |
+
"previous_first_order must map component names to [B,D_active] arrays"
|
| 288 |
+
)
|
| 289 |
+
return {name: _numpy(value) for name, value in previous_first_order.items()}
|
| 290 |
+
|
| 291 |
+
def final_first_order(
|
| 292 |
+
self,
|
| 293 |
+
action_token_ids: Sequence[TokenIds],
|
| 294 |
+
*,
|
| 295 |
+
previous_first_order: Mapping[str, torch.Tensor | np.ndarray],
|
| 296 |
+
executed_steps: int | None = None,
|
| 297 |
+
) -> dict[str, np.ndarray]:
|
| 298 |
+
"""Return reconstructed component states after a canonical-step prefix.
|
| 299 |
+
|
| 300 |
+
Results are ``[B,D_active]`` arrays at the configured codec rate,
|
| 301 |
+
suitable for the next encode/decode call. ``executed_steps`` counts
|
| 302 |
+
codec steps.
|
| 303 |
+
"""
|
| 304 |
+
return self.action_tokenizer.final_first_order(
|
| 305 |
+
[self._checked_decode_row(row).tolist() for row in action_token_ids],
|
| 306 |
+
preset=self.action_space.name,
|
| 307 |
+
previous_first_order=self._numpy_boundary(previous_first_order),
|
| 308 |
+
executed_steps=executed_steps,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
def _encode_action_indices(
|
| 312 |
+
self, action: torch.Tensor, encode_kwargs: dict | None = None
|
| 313 |
+
) -> ActionIdxBatch:
|
| 314 |
+
"""Encode finite ``(B,T,D)`` actions without fixing ``T`` at construction."""
|
| 315 |
+
kwargs = dict(encode_kwargs or {})
|
| 316 |
+
fps = float(self._source_fps(kwargs.get("fps")))
|
| 317 |
+
current_state = _numpy(kwargs.get("current_state"))
|
| 318 |
+
values = _numpy(action)
|
| 319 |
+
if values.ndim != 3 or values.shape[2] != self.action_space.input_dim:
|
| 320 |
+
raise ValueError(
|
| 321 |
+
f"actions must have shape [B, T_source, {self.action_space.input_dim}]"
|
| 322 |
+
)
|
| 323 |
+
return self.action_tokenizer.encode_batch(
|
| 324 |
+
values,
|
| 325 |
+
preset=self.action_space.name,
|
| 326 |
+
fps=fps,
|
| 327 |
+
current_state=current_state,
|
| 328 |
+
previous_first_order=self._numpy_boundary(
|
| 329 |
+
kwargs.get("previous_first_order")
|
| 330 |
+
),
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
def _decode_action_indices(
|
| 334 |
+
self,
|
| 335 |
+
action_indices: ActionIdxBatch,
|
| 336 |
+
*,
|
| 337 |
+
time_horizon: int | None = None,
|
| 338 |
+
action_dim: int | None = None,
|
| 339 |
+
decode_kwargs: dict | None = None,
|
| 340 |
+
) -> tuple[np.ndarray, list[set[str]]]:
|
| 341 |
+
"""Decode rows to the selected preset's original ``(B,T,D)`` layout."""
|
| 342 |
+
if time_horizon is not None:
|
| 343 |
+
raise ValueError(
|
| 344 |
+
"ActionCodec2 decode infers sequence length from tokens; "
|
| 345 |
+
"do not provide time_horizon"
|
| 346 |
+
)
|
| 347 |
+
if action_dim is not None and int(action_dim) != self.action_dim:
|
| 348 |
+
raise ValueError(
|
| 349 |
+
f"action_dim={action_dim} disagrees with action space "
|
| 350 |
+
f"{self.action_space.name!r} (D={self.action_dim})"
|
| 351 |
+
)
|
| 352 |
+
kwargs = dict(decode_kwargs or {})
|
| 353 |
+
unknown = sorted(set(kwargs) - self.encode_kwargs)
|
| 354 |
+
if unknown:
|
| 355 |
+
raise ValueError(
|
| 356 |
+
f"ActionCodec2Tokenizer does not consume decode_kwargs {unknown}"
|
| 357 |
+
)
|
| 358 |
+
raw_fps = kwargs.get("fps")
|
| 359 |
+
fps = float(self._source_fps(raw_fps))
|
| 360 |
+
current_state = _numpy(kwargs.get("current_state"))
|
| 361 |
+
if self.action_space.input == "absolute" and current_state is None:
|
| 362 |
+
raise ValueError(
|
| 363 |
+
f"action space {self.action_space.name!r} is absolute; decode requires "
|
| 364 |
+
"current_state with shape [B, D] to restore its external reference"
|
| 365 |
+
)
|
| 366 |
+
if self.action_space.input == "delta" and current_state is not None:
|
| 367 |
+
raise ValueError(
|
| 368 |
+
f"action space {self.action_space.name!r} is delta; omit current_state "
|
| 369 |
+
"to decode in the registered input semantics"
|
| 370 |
+
)
|
| 371 |
+
decoded = self.action_tokenizer.decode_batch(
|
| 372 |
+
action_indices,
|
| 373 |
+
preset=self.action_space.name,
|
| 374 |
+
fps=fps,
|
| 375 |
+
current_state=current_state,
|
| 376 |
+
previous_first_order=self._numpy_boundary(
|
| 377 |
+
kwargs.get("previous_first_order")
|
| 378 |
+
),
|
| 379 |
+
)
|
| 380 |
+
actions = self._restore_source_layout(decoded)
|
| 381 |
+
return actions, [set() for _ in range(actions.shape[0])]
|
| 382 |
+
|
| 383 |
+
def _restore_source_layout(self, decoded: Mapping[str, np.ndarray]) -> np.ndarray:
|
| 384 |
+
"""Place component outputs back into their registered source columns."""
|
| 385 |
+
first = next(iter(decoded.values()))
|
| 386 |
+
batch, steps = first.shape[:2]
|
| 387 |
+
if any(values.shape[:2] != (batch, steps) for values in decoded.values()):
|
| 388 |
+
raise RuntimeError(
|
| 389 |
+
"decoded components disagree on batch or sequence length"
|
| 390 |
+
)
|
| 391 |
+
actions = np.empty((batch, steps, self.action_dim), dtype=np.float32)
|
| 392 |
+
for component in self.action_space.components:
|
| 393 |
+
values = decoded[component.name]
|
| 394 |
+
motion_width = len(component.source_dims)
|
| 395 |
+
actions[:, :, component.source_dims] = values[:, :, :motion_width]
|
| 396 |
+
if component.gripper_dim is not None:
|
| 397 |
+
actions[:, :, component.gripper_dim] = values[:, :, motion_width]
|
| 398 |
+
return actions
|
runtime/hub_acceleration.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Install the shared C++ kernels in a portable ActionCodec2 runtime.
|
| 2 |
+
|
| 3 |
+
This module is copied into exported Hub artifacts. It imports runtime classes
|
| 4 |
+
by their private package name so each artifact retains its own Python fallback.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import functools
|
| 10 |
+
import importlib
|
| 11 |
+
from numbers import Integral
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_native():
|
| 18 |
+
"""Return installed portable or VLA kernels, with a fork-safe thread budget."""
|
| 19 |
+
if os.environ.get("ACTIONCODEC2_NO_NATIVE"):
|
| 20 |
+
return None
|
| 21 |
+
for name in (
|
| 22 |
+
"actioncodec2_kernels._actioncodec2_native",
|
| 23 |
+
"actioncodec2.tokenizers._actioncodec2_native",
|
| 24 |
+
):
|
| 25 |
+
try:
|
| 26 |
+
native = importlib.import_module(name)
|
| 27 |
+
except ImportError:
|
| 28 |
+
continue
|
| 29 |
+
_configure(native)
|
| 30 |
+
return native
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@functools.cache
|
| 35 |
+
def _configure(native):
|
| 36 |
+
configured = os.environ.get("ACTIONCODEC2_NUM_THREADS")
|
| 37 |
+
if configured is not None:
|
| 38 |
+
if not configured.isdigit() or int(configured) < 1:
|
| 39 |
+
raise ValueError("ACTIONCODEC2_NUM_THREADS must be a positive integer")
|
| 40 |
+
native.set_thread_budget(int(configured))
|
| 41 |
+
else:
|
| 42 |
+
native.set_thread_budget(0)
|
| 43 |
+
if hasattr(os, "register_at_fork"):
|
| 44 |
+
os.register_at_fork(after_in_child=lambda: native.set_thread_budget(1))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def install(package: str) -> None:
|
| 48 |
+
"""Accelerate an artifact while preserving its profiles and public contracts."""
|
| 49 |
+
backend = importlib.import_module(f"{package}.backend.native_backend")
|
| 50 |
+
if getattr(backend, "_portable_acceleration", False):
|
| 51 |
+
return
|
| 52 |
+
backend._portable_acceleration = True
|
| 53 |
+
backend.native = load_native()
|
| 54 |
+
semantic = importlib.import_module(f"{package}.physical.semantic")
|
| 55 |
+
presets = importlib.import_module(f"{package}.routing.presets")
|
| 56 |
+
trajectory = importlib.import_module(f"{package}.physical.trajectory")
|
| 57 |
+
physical = importlib.import_module(f"{package}.physical.physical_processor")
|
| 58 |
+
validation = importlib.import_module(f"{package}.common._validation")
|
| 59 |
+
|
| 60 |
+
def native_now():
|
| 61 |
+
return backend.native if backend.have_native() else None
|
| 62 |
+
|
| 63 |
+
def wrap_pose(name, kernel, width):
|
| 64 |
+
original = getattr(semantic, name)
|
| 65 |
+
|
| 66 |
+
@functools.wraps(original)
|
| 67 |
+
def wrapped(values):
|
| 68 |
+
native = native_now()
|
| 69 |
+
array = np.asarray(values)
|
| 70 |
+
if (native is None or array.dtype != np.float32 or array.ndim != 2
|
| 71 |
+
or array.shape[1] != width or len(array) < 1
|
| 72 |
+
or not np.all(np.isfinite(array))):
|
| 73 |
+
return original(values)
|
| 74 |
+
return getattr(native, kernel)(array[None])[0]
|
| 75 |
+
|
| 76 |
+
setattr(semantic, name, wrapped)
|
| 77 |
+
if hasattr(presets, name):
|
| 78 |
+
setattr(presets, name, wrapped)
|
| 79 |
+
|
| 80 |
+
for name, kernel, width in (
|
| 81 |
+
("integrate_eef_deltas", "integrate_eef_deltas_batch", 6),
|
| 82 |
+
("eef_pose_absolute", "eef_pose_absolute_batch", 7),
|
| 83 |
+
("eef_pose_deltas", "eef_pose_deltas_batch", 7),
|
| 84 |
+
):
|
| 85 |
+
wrap_pose(name, kernel, width)
|
| 86 |
+
|
| 87 |
+
original_resample = semantic.resample_uniform
|
| 88 |
+
|
| 89 |
+
@functools.wraps(original_resample)
|
| 90 |
+
def resample(values, *, source_hz, target_hz, kind="linear", target_length=None,
|
| 91 |
+
method="linear"):
|
| 92 |
+
native = native_now()
|
| 93 |
+
array = np.asarray(values)
|
| 94 |
+
if (native is None or array.dtype != np.float32 or array.ndim != 2
|
| 95 |
+
or len(array) < 1 or not np.all(np.isfinite(array))
|
| 96 |
+
or (target_length is not None and (
|
| 97 |
+
isinstance(target_length, bool) or not isinstance(target_length, Integral)
|
| 98 |
+
or target_length < 1))):
|
| 99 |
+
return original_resample(values, source_hz=source_hz, target_hz=target_hz,
|
| 100 |
+
kind=kind, target_length=target_length, method=method)
|
| 101 |
+
return native.resample_uniform_batch(
|
| 102 |
+
array[None], source_hz, target_hz, kind,
|
| 103 |
+
-1 if target_length is None else target_length, method,
|
| 104 |
+
)[0]
|
| 105 |
+
|
| 106 |
+
semantic.resample_uniform = presets.resample_uniform = resample
|
| 107 |
+
original_canonicalize = presets.PresetConfig.canonicalize
|
| 108 |
+
|
| 109 |
+
@functools.wraps(original_canonicalize)
|
| 110 |
+
def canonicalize(self, values, *, source_hz=None, current_state=None):
|
| 111 |
+
native = native_now()
|
| 112 |
+
array = np.asarray(values, dtype=np.float32)
|
| 113 |
+
if (native is None or self.input != "delta" or current_state is not None
|
| 114 |
+
or array.ndim != 3 or min(array.shape) < 1
|
| 115 |
+
or array.shape[2] != self.input_dim or not np.all(np.isfinite(array))
|
| 116 |
+
or any(c.space == "eef" and c.rotation_format != "rotation_vector"
|
| 117 |
+
for c in self.components)):
|
| 118 |
+
return original_canonicalize(self, values, source_hz=source_hz,
|
| 119 |
+
current_state=current_state)
|
| 120 |
+
rate = self.source_hz if source_hz is None else source_hz
|
| 121 |
+
output = {}
|
| 122 |
+
for component in self.components:
|
| 123 |
+
if component.space == "eef":
|
| 124 |
+
motion = np.concatenate((array[..., component.position_dims],
|
| 125 |
+
array[..., component.rotation_dims]), axis=2)
|
| 126 |
+
pose = native.integrate_eef_deltas_batch(motion)
|
| 127 |
+
sampled = native.resample_uniform_batch(
|
| 128 |
+
pose, rate, self.target_hz, "pose_xyzw", -1, self.interpolation
|
| 129 |
+
)
|
| 130 |
+
motion = native.eef_pose_absolute_batch(sampled)
|
| 131 |
+
else:
|
| 132 |
+
motion = np.cumsum(array[..., component.joint_dims], axis=1,
|
| 133 |
+
dtype=np.float32)
|
| 134 |
+
motion = native.resample_uniform_batch(
|
| 135 |
+
motion, rate, self.target_hz, "linear", -1, self.interpolation
|
| 136 |
+
)
|
| 137 |
+
if component.gripper_dim is not None:
|
| 138 |
+
gripper = native.resample_uniform_batch(
|
| 139 |
+
array[..., component.gripper_dim:component.gripper_dim + 1],
|
| 140 |
+
rate, self.target_hz, "hold", -1, self.interpolation,
|
| 141 |
+
)
|
| 142 |
+
gripper = semantic.binarize_gripper_actions(
|
| 143 |
+
gripper.reshape(-1, 1), threshold=component.gripper_threshold,
|
| 144 |
+
open_is_high=component.gripper_open_is_high,
|
| 145 |
+
).reshape(gripper.shape)
|
| 146 |
+
motion = np.concatenate((motion, gripper), axis=2)
|
| 147 |
+
output[component.name] = [
|
| 148 |
+
trajectory.ActiveTrajectory(row, component.active_dims) for row in motion
|
| 149 |
+
]
|
| 150 |
+
return output
|
| 151 |
+
|
| 152 |
+
presets.PresetConfig.canonicalize = canonicalize
|
| 153 |
+
original_encode = physical.ActionCodec2Processor.encode_batch
|
| 154 |
+
original_decode = physical.ActionCodec2Processor.decode_batch
|
| 155 |
+
|
| 156 |
+
def serializer(self):
|
| 157 |
+
native = native_now()
|
| 158 |
+
if native is None or self.serializer.scheme != "set_bpe":
|
| 159 |
+
return None
|
| 160 |
+
if not hasattr(self, "_portable_setbpe"):
|
| 161 |
+
saved = self.serializer
|
| 162 |
+
self._portable_setbpe = native.SetBPE(
|
| 163 |
+
saved.bins, saved.lookahead, saved.spatial_merges, saved.temporal_merges
|
| 164 |
+
)
|
| 165 |
+
return self._portable_setbpe
|
| 166 |
+
|
| 167 |
+
@functools.wraps(original_encode)
|
| 168 |
+
def encode_batch(self, actions, *, active_dims=None, previous_first_order=None):
|
| 169 |
+
codec = serializer(self)
|
| 170 |
+
if codec is None:
|
| 171 |
+
return original_encode(self, actions, active_dims=active_dims,
|
| 172 |
+
previous_first_order=previous_first_order)
|
| 173 |
+
values = np.asarray(actions, dtype=np.float32)
|
| 174 |
+
if values.ndim != 3 or min(values.shape) < 1:
|
| 175 |
+
raise ValueError("actions must have non-empty shape [B, T, D_active]")
|
| 176 |
+
dims = validation.active_dimensions(active_dims, self.action_dim)
|
| 177 |
+
if len(dims) != values.shape[2]:
|
| 178 |
+
raise ValueError("active_dims must have one entry per action coordinate")
|
| 179 |
+
dense = np.zeros((*values.shape[:2], self.action_dim), dtype=np.float32)
|
| 180 |
+
dense[..., dims] = values
|
| 181 |
+
previous = self._dense_previous_first_order(previous_first_order, dims, len(values))
|
| 182 |
+
cells = self.quantize_batch(dense, previous_first_order=previous)
|
| 183 |
+
absent = np.ones(self.action_dim, dtype=bool)
|
| 184 |
+
absent[list(dims)] = False
|
| 185 |
+
cells[..., absent] = -1
|
| 186 |
+
return codec.encode_batch(cells)
|
| 187 |
+
|
| 188 |
+
@functools.wraps(original_decode)
|
| 189 |
+
def decode_batch(self, streams, *, active_dims=None, previous_first_order=None):
|
| 190 |
+
codec = serializer(self)
|
| 191 |
+
if codec is None:
|
| 192 |
+
return original_decode(self, streams, active_dims=active_dims,
|
| 193 |
+
previous_first_order=previous_first_order)
|
| 194 |
+
dims = validation.active_dimensions(active_dims, self.action_dim)
|
| 195 |
+
checked = []
|
| 196 |
+
for stream in streams:
|
| 197 |
+
row = []
|
| 198 |
+
for token in stream:
|
| 199 |
+
if isinstance(token, bool) or not isinstance(token, Integral):
|
| 200 |
+
raise ValueError("tokens must be integers")
|
| 201 |
+
if not 0 <= int(token) < self.vocab_size:
|
| 202 |
+
raise ValueError("token is outside the vocabulary")
|
| 203 |
+
row.append(int(token))
|
| 204 |
+
checked.append(row)
|
| 205 |
+
cells = codec.decode_batch(checked, self.action_dim, sum(1 << d for d in dims))
|
| 206 |
+
previous = self._dense_previous_first_order(previous_first_order, dims, len(checked))
|
| 207 |
+
decoded = self.physical.decode_batch(
|
| 208 |
+
self._fill_inactive_cells(cells), previous_first_order=previous
|
| 209 |
+
)
|
| 210 |
+
return np.take(decoded, dims, axis=2)
|
| 211 |
+
|
| 212 |
+
physical.ActionCodec2Processor.encode_batch = encode_batch
|
| 213 |
+
physical.ActionCodec2Processor.decode_batch = decode_batch
|
| 214 |
+
|
| 215 |
+
exporting = importlib.import_module(f"{package}.integration.exporting")
|
| 216 |
+
for name in ("hub_acceleration.py", "hub_entry.py"):
|
| 217 |
+
if name not in exporting.MODULE_FILES:
|
| 218 |
+
exporting.MODULE_FILES.append(name)
|
| 219 |
+
original_export = exporting.ArtifactExporter.export
|
| 220 |
+
|
| 221 |
+
@functools.wraps(original_export)
|
| 222 |
+
def export(self):
|
| 223 |
+
import shutil
|
| 224 |
+
|
| 225 |
+
files = original_export(self)
|
| 226 |
+
source = self.package_root.parent
|
| 227 |
+
for name in ("setup.py", "pyproject.toml", "LICENSE"):
|
| 228 |
+
path = source / name
|
| 229 |
+
target = self.output / name
|
| 230 |
+
if path.is_file() and path.resolve() != target.resolve():
|
| 231 |
+
shutil.copyfile(path, target)
|
| 232 |
+
files.append(str(target))
|
| 233 |
+
kernels = source / "actioncodec2_kernels"
|
| 234 |
+
if kernels.is_dir() and kernels.resolve() != (self.output / kernels.name).resolve():
|
| 235 |
+
shutil.copytree(kernels, self.output / kernels.name, dirs_exist_ok=True,
|
| 236 |
+
ignore=shutil.ignore_patterns("__pycache__", "*.so", "*.pyc"))
|
| 237 |
+
return files
|
| 238 |
+
|
| 239 |
+
exporting.ArtifactExporter.export = export
|
runtime/hub_entry.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small Hugging Face entry point for a grouped ActionCodec2 artifact.
|
| 2 |
+
|
| 3 |
+
Transformers copies only top-level Python files from a Hub repository into its
|
| 4 |
+
dynamic-module cache. This entry point locates the artifact, then loads the
|
| 5 |
+
versioned runtime stored in ``runtime/`` without installing another package.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import importlib
|
| 12 |
+
import importlib.util
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ActionCodec2:
|
| 18 |
+
"""Load the processor implementation bundled with a pretrained artifact."""
|
| 19 |
+
|
| 20 |
+
@classmethod
|
| 21 |
+
def register_for_auto_class(cls, auto_class="AutoProcessor"):
|
| 22 |
+
"""Satisfy Transformers' dynamic class hook for this thin loader."""
|
| 23 |
+
return cls
|
| 24 |
+
|
| 25 |
+
@classmethod
|
| 26 |
+
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
|
| 27 |
+
source = Path(pretrained_model_name_or_path)
|
| 28 |
+
subfolder = str(kwargs.pop("subfolder", ""))
|
| 29 |
+
action_space = kwargs.pop("action_space", None)
|
| 30 |
+
kwargs.pop("_from_auto", None)
|
| 31 |
+
kwargs.pop("trust_remote_code", None)
|
| 32 |
+
allowed = {
|
| 33 |
+
"cache_dir",
|
| 34 |
+
"force_download",
|
| 35 |
+
"local_files_only",
|
| 36 |
+
"token",
|
| 37 |
+
"revision",
|
| 38 |
+
"repo_type",
|
| 39 |
+
}
|
| 40 |
+
unknown = sorted(set(kwargs) - allowed)
|
| 41 |
+
if unknown:
|
| 42 |
+
raise TypeError(
|
| 43 |
+
f"unsupported from_pretrained keyword(s): {', '.join(unknown)}"
|
| 44 |
+
)
|
| 45 |
+
if source.is_dir():
|
| 46 |
+
root = source / subfolder
|
| 47 |
+
else:
|
| 48 |
+
from huggingface_hub import snapshot_download
|
| 49 |
+
|
| 50 |
+
prefix = f"{subfolder.rstrip('/')}/" if subfolder else ""
|
| 51 |
+
patterns = [
|
| 52 |
+
"config.json",
|
| 53 |
+
"processor_config.json",
|
| 54 |
+
"router_config.yaml",
|
| 55 |
+
"profiles/**",
|
| 56 |
+
"runtime/**",
|
| 57 |
+
"actioncodec2_kernels/**",
|
| 58 |
+
"setup.py", "pyproject.toml", "LICENSE",
|
| 59 |
+
"README.md",
|
| 60 |
+
"requirements.txt",
|
| 61 |
+
"fit_report.json",
|
| 62 |
+
]
|
| 63 |
+
snapshot = snapshot_download(
|
| 64 |
+
repo_id=str(pretrained_model_name_or_path),
|
| 65 |
+
allow_patterns=[prefix + pattern for pattern in patterns],
|
| 66 |
+
**kwargs,
|
| 67 |
+
)
|
| 68 |
+
root = Path(snapshot) / subfolder
|
| 69 |
+
|
| 70 |
+
runtime = root / "runtime"
|
| 71 |
+
package_file = runtime / "__init__.py"
|
| 72 |
+
if not package_file.is_file():
|
| 73 |
+
raise FileNotFoundError(
|
| 74 |
+
f"ActionCodec2 runtime is missing from {root}; copy the whole artifact"
|
| 75 |
+
)
|
| 76 |
+
identity = hashlib.sha256(str(runtime.resolve()).encode()).hexdigest()[:16]
|
| 77 |
+
package_name = f"_actioncodec2_artifact_{identity}"
|
| 78 |
+
if package_name not in sys.modules:
|
| 79 |
+
spec = importlib.util.spec_from_file_location(
|
| 80 |
+
package_name, package_file, submodule_search_locations=[str(runtime)]
|
| 81 |
+
)
|
| 82 |
+
if spec is None or spec.loader is None:
|
| 83 |
+
raise ImportError(f"cannot load ActionCodec2 runtime from {runtime}")
|
| 84 |
+
package = importlib.util.module_from_spec(spec)
|
| 85 |
+
sys.modules[package_name] = package
|
| 86 |
+
try:
|
| 87 |
+
spec.loader.exec_module(package)
|
| 88 |
+
except BaseException:
|
| 89 |
+
del sys.modules[package_name]
|
| 90 |
+
raise
|
| 91 |
+
processor = importlib.import_module(f"{package_name}.processing_actioncodec2")
|
| 92 |
+
acceleration = importlib.import_module(f"{package_name}.hub_acceleration")
|
| 93 |
+
acceleration.install(package_name)
|
| 94 |
+
return processor.ActionCodec2.from_pretrained(root, action_space=action_space)
|
runtime/integration/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal integration components for ActionCodec2."""
|
runtime/integration/exporting.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Write the portable runtime and user guide for a saved ActionCodec2 artifact."""
|
| 2 |
+
|
| 3 |
+
import shutil
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# Files included in a portable runtime artifact.
|
| 7 |
+
MODULE_FILES: list[str]
|
| 8 |
+
SUPPORT_FILES: dict[str, str]
|
| 9 |
+
USER_GUIDE: str
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ArtifactExporter:
|
| 13 |
+
"""Copy portable code alongside numerical artifacts.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
directory: Destination directory.
|
| 17 |
+
source_directory: Existing artifact or package directory used as a
|
| 18 |
+
fallback for runtime files and native resources.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self, directory: str | Path, *, source_directory: str | Path | None = None
|
| 23 |
+
) -> None:
|
| 24 |
+
self.output = Path(directory)
|
| 25 |
+
self.package_root = Path(__file__).resolve().parent.parent
|
| 26 |
+
self.source = Path(source_directory) if source_directory else self.package_root
|
| 27 |
+
|
| 28 |
+
def export(self) -> list[str]:
|
| 29 |
+
self.output.mkdir(parents=True, exist_ok=True)
|
| 30 |
+
files = []
|
| 31 |
+
runtime = self.output / "runtime"
|
| 32 |
+
runtime.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
for name in MODULE_FILES:
|
| 34 |
+
# Copy the current implementation so an artifact can be moved alone.
|
| 35 |
+
source = self.package_root / name
|
| 36 |
+
if not source.is_file():
|
| 37 |
+
source = self.source / name
|
| 38 |
+
target = runtime / name
|
| 39 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
if source.resolve() != target.resolve():
|
| 41 |
+
shutil.copyfile(source, target)
|
| 42 |
+
files.append(str(target))
|
| 43 |
+
(runtime / "__init__.py").write_text(
|
| 44 |
+
'"""Private inference runtime bundled with this ActionCodec2 artifact."""\n',
|
| 45 |
+
encoding="utf-8",
|
| 46 |
+
)
|
| 47 |
+
entry = self.package_root / "hub_entry.py"
|
| 48 |
+
if not entry.is_file():
|
| 49 |
+
entry = self.source / "runtime" / "hub_entry.py"
|
| 50 |
+
target = self.output / "processing_actioncodec2.py"
|
| 51 |
+
if entry.resolve() != target.resolve():
|
| 52 |
+
shutil.copyfile(entry, target)
|
| 53 |
+
files.append(str(target))
|
| 54 |
+
# A saved artifact needs runtime dependencies, but not training CLIs or
|
| 55 |
+
# package-build metadata.
|
| 56 |
+
for name in ("requirements.txt",):
|
| 57 |
+
content = SUPPORT_FILES[name]
|
| 58 |
+
target = self.output / name
|
| 59 |
+
target.write_text(content, encoding="utf-8")
|
| 60 |
+
files.append(str(target))
|
| 61 |
+
if not (self.output / "README.md").exists():
|
| 62 |
+
(self.output / "README.md").write_text(USER_GUIDE, encoding="utf-8")
|
| 63 |
+
return files
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
USER_GUIDE = """# Loading this saved artifact
|
| 67 |
+
|
| 68 |
+
This directory is a complete, portable ActionCodec2 artifact. Install its runtime dependencies, then point Transformers at this directory. Keep every file and subdirectory together when copying or uploading it.
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
python -m pip install -r requirements.txt
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
```python
|
| 75 |
+
from transformers import AutoProcessor
|
| 76 |
+
|
| 77 |
+
codec = AutoProcessor.from_pretrained(
|
| 78 |
+
"ZibinDong/ActionCodec2-1st-order",
|
| 79 |
+
trust_remote_code=True,
|
| 80 |
+
)
|
| 81 |
+
print(codec)
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
# ActionCodec2
|
| 85 |
+
|
| 86 |
+
ActionCodec2 converts continuous robot actions into integer token IDs and decodes
|
| 87 |
+
them to the same column layout. Physical quantization is lossy; token
|
| 88 |
+
serialization is exact for a fixed fitted codec. The Python `ActionCodec2` class
|
| 89 |
+
is the main interface. Its input is an action tensor, not a text prompt.
|
| 90 |
+
|
| 91 |
+
Python 3.10+ is required.
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
python -m pip install .
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## Choose an action space before encoding
|
| 98 |
+
|
| 99 |
+
Action shape alone cannot identify units or column meanings. Confirm the action
|
| 100 |
+
order, units, rotation representation, absolute versus delta convention,
|
| 101 |
+
gripper convention, and sampling rate from your data source. `D=7` can match
|
| 102 |
+
several different layouts.
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
from actioncodec2 import ActionCodec2
|
| 106 |
+
|
| 107 |
+
# No artifact is needed to inspect the built-in layout definitions.
|
| 108 |
+
print(ActionCodec2.builtin_action_spaces().keys())
|
| 109 |
+
|
| 110 |
+
ARTIFACT = "ZibinDong/ActionCodec2-1st-order" # or a Hub repository ID
|
| 111 |
+
print(ActionCodec2.describe_pretrained(ARTIFACT, action_dim=7))
|
| 112 |
+
codec = ActionCodec2.from_pretrained(ARTIFACT, action_space="single_eef_delta")
|
| 113 |
+
print(codec) # All registered layouts, column meanings, units, and requirements
|
| 114 |
+
codec.print_action_spaces(action_dim=7) # Candidates only; never auto-selects
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
A saved artifact can include its selected `action_space`; in that case
|
| 118 |
+
`ActionCodec2.from_pretrained(ARTIFACT)` restores it. For an unbound router,
|
| 119 |
+
`action_space` is required. You can inspect `codec.action_space` for the selected
|
| 120 |
+
contract and `codec.action_spaces` for every registered contract. Use
|
| 121 |
+
`codec.for_action_space("NAME")` to create another binding over the same fitted
|
| 122 |
+
profiles. The original object keeps its binding.
|
| 123 |
+
|
| 124 |
+
To define a layout with explicit semantics, register a preset mapping or YAML
|
| 125 |
+
file. Start from a known contract only when its column meanings really match:
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
preset = codec.action_space.to_dict()
|
| 129 |
+
preset["name"] = "my_30hz_eef_delta"
|
| 130 |
+
preset["source_hz"] = 30.0
|
| 131 |
+
codec.register_action_space(preset)
|
| 132 |
+
codec = codec.for_action_space("my_30hz_eef_delta")
|
| 133 |
+
codec.save_pretrained("./my-bound-codec") # Persists the custom layout and binding
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
Changing `source_hz` does not change the meaning of action columns. For a new
|
| 137 |
+
robot or a different physical tensor, define the complete preset instead of
|
| 138 |
+
renaming an existing layout.
|
| 139 |
+
|
| 140 |
+
## Encode and decode
|
| 141 |
+
|
| 142 |
+
```python
|
| 143 |
+
import numpy as np
|
| 144 |
+
|
| 145 |
+
actions = np.asarray(your_actions, dtype=np.float32) # (T, D) or (B, T, D)
|
| 146 |
+
tokens = codec.encode(actions, fps=30) # list[list[int]], one row per episode
|
| 147 |
+
reconstructed = codec.decode(tokens, fps=30) # CPU torch.float32, (B, T_out, D)
|
| 148 |
+
features = codec(actions, fps=30) # Transformers BatchFeature with input_ids
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
`T` can vary across calls. For unequal episode lengths, encode episodes one at a
|
| 152 |
+
time; a single tensor batch must have equal `T`. Pass the real sampling rate with
|
| 153 |
+
`fps`. If omitted, the codec warns once and assumes the selected preset's
|
| 154 |
+
`source_hz`. Decoding at a different rate resamples the reconstructed path, so
|
| 155 |
+
`T_out` may differ from the original `T`. Physical quantization and time
|
| 156 |
+
resampling prevent exact numeric round trips.
|
| 157 |
+
|
| 158 |
+
For independently generated rows with different decoded horizons, use
|
| 159 |
+
`codec.batch_decode(rows, fps=30)`. It returns a list of tensors shaped
|
| 160 |
+
`(1, T_i, D)`. With `errors="return"`, an invalid row becomes a `ValueError` in
|
| 161 |
+
its list position without aborting other rows. Ordinary `decode` expects a
|
| 162 |
+
regular batch with the same decoded length.
|
| 163 |
+
|
| 164 |
+
### Absolute actions and second-order profiles
|
| 165 |
+
|
| 166 |
+
An absolute layout needs the physical state immediately before the first action.
|
| 167 |
+
Pass the same `current_state` to encode and decode; it has shape `(B, D)` even
|
| 168 |
+
when the input is one `(T, D)` episode:
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
tokens = codec.encode(actions, fps=30, current_state=current_state)
|
| 172 |
+
reconstructed = codec.decode(tokens, fps=30, current_state=current_state)
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
A second-order profile additionally needs the preceding reconstructed
|
| 176 |
+
first-order increment for each active component. Inspect
|
| 177 |
+
`codec.requires_previous_first_order`. The boundary mapping has the form
|
| 178 |
+
`{component_name: (B, D_active)}` at the codec's target rate. It is independent
|
| 179 |
+
of `current_state`.
|
| 180 |
+
|
| 181 |
+
```python
|
| 182 |
+
# Only use zeros when the preceding motion is known to be zero.
|
| 183 |
+
boundary = codec.first_order_template(batch_size=1)
|
| 184 |
+
tokens = codec.encode(actions, fps=30, previous_first_order=boundary)
|
| 185 |
+
reconstructed = codec.decode(tokens, fps=30, previous_first_order=boundary)
|
| 186 |
+
next_boundary = codec.final_first_order(
|
| 187 |
+
tokens, previous_first_order=boundary, executed_steps=None
|
| 188 |
+
)
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
`final_first_order` returns the boundary for the next chunk. `executed_steps`
|
| 192 |
+
can select an executed prefix in codec-rate steps. For an absolute second-order
|
| 193 |
+
layout, pass both `current_state` and `previous_first_order` to encode/decode.
|
| 194 |
+
The library never invents a boundary from the first action.
|
| 195 |
+
|
| 196 |
+
For diagnostics, `codec.is_fitted`, `codec.fitted_profiles`, and
|
| 197 |
+
`codec.requires_previous_first_order` report whether the selected artifact is
|
| 198 |
+
ready for inference and whether the second-order boundary is mandatory.
|
| 199 |
+
|
| 200 |
+
## Fit and save
|
| 201 |
+
|
| 202 |
+
Fit on actions whose columns follow the selected layout. The same `ActionCodec2`
|
| 203 |
+
object is the training interface:
|
| 204 |
+
|
| 205 |
+
```python
|
| 206 |
+
codec = ActionCodec2(action_space="single_eef_delta")
|
| 207 |
+
codec.fit(episodes, fps=15, backend="auto")
|
| 208 |
+
print(codec.is_fitted, codec.fitted_profiles)
|
| 209 |
+
print(codec.training_report)
|
| 210 |
+
codec.save_pretrained("./my-codec")
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
`episodes` can be one `(T, D)` array, one regular `(B, T, D)` array, or an
|
| 214 |
+
iterable of variable-length episodes. For mixed layouts or sampling rates, pass
|
| 215 |
+
records with explicit metadata:
|
| 216 |
+
|
| 217 |
+
```python
|
| 218 |
+
records = [
|
| 219 |
+
{"actions": episode_a, "action_space": "single_eef_delta", "fps": 15},
|
| 220 |
+
{"actions": episode_b, "action_space": "single_joint_delta", "fps": 30},
|
| 221 |
+
]
|
| 222 |
+
codec.fit(records, backend="auto")
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
A record may also contain `current_state` and `previous_first_order` as required
|
| 226 |
+
by its layout. `fit` supports `primitive_order=2` codecs, an explicit
|
| 227 |
+
`bootstrap_context=True` option for two measured context frames, and
|
| 228 |
+
`profile_configs={"joint": ..., "eef": ...}` at construction for custom
|
| 229 |
+
physical profile settings. `backend` is `"auto"`, `"native"`, or `"python"`;
|
| 230 |
+
`threads`, `batch_size`, `progress`, `max_cell_bytes`, and `work_dir` control
|
| 231 |
+
resource use. Fitting replaces token assignments; re-encode datasets after
|
| 232 |
+
refitting. `save_pretrained` writes the complete loadable artifact, including
|
| 233 |
+
the layout binding and model card. `save_pretrained(path, push_to_hub=True,
|
| 234 |
+
repo_id="ZibinDong/my-actioncodec2")` also uploads it to a Hub repository you control.
|
| 235 |
+
|
| 236 |
+
The optional C++ extension accelerates fitting when available. `backend="auto"`
|
| 237 |
+
uses it if installed and otherwise uses the Python implementation.
|
| 238 |
+
|
| 239 |
+
### Reuse separately fitted physical profiles
|
| 240 |
+
|
| 241 |
+
If you already have fitted joint and EEF profile directories, assemble a router
|
| 242 |
+
without refitting. Budgets come from the recorded training configuration unless
|
| 243 |
+
you supply `token_budgets`:
|
| 244 |
+
|
| 245 |
+
```python
|
| 246 |
+
codec = ActionCodec2.from_profiles(
|
| 247 |
+
"./joint-profile", "./eef-profile", action_space="single_eef_delta"
|
| 248 |
+
)
|
| 249 |
+
codec.save_pretrained("./combined-codec")
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
The equivalent command is:
|
| 253 |
+
|
| 254 |
+
```bash
|
| 255 |
+
actioncodec2-bundle --joint ./joint-profile --eef ./eef-profile \\
|
| 256 |
+
--action-space single_eef_delta --out ./combined-codec
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
The command also accepts `--joint-budget` and `--eef-budget`. Omit
|
| 260 |
+
`--action-space` only when you intentionally want to select a layout at load
|
| 261 |
+
time.
|
| 262 |
+
|
| 263 |
+
## Constrain model generation
|
| 264 |
+
|
| 265 |
+
The codec supplies a grammar for an exact action horizon; it does not generate
|
| 266 |
+
actions by itself. `codec.grammar(horizon, fps=...)` has
|
| 267 |
+
`next_token_mask(prefix)` and `is_complete(tokens)`. A truncated or otherwise
|
| 268 |
+
invalid stream cannot be decoded as a complete action chunk.
|
| 269 |
+
|
| 270 |
+
```python
|
| 271 |
+
horizon = 12
|
| 272 |
+
grammar = codec.grammar(horizon, fps=15)
|
| 273 |
+
max_action_tokens = codec.max_tokens_for_horizon(horizon, fps=15)
|
| 274 |
+
assert grammar.is_complete(tokens)
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
For a Transformers model, use the adapter returned by
|
| 278 |
+
`codec.logits_processor(...)` in `generate`:
|
| 279 |
+
|
| 280 |
+
```python
|
| 281 |
+
from transformers import LogitsProcessorList
|
| 282 |
+
|
| 283 |
+
constraint = codec.logits_processor(
|
| 284 |
+
horizon,
|
| 285 |
+
fps=15,
|
| 286 |
+
prompt_length=input_ids.shape[1],
|
| 287 |
+
eos_token_id=model.config.eos_token_id,
|
| 288 |
+
token_offset=action_token_offset,
|
| 289 |
+
)
|
| 290 |
+
output = model.generate(
|
| 291 |
+
input_ids,
|
| 292 |
+
logits_processor=LogitsProcessorList([constraint]),
|
| 293 |
+
)
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
The model must already have a contiguous range of action IDs beginning at
|
| 297 |
+
`action_token_offset`; the codec does not modify its vocabulary. EOS and any
|
| 298 |
+
padding ID must lie outside that range. `codec.vocab_size` is the number of
|
| 299 |
+
action IDs. `codec.new_action_tokens` provides their ordered names, and
|
| 300 |
+
`codec.to_token_strs(one_token_row)` renders a row for inspection. Remove the
|
| 301 |
+
model's token offset and EOS/padding IDs before passing generated action IDs to
|
| 302 |
+
`codec.decode`.
|
| 303 |
+
|
| 304 |
+
## Advanced profile commands
|
| 305 |
+
|
| 306 |
+
The recommended training path is `codec.fit`. The following commands operate
|
| 307 |
+
on physical profile tensors rather than raw action-space layouts. Use them when
|
| 308 |
+
you intentionally need to fit or audit individual joint/EEF profiles. Their
|
| 309 |
+
`.npy`/`.npz` inputs must already contain canonical `(T, D_profile)` or
|
| 310 |
+
`(B, T, D_profile)` trajectories; `actioncodec2-fit-profile` does not perform
|
| 311 |
+
layout routing or source-rate conversion.
|
| 312 |
+
|
| 313 |
+
```bash
|
| 314 |
+
actioncodec2-fit-profile --config ./eef-config.yaml --data ./eef.npy \\
|
| 315 |
+
--output-dir ./eef-profile
|
| 316 |
+
actioncodec2-audit-profile stats --config ./eef-config.yaml \\
|
| 317 |
+
--data ./eef.npy --output ./eef-stats.json
|
| 318 |
+
actioncodec2-audit-profile validate --artifact ./eef-profile \\
|
| 319 |
+
--data ./eef.npy --output ./eef-validation.json
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
`--config` is an `ActionCodec2` physical profile YAML with a `train` section.
|
| 323 |
+
The fit command writes a physical profile and a JSON report. `stats` summarizes
|
| 324 |
+
source trajectories before fitting; `validate` checks a fitted profile against
|
| 325 |
+
data and writes a machine-readable report. Run each command with `--help` for
|
| 326 |
+
resource limits, metadata fields, and overwrite controls.
|
| 327 |
+
|
| 328 |
+
## Public interface at a glance
|
| 329 |
+
|
| 330 |
+
| Task | Entry point | Result |
|
| 331 |
+
| --- | --- | --- |
|
| 332 |
+
| Inspect layouts | `ActionCodec2.builtin_action_spaces()`, `describe_pretrained()`, `print(codec)`, `codec.print_action_spaces()` | Explicit physical contracts |
|
| 333 |
+
| Load and switch | `from_pretrained()`, `for_action_space()`, `register_action_space()` | Bound `ActionCodec2` |
|
| 334 |
+
| Fit and package | `ActionCodec2(...)`, `fit()`, `from_profiles()`, `save_pretrained()` | Loadable artifact |
|
| 335 |
+
| Encode and decode | `encode()`, `decode()`, `batch_decode()`, `codec(...)` | Token rows or action tensors |
|
| 336 |
+
| Stateful chunks | `first_order_template()`, `final_first_order()` | Explicit second-order boundaries |
|
| 337 |
+
| Generate | `grammar()`, `logits_processor()`, `max_tokens_for_horizon()` | Valid token constraints |
|
| 338 |
+
| Inspect tokens | `vocab_size`, `new_action_tokens`, `to_token_strs()` | ID-to-name mapping |
|
| 339 |
+
|
| 340 |
+
`AutoProcessor.from_pretrained(path, trust_remote_code=True)` loads an artifact
|
| 341 |
+
without importing this package first. If the package is installed and imported,
|
| 342 |
+
its processor registration also supports local `AutoProcessor` resolution.
|
| 343 |
+
|
| 344 |
+
## Tests
|
| 345 |
+
|
| 346 |
+
```bash
|
| 347 |
+
python -m pytest tests -q
|
| 348 |
+
```
|
| 349 |
+
|
| 350 |
+
## Artifact contents
|
| 351 |
+
|
| 352 |
+
A saved artifact contains the metadata, fitted profiles, private runtime, and
|
| 353 |
+
this guide. Keep the complete directory together when moving it or uploading it
|
| 354 |
+
to Hugging Face.
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
SUPPORT_FILES = {
|
| 358 |
+
"requirements.txt": "numpy\nscipy\ntorch>=2.3\ntransformers>=4.57,<6\nhuggingface-hub\npyyaml\n",
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
MODULE_FILES = [
|
| 362 |
+
"backend/__init__.py",
|
| 363 |
+
"backend/native_backend.py",
|
| 364 |
+
"common/__init__.py",
|
| 365 |
+
"common/_frontend_base.py",
|
| 366 |
+
"common/_types.py",
|
| 367 |
+
"common/_validation.py",
|
| 368 |
+
"frontend/__init__.py",
|
| 369 |
+
"frontend/tokenizer.py",
|
| 370 |
+
"integration/__init__.py",
|
| 371 |
+
"integration/exporting.py",
|
| 372 |
+
"integration/hf.py",
|
| 373 |
+
"physical/__init__.py",
|
| 374 |
+
"physical/_so3.py",
|
| 375 |
+
"physical/boundary.py",
|
| 376 |
+
"physical/configuration.py",
|
| 377 |
+
"physical/physical_codec.py",
|
| 378 |
+
"physical/physical_processor.py",
|
| 379 |
+
"physical/primitives.py",
|
| 380 |
+
"physical/semantic.py",
|
| 381 |
+
"physical/trajectory.py",
|
| 382 |
+
"routing/__init__.py",
|
| 383 |
+
"routing/cache.py",
|
| 384 |
+
"routing/defaults.py",
|
| 385 |
+
"routing/presets.py",
|
| 386 |
+
"routing/routing.py",
|
| 387 |
+
"tokenization/__init__.py",
|
| 388 |
+
"tokenization/bpe.py",
|
| 389 |
+
"tokenization/bpe_training.py",
|
| 390 |
+
"tokenization/bpe_vocabulary.py",
|
| 391 |
+
"tokenization/generation.py",
|
| 392 |
+
"tokenization/grammar.py",
|
| 393 |
+
"tokenization/setbpe_spatial.py",
|
| 394 |
+
"tokenization/setbpe_spatial_training.py",
|
| 395 |
+
"tokenization/setbpe_temporal.py",
|
| 396 |
+
"tokenization/setbpe_temporal_training.py",
|
| 397 |
+
"training/__init__.py",
|
| 398 |
+
"training/fitting.py",
|
| 399 |
+
"training/training.py",
|
| 400 |
+
"processing_actioncodec2.py",
|
| 401 |
+
]
|
runtime/integration/hf.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face AutoProcessor integration.
|
| 2 |
+
|
| 3 |
+
ActionCodec2 is a processor, not a model: it has no trainable ``forward``
|
| 4 |
+
parameters and maps continuous trajectories to/from discrete model-facing token
|
| 5 |
+
IDs. The numerical artifact remains ``config.yaml``/``primitives.npz``/
|
| 6 |
+
``bpe.json``; the two small JSON files here are standard Hugging Face discovery
|
| 7 |
+
metadata only.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import TYPE_CHECKING, Any
|
| 15 |
+
|
| 16 |
+
from transformers import AutoConfig, AutoProcessor, PretrainedConfig
|
| 17 |
+
|
| 18 |
+
from ..physical.configuration import MODEL_TYPE
|
| 19 |
+
from .exporting import USER_GUIDE
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from ..physical.physical_processor import ActionCodec2Processor
|
| 23 |
+
from ..routing.routing import ActionCodec2Router
|
| 24 |
+
|
| 25 |
+
HF_CONFIG_NAME = "config.json"
|
| 26 |
+
PROCESSOR_CONFIG_NAME = "processor_config.json"
|
| 27 |
+
PROCESSOR_CLASS_NAME = "ActionCodec2Processor"
|
| 28 |
+
ROUTER_MODEL_TYPE = "actioncodec2_router"
|
| 29 |
+
ROUTER_PROCESSOR_CLASS_NAME = "ActionCodec2Router"
|
| 30 |
+
ROUTER_CONFIG_NAME = "router_config.yaml"
|
| 31 |
+
ROUTER_ALLOW_PATTERNS = [
|
| 32 |
+
ROUTER_CONFIG_NAME,
|
| 33 |
+
HF_CONFIG_NAME,
|
| 34 |
+
PROCESSOR_CONFIG_NAME,
|
| 35 |
+
"profiles/**",
|
| 36 |
+
"fit_report.json",
|
| 37 |
+
"README.md",
|
| 38 |
+
"*.py",
|
| 39 |
+
"requirements.txt",
|
| 40 |
+
"runtime/**", "actioncodec2_kernels/**", "setup.py", "pyproject.toml", "LICENSE",
|
| 41 |
+
# Compatibility for artifacts emitted before runtime/ was introduced.
|
| 42 |
+
"native/*",
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ActionCodec2FrontendHFConfig(PretrainedConfig):
|
| 47 |
+
"""AutoConfig discovery key for the action-space-bound public processor."""
|
| 48 |
+
|
| 49 |
+
model_type = "actioncodec2"
|
| 50 |
+
|
| 51 |
+
def __init__(self, **kwargs: Any) -> None:
|
| 52 |
+
kwargs.setdefault("processor_class", "ActionCodec2")
|
| 53 |
+
super().__init__(**kwargs)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class ActionCodec2HFConfig(PretrainedConfig):
|
| 57 |
+
"""Minimal AutoConfig key that dispatches to :class:`ActionCodec2Processor`."""
|
| 58 |
+
|
| 59 |
+
model_type = MODEL_TYPE
|
| 60 |
+
|
| 61 |
+
def __init__(self, **kwargs: Any) -> None:
|
| 62 |
+
kwargs.setdefault("processor_class", PROCESSOR_CLASS_NAME)
|
| 63 |
+
super().__init__(**kwargs)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ActionCodec2RouterHFConfig(PretrainedConfig):
|
| 67 |
+
"""Minimal AutoConfig key that dispatches to :class:`ActionCodec2Router`."""
|
| 68 |
+
|
| 69 |
+
model_type = ROUTER_MODEL_TYPE
|
| 70 |
+
|
| 71 |
+
def __init__(self, **kwargs: Any) -> None:
|
| 72 |
+
kwargs.setdefault("processor_class", ROUTER_PROCESSOR_CLASS_NAME)
|
| 73 |
+
super().__init__(**kwargs)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def save_huggingface_metadata(
|
| 77 |
+
directory: str | Path,
|
| 78 |
+
*,
|
| 79 |
+
model_type: str = MODEL_TYPE,
|
| 80 |
+
processor_class: str = PROCESSOR_CLASS_NAME,
|
| 81 |
+
**processor_config: Any,
|
| 82 |
+
) -> tuple[Path, Path]:
|
| 83 |
+
"""Write standard local/Hub discovery files for ``AutoProcessor``."""
|
| 84 |
+
root = Path(directory)
|
| 85 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 86 |
+
config_path = root / HF_CONFIG_NAME
|
| 87 |
+
processor_path = root / PROCESSOR_CONFIG_NAME
|
| 88 |
+
config_path.write_text(
|
| 89 |
+
json.dumps(
|
| 90 |
+
{
|
| 91 |
+
"model_type": model_type,
|
| 92 |
+
"processor_class": processor_class,
|
| 93 |
+
},
|
| 94 |
+
indent=2,
|
| 95 |
+
)
|
| 96 |
+
+ "\n",
|
| 97 |
+
encoding="utf-8",
|
| 98 |
+
)
|
| 99 |
+
processor_path.write_text(
|
| 100 |
+
json.dumps({"processor_class": processor_class, **processor_config}, indent=2)
|
| 101 |
+
+ "\n",
|
| 102 |
+
encoding="utf-8",
|
| 103 |
+
)
|
| 104 |
+
return config_path, processor_path
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def resolve_pretrained_directory(
|
| 108 |
+
pretrained_model_name_or_path: str | Path,
|
| 109 |
+
*,
|
| 110 |
+
allow_patterns: list[str] | None = None,
|
| 111 |
+
**kwargs: Any,
|
| 112 |
+
) -> Path:
|
| 113 |
+
"""Resolve a local directory or download one immutable Hub snapshot."""
|
| 114 |
+
source = Path(pretrained_model_name_or_path)
|
| 115 |
+
kwargs.pop("_from_auto", None)
|
| 116 |
+
kwargs.pop("trust_remote_code", None)
|
| 117 |
+
subfolder = str(kwargs.pop("subfolder", ""))
|
| 118 |
+
allowed = {
|
| 119 |
+
"cache_dir",
|
| 120 |
+
"force_download",
|
| 121 |
+
"local_files_only",
|
| 122 |
+
"token",
|
| 123 |
+
"revision",
|
| 124 |
+
"repo_type",
|
| 125 |
+
}
|
| 126 |
+
unknown = sorted(set(kwargs) - allowed)
|
| 127 |
+
if unknown:
|
| 128 |
+
raise TypeError(f"unsupported from_pretrained keyword(s): {', '.join(unknown)}")
|
| 129 |
+
if source.is_dir():
|
| 130 |
+
return source / subfolder
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
from huggingface_hub import snapshot_download
|
| 134 |
+
except ImportError as error:
|
| 135 |
+
raise ImportError(
|
| 136 |
+
"loading a Hub artifact requires huggingface_hub (installed with Transformers)"
|
| 137 |
+
) from error
|
| 138 |
+
patterns = allow_patterns or [
|
| 139 |
+
"config.yaml",
|
| 140 |
+
"primitives.npz",
|
| 141 |
+
"bpe.json",
|
| 142 |
+
HF_CONFIG_NAME,
|
| 143 |
+
PROCESSOR_CONFIG_NAME,
|
| 144 |
+
]
|
| 145 |
+
if subfolder:
|
| 146 |
+
prefix = subfolder.rstrip("/")
|
| 147 |
+
patterns = [f"{prefix}/{pattern}" for pattern in patterns]
|
| 148 |
+
snapshot = snapshot_download(
|
| 149 |
+
repo_id=str(pretrained_model_name_or_path),
|
| 150 |
+
allow_patterns=patterns,
|
| 151 |
+
**kwargs,
|
| 152 |
+
)
|
| 153 |
+
return Path(snapshot) / subfolder
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def upload_pretrained_directory(directory: Path, **kwargs: Any) -> None:
|
| 157 |
+
"""Upload a complete, already-saved processor artifact to the Hub."""
|
| 158 |
+
from huggingface_hub import HfApi
|
| 159 |
+
|
| 160 |
+
repo_id = kwargs.pop("repo_id", directory.name)
|
| 161 |
+
commit_message = kwargs.pop("commit_message", "Upload ActionCodec2 processor")
|
| 162 |
+
api = HfApi(token=kwargs.pop("token", None))
|
| 163 |
+
api.create_repo(repo_id=repo_id, exist_ok=True, **kwargs)
|
| 164 |
+
api.upload_folder(
|
| 165 |
+
repo_id=repo_id,
|
| 166 |
+
folder_path=str(directory),
|
| 167 |
+
commit_message=commit_message,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def register_actioncodec2_auto_processors(
|
| 172 |
+
processor_class: type[ActionCodec2Processor],
|
| 173 |
+
router_class: type[ActionCodec2Router],
|
| 174 |
+
) -> bool:
|
| 175 |
+
"""Register both installed ActionCodec2 artifact types with Transformers.
|
| 176 |
+
|
| 177 |
+
Registration is idempotent and never changes the serialized numerical artifact.
|
| 178 |
+
"""
|
| 179 |
+
AutoConfig.register(MODEL_TYPE, ActionCodec2HFConfig, exist_ok=True)
|
| 180 |
+
AutoProcessor.register(ActionCodec2HFConfig, processor_class, exist_ok=True)
|
| 181 |
+
AutoConfig.register(ROUTER_MODEL_TYPE, ActionCodec2RouterHFConfig, exist_ok=True)
|
| 182 |
+
AutoProcessor.register(ActionCodec2RouterHFConfig, router_class, exist_ok=True)
|
| 183 |
+
return True
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def save_model_card(directory: str | Path, description: str) -> Path:
|
| 187 |
+
"""Create an informative Hub card on first save, preserving existing user cards."""
|
| 188 |
+
path = Path(directory) / "README.md"
|
| 189 |
+
if not path.exists():
|
| 190 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 191 |
+
guide = USER_GUIDE
|
| 192 |
+
path.write_text(
|
| 193 |
+
guide
|
| 194 |
+
+ "\n\n## Artifact-specific report\n\n```text\n"
|
| 195 |
+
+ description
|
| 196 |
+
+ "\n```\n",
|
| 197 |
+
encoding="utf-8",
|
| 198 |
+
)
|
| 199 |
+
return path
|
runtime/physical/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal physical components for ActionCodec2."""
|
runtime/physical/_so3.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared NumPy quaternion operations using the xyzw convention."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def normalize_quaternion(values: np.ndarray) -> np.ndarray:
|
| 9 |
+
"""Normalize quaternion rows and reject an undefined zero rotation."""
|
| 10 |
+
quaternions = np.asarray(values, dtype=np.float64)
|
| 11 |
+
norms = np.linalg.norm(quaternions, axis=-1, keepdims=True)
|
| 12 |
+
if np.any(norms < 1e-12):
|
| 13 |
+
raise ValueError("a pose contains a zero quaternion")
|
| 14 |
+
return quaternions / norms
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def multiply_quaternions(left: np.ndarray, right: np.ndarray) -> np.ndarray:
|
| 18 |
+
"""Return the Hamilton product of broadcastable xyzw arrays."""
|
| 19 |
+
first = np.asarray(left, dtype=np.float64)
|
| 20 |
+
second = np.asarray(right, dtype=np.float64)
|
| 21 |
+
first_vector, first_scalar = first[..., :3], first[..., 3:4]
|
| 22 |
+
second_vector, second_scalar = second[..., :3], second[..., 3:4]
|
| 23 |
+
return normalize_quaternion(
|
| 24 |
+
np.concatenate(
|
| 25 |
+
(
|
| 26 |
+
first_scalar * second_vector
|
| 27 |
+
+ second_scalar * first_vector
|
| 28 |
+
+ np.cross(first_vector, second_vector),
|
| 29 |
+
first_scalar * second_scalar
|
| 30 |
+
- np.sum(first_vector * second_vector, axis=-1, keepdims=True),
|
| 31 |
+
),
|
| 32 |
+
axis=-1,
|
| 33 |
+
)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def conjugate_quaternion(quaternion: np.ndarray) -> np.ndarray:
|
| 38 |
+
"""Return the conjugate of an xyzw quaternion array."""
|
| 39 |
+
result = np.asarray(quaternion, dtype=np.float64).copy()
|
| 40 |
+
result[..., :3] *= -1.0
|
| 41 |
+
return result
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def rotvec_to_quaternion(rotvec: np.ndarray) -> np.ndarray:
|
| 45 |
+
"""Convert rotation vectors to xyzw quaternions."""
|
| 46 |
+
values = np.asarray(rotvec, dtype=np.float64)
|
| 47 |
+
angle = np.linalg.norm(values, axis=-1)
|
| 48 |
+
scale = np.full_like(angle, 0.5)
|
| 49 |
+
regular = angle >= 1e-12
|
| 50 |
+
scale[regular] = np.sin(0.5 * angle[regular]) / angle[regular]
|
| 51 |
+
return normalize_quaternion(
|
| 52 |
+
np.concatenate(
|
| 53 |
+
(values * scale[..., None], np.cos(0.5 * angle)[..., None]), axis=-1
|
| 54 |
+
)
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def quaternion_to_rotvec(quaternion: np.ndarray) -> np.ndarray:
|
| 59 |
+
"""Convert xyzw quaternions to shortest-path rotation vectors."""
|
| 60 |
+
values = normalize_quaternion(quaternion).copy()
|
| 61 |
+
values[values[..., 3] < 0.0] *= -1.0
|
| 62 |
+
vector_norm = np.linalg.norm(values[..., :3], axis=-1)
|
| 63 |
+
angle = 2.0 * np.arctan2(vector_norm, np.clip(values[..., 3], 0.0, 1.0))
|
| 64 |
+
scale = np.full_like(angle, 2.0)
|
| 65 |
+
regular = vector_norm >= 1e-12
|
| 66 |
+
scale[regular] = angle[regular] / vector_norm[regular]
|
| 67 |
+
return values[..., :3] * scale[..., None]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def euler_xyz_to_quaternion(euler: np.ndarray) -> np.ndarray:
|
| 71 |
+
"""Convert ``(N,3)`` XYZ Euler angles to xyzw quaternions."""
|
| 72 |
+
values = np.asarray(euler, dtype=np.float64)
|
| 73 |
+
if values.ndim != 2 or values.shape[1] != 3:
|
| 74 |
+
raise ValueError("XYZ Euler angles must have shape [N, 3]")
|
| 75 |
+
half = 0.5 * values
|
| 76 |
+
sin_roll, sin_pitch, sin_yaw = np.sin(half).T
|
| 77 |
+
cos_roll, cos_pitch, cos_yaw = np.cos(half).T
|
| 78 |
+
result = np.empty((len(values), 4), dtype=np.float64)
|
| 79 |
+
result[:, 0] = sin_roll * cos_pitch * cos_yaw - cos_roll * sin_pitch * sin_yaw
|
| 80 |
+
result[:, 1] = cos_roll * sin_pitch * cos_yaw + sin_roll * cos_pitch * sin_yaw
|
| 81 |
+
result[:, 2] = cos_roll * cos_pitch * sin_yaw - sin_roll * sin_pitch * cos_yaw
|
| 82 |
+
result[:, 3] = cos_roll * cos_pitch * cos_yaw + sin_roll * sin_pitch * sin_yaw
|
| 83 |
+
return normalize_quaternion(result)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def quaternion_to_euler_xyz(quaternion: np.ndarray) -> np.ndarray:
|
| 87 |
+
"""Convert ``(N,4)`` xyzw quaternions to XYZ Euler angles."""
|
| 88 |
+
values = normalize_quaternion(quaternion)
|
| 89 |
+
if values.ndim != 2 or values.shape[1] != 4:
|
| 90 |
+
raise ValueError("xyzw quaternions must have shape [N, 4]")
|
| 91 |
+
x, y, z, w = values.T
|
| 92 |
+
return np.stack(
|
| 93 |
+
(
|
| 94 |
+
np.arctan2(2.0 * (w * x + y * z), 1.0 - 2.0 * (x * x + y * y)),
|
| 95 |
+
np.arcsin(np.clip(2.0 * (w * y - z * x), -1.0, 1.0)),
|
| 96 |
+
np.arctan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z)),
|
| 97 |
+
),
|
| 98 |
+
axis=1,
|
| 99 |
+
)
|
runtime/physical/boundary.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit measured-context preparation for stateful trajectory fitting."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from .configuration import ActionCodec2Config
|
| 8 |
+
from .physical_codec import _scale, _SO3Runtime
|
| 9 |
+
from .trajectory import (
|
| 10 |
+
ActiveTrajectory,
|
| 11 |
+
TrajectoryLike,
|
| 12 |
+
prepare_trajectory,
|
| 13 |
+
trajectory_boundary,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def source_differences(
|
| 18 |
+
trajectory: TrajectoryLike, config: ActionCodec2Config
|
| 19 |
+
) -> dict[str, tuple[tuple[int, ...], np.ndarray, np.ndarray]]:
|
| 20 |
+
"""Measure physical first/second differences with the declared group law.
|
| 21 |
+
|
| 22 |
+
Returns component names mapped to global dims, ``[T,d]`` first-order
|
| 23 |
+
values, and second-order values. If no first-order boundary is supplied
|
| 24 |
+
for a first-order profile, the first second difference is unobserved and
|
| 25 |
+
omitted. Second-order profiles require their explicit boundary.
|
| 26 |
+
"""
|
| 27 |
+
values, dims = prepare_trajectory(
|
| 28 |
+
trajectory, dimension=config.physical.input_dim, index=0
|
| 29 |
+
)
|
| 30 |
+
boundary = trajectory_boundary(trajectory, config, dims)
|
| 31 |
+
output = {}
|
| 32 |
+
for name, component in config.physical.components.items():
|
| 33 |
+
present = tuple(d for d in component.dims if d in dims)
|
| 34 |
+
if not present or component.primitive_order == 0:
|
| 35 |
+
continue
|
| 36 |
+
local = [dims.index(d) for d in present]
|
| 37 |
+
indices = [component.dims.index(d) for d in present]
|
| 38 |
+
scale = np.take(_scale(component), indices)
|
| 39 |
+
raw = values[:, local].astype(np.float64) * scale
|
| 40 |
+
previous = (
|
| 41 |
+
None
|
| 42 |
+
if boundary is None
|
| 43 |
+
else boundary[list(present)].astype(np.float64) * scale
|
| 44 |
+
)
|
| 45 |
+
if component.space == "additive":
|
| 46 |
+
if component.input_order == 0:
|
| 47 |
+
first = np.diff(raw, axis=0, prepend=np.zeros((1, len(present))))
|
| 48 |
+
elif component.input_order == 1:
|
| 49 |
+
first = raw
|
| 50 |
+
else:
|
| 51 |
+
if previous is None:
|
| 52 |
+
raise ValueError(
|
| 53 |
+
"order-2 input statistics require previous_first_order"
|
| 54 |
+
)
|
| 55 |
+
first = np.cumsum(raw, axis=0) + previous
|
| 56 |
+
second = (
|
| 57 |
+
np.diff(first, axis=0)
|
| 58 |
+
if previous is None
|
| 59 |
+
else np.diff(first, axis=0, prepend=previous[None])
|
| 60 |
+
)
|
| 61 |
+
else:
|
| 62 |
+
if len(present) != 3:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
"differential statistics require complete SO(3) components"
|
| 65 |
+
)
|
| 66 |
+
runtime = _SO3Runtime(component, (np.zeros(1),) * 3)
|
| 67 |
+
quaternion = runtime._from_representation(raw)
|
| 68 |
+
if component.input_order == 0:
|
| 69 |
+
anchors = np.concatenate(
|
| 70 |
+
(np.array([[0.0, 0.0, 0.0, 1.0]]), quaternion[:-1])
|
| 71 |
+
)
|
| 72 |
+
increments = runtime._residual(quaternion, anchors)
|
| 73 |
+
else:
|
| 74 |
+
increments = quaternion
|
| 75 |
+
first = runtime._to_representation(increments)
|
| 76 |
+
if previous is None:
|
| 77 |
+
changes = runtime._residual(increments[1:], increments[:-1])
|
| 78 |
+
else:
|
| 79 |
+
anchors = np.concatenate(
|
| 80 |
+
(runtime._from_representation(previous[None]), increments[:-1])
|
| 81 |
+
)
|
| 82 |
+
changes = runtime._residual(increments, anchors)
|
| 83 |
+
second = runtime._to_representation(changes)
|
| 84 |
+
output[name] = present, first, second
|
| 85 |
+
return output
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def bootstrap_second_order_trajectory(
|
| 89 |
+
trajectory: ActiveTrajectory, config: ActionCodec2Config
|
| 90 |
+
) -> ActiveTrajectory:
|
| 91 |
+
"""Use two observed positions as context and encode the remaining suffix.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
trajectory: Canonical ``[T,D_active]`` absolute trajectory, ``T >= 3``.
|
| 95 |
+
The first two frames establish an exact measured first-order
|
| 96 |
+
boundary; they are context, not encoded targets.
|
| 97 |
+
config: Physical profile with order-0 input components.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
ActiveTrajectory: ``[T-2,D_active]`` local trajectory anchored at the
|
| 101 |
+
second observed frame, with explicit ``previous_first_order``. This is
|
| 102 |
+
an offline initialization helper, not a replacement for carrying the
|
| 103 |
+
reconstructed boundary between deployment chunks.
|
| 104 |
+
"""
|
| 105 |
+
values, dims = prepare_trajectory(
|
| 106 |
+
trajectory, dimension=config.physical.input_dim, index=0
|
| 107 |
+
)
|
| 108 |
+
if len(values) < 3:
|
| 109 |
+
raise ValueError(
|
| 110 |
+
"second-order bootstrap requires two context frames and at least one target"
|
| 111 |
+
)
|
| 112 |
+
if trajectory.previous_first_order is not None:
|
| 113 |
+
raise ValueError(
|
| 114 |
+
"trajectory already has previous_first_order; do not bootstrap it again"
|
| 115 |
+
)
|
| 116 |
+
result = values[2:].copy()
|
| 117 |
+
previous = np.zeros(len(dims), dtype=np.float32)
|
| 118 |
+
for component in config.physical.components.values():
|
| 119 |
+
local = [dims.index(d) for d in component.dims if d in dims]
|
| 120 |
+
if not local:
|
| 121 |
+
continue
|
| 122 |
+
if component.input_order != 0:
|
| 123 |
+
raise ValueError("measured-context bootstrap requires input_order=0")
|
| 124 |
+
if component.primitive_order == 0:
|
| 125 |
+
continue
|
| 126 |
+
if component.space == "additive":
|
| 127 |
+
result[:, local] -= values[1, local]
|
| 128 |
+
previous[local] = values[1, local] - values[0, local]
|
| 129 |
+
else:
|
| 130 |
+
if len(local) != 3:
|
| 131 |
+
raise ValueError("bootstrap requires complete SO(3) components")
|
| 132 |
+
runtime = _SO3Runtime(component, (np.zeros(1),) * 3)
|
| 133 |
+
quaternion = runtime._from_representation(
|
| 134 |
+
values[:, local] * _scale(component)
|
| 135 |
+
)
|
| 136 |
+
increment = runtime._residual(quaternion[1:2], quaternion[:1])
|
| 137 |
+
# Rebase by a fixed group action that preserves the configured
|
| 138 |
+
# increment convention (body/right versus world/left).
|
| 139 |
+
from ._so3 import conjugate_quaternion, multiply_quaternions
|
| 140 |
+
|
| 141 |
+
anchor = conjugate_quaternion(quaternion[1:2])
|
| 142 |
+
local_path = (
|
| 143 |
+
multiply_quaternions(quaternion[2:], anchor)
|
| 144 |
+
if runtime.left
|
| 145 |
+
else multiply_quaternions(anchor, quaternion[2:])
|
| 146 |
+
)
|
| 147 |
+
result[:, local] = runtime._to_representation(local_path) / _scale(
|
| 148 |
+
component
|
| 149 |
+
)
|
| 150 |
+
previous[local] = runtime._to_representation(increment)[0] / _scale(
|
| 151 |
+
component
|
| 152 |
+
)
|
| 153 |
+
return ActiveTrajectory(result, dims, previous_first_order=previous)
|
runtime/physical/configuration.py
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validated ``config.yaml`` model contract for ActionCodec2."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
import re
|
| 7 |
+
from collections.abc import Mapping, Sequence
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from types import MappingProxyType
|
| 11 |
+
|
| 12 |
+
import yaml
|
| 13 |
+
|
| 14 |
+
from ..common._validation import validate_dimension_partition
|
| 15 |
+
|
| 16 |
+
CONFIG_NAME = "config.yaml"
|
| 17 |
+
# On-disk format identifier; retained because it participates in artifact fingerprints.
|
| 18 |
+
MODEL_TYPE = "actioncodec2_unified"
|
| 19 |
+
CONFIG_VERSION = 1
|
| 20 |
+
_SPACES = ("additive", "so3")
|
| 21 |
+
_SO3_REPRESENTATIONS = ("rotation_vector", "euler_xyz")
|
| 22 |
+
_SO3_MULTIPLICATIONS = ("left", "right")
|
| 23 |
+
_PRIMITIVE_SELECTIONS = ("nearest", "smooth")
|
| 24 |
+
_ORDER_KEY = re.compile(r"order_(0|[1-9][0-9]*)\Z")
|
| 25 |
+
|
| 26 |
+
NumberOrVector = float | tuple[float, ...]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ConfigError(ValueError):
|
| 30 |
+
"""The serialized model definition is incomplete or inconsistent."""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class _UniqueKeyLoader(yaml.SafeLoader):
|
| 34 |
+
"""Safe YAML loader that rejects duplicate mapping keys."""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _construct_unique_mapping(
|
| 38 |
+
loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False
|
| 39 |
+
) -> dict[object, object]:
|
| 40 |
+
loader.flatten_mapping(node)
|
| 41 |
+
output: dict[object, object] = {}
|
| 42 |
+
for key_node, value_node in node.value:
|
| 43 |
+
key = loader.construct_object(key_node, deep=deep)
|
| 44 |
+
try:
|
| 45 |
+
duplicate = key in output
|
| 46 |
+
except TypeError as error:
|
| 47 |
+
raise ConfigError("config.yaml contains an invalid mapping key") from error
|
| 48 |
+
if duplicate:
|
| 49 |
+
raise ConfigError(f"config.yaml contains duplicate field {key!r}")
|
| 50 |
+
output[key] = loader.construct_object(value_node, deep=deep)
|
| 51 |
+
return output
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
_UniqueKeyLoader.add_constructor(
|
| 55 |
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
| 56 |
+
_construct_unique_mapping,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _mapping(value: object, path: str) -> Mapping[str, object]:
|
| 61 |
+
if not isinstance(value, Mapping):
|
| 62 |
+
raise ConfigError(f"{path} must be a mapping")
|
| 63 |
+
if not all(isinstance(key, str) for key in value):
|
| 64 |
+
raise ConfigError(f"every field in {path} must be a string")
|
| 65 |
+
return value
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _reject_unknown(
|
| 69 |
+
payload: Mapping[str, object], allowed: set[str], path: str
|
| 70 |
+
) -> None:
|
| 71 |
+
unknown = sorted(set(payload) - allowed)
|
| 72 |
+
if unknown:
|
| 73 |
+
rendered = ", ".join(unknown)
|
| 74 |
+
raise ConfigError(f"{path} contains unknown field(s): {rendered}")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _required(payload: Mapping[str, object], key: str, path: str) -> object:
|
| 78 |
+
if key not in payload:
|
| 79 |
+
raise ConfigError(f"{path} is missing required field {key!r}")
|
| 80 |
+
return payload[key]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _integer(value: object, path: str, *, positive: bool = False) -> int:
|
| 84 |
+
if isinstance(value, bool) or not isinstance(value, int):
|
| 85 |
+
raise ConfigError(f"{path} must be an integer")
|
| 86 |
+
minimum = 1 if positive else 0
|
| 87 |
+
if value < minimum:
|
| 88 |
+
qualifier = "positive" if positive else "non-negative"
|
| 89 |
+
raise ConfigError(f"{path} must be {qualifier}")
|
| 90 |
+
return int(value)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _number(value: object, path: str) -> float:
|
| 94 |
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
| 95 |
+
raise ConfigError(f"{path} must be a number")
|
| 96 |
+
result = float(value)
|
| 97 |
+
if not math.isfinite(result):
|
| 98 |
+
raise ConfigError(f"{path} must be finite")
|
| 99 |
+
return result
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _number_or_vector(value: object, width: int, path: str) -> NumberOrVector:
|
| 103 |
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
| 104 |
+
result = tuple(
|
| 105 |
+
_number(item, f"{path}[{index}]") for index, item in enumerate(value)
|
| 106 |
+
)
|
| 107 |
+
if len(result) != width:
|
| 108 |
+
raise ConfigError(f"{path} must be a scalar or contain {width} values")
|
| 109 |
+
return result
|
| 110 |
+
return _number(value, path)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _expanded(value: NumberOrVector, width: int) -> tuple[float, ...]:
|
| 114 |
+
if isinstance(value, tuple):
|
| 115 |
+
return value
|
| 116 |
+
return (value,) * width
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _serialize_number_or_vector(value: NumberOrVector) -> float | list[float]:
|
| 120 |
+
return list(value) if isinstance(value, tuple) else value
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _parse_dims(value: object, input_dim: int, path: str) -> tuple[int, ...]:
|
| 124 |
+
if isinstance(value, Mapping):
|
| 125 |
+
payload = _mapping(value, path)
|
| 126 |
+
_reject_unknown(payload, {"start", "stop"}, path)
|
| 127 |
+
start = _integer(_required(payload, "start", path), f"{path}.start")
|
| 128 |
+
stop = _integer(_required(payload, "stop", path), f"{path}.stop")
|
| 129 |
+
if stop <= start:
|
| 130 |
+
raise ConfigError(f"{path}.stop must be greater than {path}.start")
|
| 131 |
+
dims = tuple(range(start, stop))
|
| 132 |
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
| 133 |
+
dims = tuple(
|
| 134 |
+
_integer(item, f"{path}[{index}]") for index, item in enumerate(value)
|
| 135 |
+
)
|
| 136 |
+
if not dims:
|
| 137 |
+
raise ConfigError(f"{path} must not be empty")
|
| 138 |
+
else:
|
| 139 |
+
raise ConfigError(f"{path} must be a list or a start/stop range")
|
| 140 |
+
|
| 141 |
+
if len(set(dims)) != len(dims):
|
| 142 |
+
raise ConfigError(f"{path} contains a repeated dimension")
|
| 143 |
+
outside = [dimension for dimension in dims if dimension >= input_dim]
|
| 144 |
+
if outside:
|
| 145 |
+
raise ConfigError(
|
| 146 |
+
f"{path} contains dimension {outside[0]}, outside input_dim={input_dim}"
|
| 147 |
+
)
|
| 148 |
+
return dims
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _serialize_dims(dims: tuple[int, ...]) -> object:
|
| 152 |
+
if len(dims) > 1 and dims == tuple(range(dims[0], dims[-1] + 1)):
|
| 153 |
+
return {"start": dims[0], "stop": dims[-1] + 1}
|
| 154 |
+
return list(dims)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@dataclass(frozen=True)
|
| 158 |
+
class ConstraintConfig:
|
| 159 |
+
"""Allowed inclusive interval for one differential order."""
|
| 160 |
+
|
| 161 |
+
minimum: NumberOrVector
|
| 162 |
+
maximum: NumberOrVector
|
| 163 |
+
|
| 164 |
+
@classmethod
|
| 165 |
+
def from_dict(cls, payload: object, *, width: int, path: str) -> ConstraintConfig:
|
| 166 |
+
values = _mapping(payload, path)
|
| 167 |
+
_reject_unknown(values, {"min", "max"}, path)
|
| 168 |
+
minimum = _number_or_vector(
|
| 169 |
+
_required(values, "min", path), width, f"{path}.min"
|
| 170 |
+
)
|
| 171 |
+
maximum = _number_or_vector(
|
| 172 |
+
_required(values, "max", path), width, f"{path}.max"
|
| 173 |
+
)
|
| 174 |
+
if any(
|
| 175 |
+
lower > upper
|
| 176 |
+
for lower, upper in zip(
|
| 177 |
+
_expanded(minimum, width), _expanded(maximum, width), strict=True
|
| 178 |
+
)
|
| 179 |
+
):
|
| 180 |
+
raise ConfigError(f"{path}.min must not exceed {path}.max")
|
| 181 |
+
return cls(minimum=minimum, maximum=maximum)
|
| 182 |
+
|
| 183 |
+
def to_dict(self) -> dict[str, object]:
|
| 184 |
+
return {
|
| 185 |
+
"min": _serialize_number_or_vector(self.minimum),
|
| 186 |
+
"max": _serialize_number_or_vector(self.maximum),
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@dataclass(frozen=True)
|
| 191 |
+
class InputTransformConfig:
|
| 192 |
+
"""Input-unit conversion and optional source-side clipping."""
|
| 193 |
+
|
| 194 |
+
scale: NumberOrVector = 1.0
|
| 195 |
+
clip: bool = False
|
| 196 |
+
|
| 197 |
+
@classmethod
|
| 198 |
+
def from_dict(
|
| 199 |
+
cls, payload: object | None, *, width: int, path: str
|
| 200 |
+
) -> InputTransformConfig:
|
| 201 |
+
if payload is None:
|
| 202 |
+
return cls()
|
| 203 |
+
values = _mapping(payload, path)
|
| 204 |
+
_reject_unknown(values, {"scale", "clip"}, path)
|
| 205 |
+
scale = _number_or_vector(values.get("scale", 1.0), width, f"{path}.scale")
|
| 206 |
+
if any(value <= 0.0 for value in _expanded(scale, width)):
|
| 207 |
+
raise ConfigError(f"{path}.scale must be positive")
|
| 208 |
+
clip = values.get("clip", False)
|
| 209 |
+
if not isinstance(clip, bool):
|
| 210 |
+
raise ConfigError(f"{path}.clip must be true or false")
|
| 211 |
+
return cls(scale=scale, clip=clip)
|
| 212 |
+
|
| 213 |
+
def to_dict(self) -> dict[str, object]:
|
| 214 |
+
return {
|
| 215 |
+
"scale": _serialize_number_or_vector(self.scale),
|
| 216 |
+
"clip": self.clip,
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _parse_component_geometry(
|
| 221 |
+
values: Mapping[str, object], *, input_dim: int, path: str
|
| 222 |
+
) -> tuple[tuple[int, ...], str, int, int, str | None, str | None]:
|
| 223 |
+
dims = _parse_dims(_required(values, "dims", path), input_dim, f"{path}.dims")
|
| 224 |
+
space = _required(values, "space", path)
|
| 225 |
+
if space not in _SPACES:
|
| 226 |
+
raise ConfigError(f"{path}.space must be one of {_SPACES}")
|
| 227 |
+
input_order = _integer(
|
| 228 |
+
_required(values, "input_order", path), f"{path}.input_order"
|
| 229 |
+
)
|
| 230 |
+
primitive_order = _integer(
|
| 231 |
+
_required(values, "primitive_order", path), f"{path}.primitive_order"
|
| 232 |
+
)
|
| 233 |
+
representation = values.get("representation")
|
| 234 |
+
multiplication = values.get("multiplication")
|
| 235 |
+
if space == "so3":
|
| 236 |
+
if len(dims) != 3:
|
| 237 |
+
raise ConfigError(f"{path} with space='so3' must contain exactly 3 dims")
|
| 238 |
+
if input_order not in {0, 1} or primitive_order not in {1, 2}:
|
| 239 |
+
raise ConfigError(
|
| 240 |
+
f"{path} with space='so3' supports input_order 0 or 1 and primitive_order 1 or 2"
|
| 241 |
+
)
|
| 242 |
+
if representation not in _SO3_REPRESENTATIONS:
|
| 243 |
+
raise ConfigError(
|
| 244 |
+
f"{path}.representation must be one of {_SO3_REPRESENTATIONS}"
|
| 245 |
+
)
|
| 246 |
+
if multiplication not in _SO3_MULTIPLICATIONS:
|
| 247 |
+
raise ConfigError(
|
| 248 |
+
f"{path}.multiplication must be one of {_SO3_MULTIPLICATIONS}"
|
| 249 |
+
)
|
| 250 |
+
else:
|
| 251 |
+
if input_order not in {0, 1, 2} or primitive_order not in {0, 1, 2}:
|
| 252 |
+
raise ConfigError(
|
| 253 |
+
f"{path} with space='additive' supports input_order and primitive_order 0, 1, or 2"
|
| 254 |
+
)
|
| 255 |
+
if representation is not None or multiplication is not None:
|
| 256 |
+
raise ConfigError(
|
| 257 |
+
f"{path}.representation and multiplication are only valid for space='so3'"
|
| 258 |
+
)
|
| 259 |
+
return (
|
| 260 |
+
dims,
|
| 261 |
+
str(space),
|
| 262 |
+
input_order,
|
| 263 |
+
primitive_order,
|
| 264 |
+
representation,
|
| 265 |
+
multiplication,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def _parse_component_constraints(
|
| 270 |
+
values: Mapping[str, object], *, width: int, path: str
|
| 271 |
+
) -> dict[str, ConstraintConfig]:
|
| 272 |
+
raw = _mapping(_required(values, "constraints", path), f"{path}.constraints")
|
| 273 |
+
if not raw:
|
| 274 |
+
raise ConfigError(f"{path}.constraints must not be empty")
|
| 275 |
+
constraints: dict[str, ConstraintConfig] = {}
|
| 276 |
+
for key, constraint in raw.items():
|
| 277 |
+
if _ORDER_KEY.fullmatch(key) is None:
|
| 278 |
+
raise ConfigError(
|
| 279 |
+
f"{path}.constraints field {key!r} must be named order_0, order_1, ..."
|
| 280 |
+
)
|
| 281 |
+
constraints[key] = ConstraintConfig.from_dict(
|
| 282 |
+
constraint, width=width, path=f"{path}.constraints.{key}"
|
| 283 |
+
)
|
| 284 |
+
return constraints
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@dataclass(frozen=True)
|
| 288 |
+
class ComponentConfig:
|
| 289 |
+
"""One orthogonal physical component of the action vector.
|
| 290 |
+
|
| 291 |
+
``dims`` defines the only global slots this component owns. ``input_order``
|
| 292 |
+
describes the dataset boundary, ``primitive_order`` describes the decoder
|
| 293 |
+
recursion, constraints are expressed after ``input_transform.scale``, and
|
| 294 |
+
``distortion_limit`` is coordinate-wise for additive spaces but one
|
| 295 |
+
geodesic angle for SO(3).
|
| 296 |
+
"""
|
| 297 |
+
|
| 298 |
+
dims: tuple[int, ...]
|
| 299 |
+
space: str
|
| 300 |
+
input_order: int
|
| 301 |
+
primitive_order: int
|
| 302 |
+
input_transform: InputTransformConfig
|
| 303 |
+
constraints: Mapping[str, ConstraintConfig]
|
| 304 |
+
distortion_limit: NumberOrVector
|
| 305 |
+
primitive_selection: str
|
| 306 |
+
representation: str | None = None
|
| 307 |
+
multiplication: str | None = None
|
| 308 |
+
|
| 309 |
+
@classmethod
|
| 310 |
+
def from_dict(
|
| 311 |
+
cls,
|
| 312 |
+
payload: object,
|
| 313 |
+
*,
|
| 314 |
+
input_dim: int,
|
| 315 |
+
path: str,
|
| 316 |
+
) -> ComponentConfig:
|
| 317 |
+
values = _mapping(payload, path)
|
| 318 |
+
allowed = {
|
| 319 |
+
"dims",
|
| 320 |
+
"space",
|
| 321 |
+
"input_order",
|
| 322 |
+
"primitive_order",
|
| 323 |
+
"input_transform",
|
| 324 |
+
"constraints",
|
| 325 |
+
"distortion_limit",
|
| 326 |
+
"primitive_selection",
|
| 327 |
+
"representation",
|
| 328 |
+
"multiplication",
|
| 329 |
+
}
|
| 330 |
+
_reject_unknown(values, allowed, path)
|
| 331 |
+
dims, space, input_order, primitive_order, representation, multiplication = (
|
| 332 |
+
_parse_component_geometry(values, input_dim=input_dim, path=path)
|
| 333 |
+
)
|
| 334 |
+
width = len(dims)
|
| 335 |
+
constraints = _parse_component_constraints(values, width=width, path=path)
|
| 336 |
+
|
| 337 |
+
input_transform = InputTransformConfig.from_dict(
|
| 338 |
+
values.get("input_transform"),
|
| 339 |
+
width=width,
|
| 340 |
+
path=f"{path}.input_transform",
|
| 341 |
+
)
|
| 342 |
+
if input_transform.clip and f"order_{input_order}" not in constraints:
|
| 343 |
+
raise ConfigError(
|
| 344 |
+
f"{path}.input_transform.clip requires constraints.order_{input_order}"
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
distortion_limit = _number_or_vector(
|
| 348 |
+
_required(values, "distortion_limit", path),
|
| 349 |
+
width,
|
| 350 |
+
f"{path}.distortion_limit",
|
| 351 |
+
)
|
| 352 |
+
if any(value <= 0.0 for value in _expanded(distortion_limit, width)):
|
| 353 |
+
raise ConfigError(f"{path}.distortion_limit must be positive")
|
| 354 |
+
if space == "so3" and isinstance(distortion_limit, tuple):
|
| 355 |
+
raise ConfigError(
|
| 356 |
+
f"{path}.distortion_limit must be one scalar geodesic angle for SO(3)"
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
primitive_selection = _required(values, "primitive_selection", path)
|
| 360 |
+
if primitive_selection not in _PRIMITIVE_SELECTIONS:
|
| 361 |
+
raise ConfigError(f"{path}.primitive_selection must be nearest or smooth")
|
| 362 |
+
if (
|
| 363 |
+
primitive_selection == "smooth"
|
| 364 |
+
and space == "additive"
|
| 365 |
+
and primitive_order not in {1, 2}
|
| 366 |
+
):
|
| 367 |
+
raise ConfigError(
|
| 368 |
+
f"{path}.primitive_selection='smooth' requires first- or second-order additive primitives"
|
| 369 |
+
)
|
| 370 |
+
if primitive_order == 2 and "order_2" not in constraints:
|
| 371 |
+
raise ConfigError(
|
| 372 |
+
f"{path} second-order primitives require constraints.order_2"
|
| 373 |
+
)
|
| 374 |
+
return cls(
|
| 375 |
+
dims=dims,
|
| 376 |
+
space=space,
|
| 377 |
+
input_order=input_order,
|
| 378 |
+
primitive_order=primitive_order,
|
| 379 |
+
input_transform=input_transform,
|
| 380 |
+
constraints=MappingProxyType(constraints),
|
| 381 |
+
distortion_limit=distortion_limit,
|
| 382 |
+
primitive_selection=str(primitive_selection),
|
| 383 |
+
representation=None if representation is None else str(representation),
|
| 384 |
+
multiplication=None if multiplication is None else str(multiplication),
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
def to_dict(self) -> dict[str, object]:
|
| 388 |
+
payload: dict[str, object] = {
|
| 389 |
+
"dims": _serialize_dims(self.dims),
|
| 390 |
+
"space": self.space,
|
| 391 |
+
"input_order": self.input_order,
|
| 392 |
+
"primitive_order": self.primitive_order,
|
| 393 |
+
"input_transform": self.input_transform.to_dict(),
|
| 394 |
+
"constraints": {
|
| 395 |
+
key: constraint.to_dict()
|
| 396 |
+
for key, constraint in self.constraints.items()
|
| 397 |
+
},
|
| 398 |
+
"distortion_limit": _serialize_number_or_vector(self.distortion_limit),
|
| 399 |
+
"primitive_selection": self.primitive_selection,
|
| 400 |
+
}
|
| 401 |
+
if self.space == "so3":
|
| 402 |
+
payload["representation"] = self.representation
|
| 403 |
+
payload["multiplication"] = self.multiplication
|
| 404 |
+
return payload
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
@dataclass(frozen=True)
|
| 408 |
+
class PhysicalConfig:
|
| 409 |
+
"""Complete, non-overlapping component partition of the input's last axis."""
|
| 410 |
+
|
| 411 |
+
input_dim: int
|
| 412 |
+
components: Mapping[str, ComponentConfig]
|
| 413 |
+
|
| 414 |
+
@classmethod
|
| 415 |
+
def from_dict(cls, payload: object, *, path: str = "physical") -> PhysicalConfig:
|
| 416 |
+
values = _mapping(payload, path)
|
| 417 |
+
_reject_unknown(values, {"input_dim", "components"}, path)
|
| 418 |
+
input_dim = _integer(
|
| 419 |
+
_required(values, "input_dim", path), f"{path}.input_dim", positive=True
|
| 420 |
+
)
|
| 421 |
+
raw_components = _mapping(
|
| 422 |
+
_required(values, "components", path), f"{path}.components"
|
| 423 |
+
)
|
| 424 |
+
if not raw_components:
|
| 425 |
+
raise ConfigError(f"{path}.components must not be empty")
|
| 426 |
+
|
| 427 |
+
components: dict[str, ComponentConfig] = {}
|
| 428 |
+
for name, component_payload in raw_components.items():
|
| 429 |
+
if not name.strip():
|
| 430 |
+
raise ConfigError(f"{path}.components contains an empty component name")
|
| 431 |
+
component = ComponentConfig.from_dict(
|
| 432 |
+
component_payload,
|
| 433 |
+
input_dim=input_dim,
|
| 434 |
+
path=f"{path}.components.{name}",
|
| 435 |
+
)
|
| 436 |
+
components[name] = component
|
| 437 |
+
|
| 438 |
+
validate_dimension_partition(
|
| 439 |
+
((name, component.dims) for name, component in components.items()),
|
| 440 |
+
input_dim,
|
| 441 |
+
label="physical dimension",
|
| 442 |
+
error_type=ConfigError,
|
| 443 |
+
)
|
| 444 |
+
return cls(input_dim=input_dim, components=MappingProxyType(components))
|
| 445 |
+
|
| 446 |
+
def to_dict(self) -> dict[str, object]:
|
| 447 |
+
return {
|
| 448 |
+
"input_dim": self.input_dim,
|
| 449 |
+
"components": {
|
| 450 |
+
name: component.to_dict() for name, component in self.components.items()
|
| 451 |
+
},
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
TOKENIZATION_SCHEMES = ("set_bpe", "bpe")
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
@dataclass(frozen=True)
|
| 459 |
+
class SetBPEConfig:
|
| 460 |
+
"""Runtime settings of the tokenization stage.
|
| 461 |
+
|
| 462 |
+
``scheme`` selects between rooted spatial-temporal Set-BPE (``"set_bpe"``,
|
| 463 |
+
the production default) and plain BPE over the row-major flattened cell
|
| 464 |
+
stream (``"bpe"``), the FAST-style ablation baseline. Both schemes share
|
| 465 |
+
the primitive atoms, the artifact layout, and the vocabulary budget.
|
| 466 |
+
|
| 467 |
+
``lookahead`` caps a Set-BPE temporal token at that many control steps and
|
| 468 |
+
a plain-BPE token at ``lookahead * physical.input_dim`` primitive cells.
|
| 469 |
+
|
| 470 |
+
``allow_cross_component_merge`` is frozen into the artifact. ``False``
|
| 471 |
+
makes each arm/body component a hard spatial and temporal BPE boundary,
|
| 472 |
+
while all components still contribute pooled frequencies to the same
|
| 473 |
+
joint or EEF vocabulary.
|
| 474 |
+
"""
|
| 475 |
+
|
| 476 |
+
lookahead: int
|
| 477 |
+
allow_cross_component_merge: bool = False
|
| 478 |
+
scheme: str = "set_bpe"
|
| 479 |
+
|
| 480 |
+
@classmethod
|
| 481 |
+
def from_dict(cls, payload: object, *, path: str = "set_bpe") -> SetBPEConfig:
|
| 482 |
+
values = _mapping(payload, path)
|
| 483 |
+
_reject_unknown(
|
| 484 |
+
values, {"lookahead", "allow_cross_component_merge", "scheme"}, path
|
| 485 |
+
)
|
| 486 |
+
lookahead = _integer(
|
| 487 |
+
_required(values, "lookahead", path), f"{path}.lookahead", positive=True
|
| 488 |
+
)
|
| 489 |
+
allow_cross = values.get("allow_cross_component_merge", False)
|
| 490 |
+
if not isinstance(allow_cross, bool):
|
| 491 |
+
raise ConfigError(
|
| 492 |
+
f"{path}.allow_cross_component_merge must be true or false"
|
| 493 |
+
)
|
| 494 |
+
scheme = values.get("scheme", "set_bpe")
|
| 495 |
+
if scheme not in TOKENIZATION_SCHEMES:
|
| 496 |
+
raise ConfigError(
|
| 497 |
+
f"{path}.scheme must be one of {', '.join(TOKENIZATION_SCHEMES)}"
|
| 498 |
+
)
|
| 499 |
+
return cls(
|
| 500 |
+
lookahead=lookahead, allow_cross_component_merge=allow_cross, scheme=scheme
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
def to_dict(self) -> dict[str, object]:
|
| 504 |
+
payload: dict[str, object] = {
|
| 505 |
+
"lookahead": self.lookahead,
|
| 506 |
+
"allow_cross_component_merge": self.allow_cross_component_merge,
|
| 507 |
+
}
|
| 508 |
+
# Omitted for the default so every already-published artifact keeps its
|
| 509 |
+
# fingerprint; the field only appears on ablation artifacts.
|
| 510 |
+
if self.scheme != "set_bpe":
|
| 511 |
+
payload["scheme"] = self.scheme
|
| 512 |
+
return payload
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
@dataclass(frozen=True)
|
| 516 |
+
class PrimitiveTrainingConfig:
|
| 517 |
+
"""Serialized config-v1 metadata retained until artifacts are versioned again.
|
| 518 |
+
|
| 519 |
+
Fitting forces exact zero on every primitive axis regardless of this list.
|
| 520 |
+
Keeping strict parsing here preserves the currently published artifact
|
| 521 |
+
fingerprint; the next schema version can remove the field and republish.
|
| 522 |
+
"""
|
| 523 |
+
|
| 524 |
+
zero_primitive_components: tuple[str, ...]
|
| 525 |
+
|
| 526 |
+
@classmethod
|
| 527 |
+
def from_dict(
|
| 528 |
+
cls,
|
| 529 |
+
payload: object,
|
| 530 |
+
*,
|
| 531 |
+
physical: PhysicalConfig,
|
| 532 |
+
path: str = "train.primitives",
|
| 533 |
+
) -> PrimitiveTrainingConfig:
|
| 534 |
+
values = _mapping(payload, path)
|
| 535 |
+
_reject_unknown(values, {"zero_primitive_components"}, path)
|
| 536 |
+
raw_names = _required(values, "zero_primitive_components", path)
|
| 537 |
+
if not isinstance(raw_names, Sequence) or isinstance(
|
| 538 |
+
raw_names, (str, bytes, bytearray)
|
| 539 |
+
):
|
| 540 |
+
raise ConfigError(f"{path}.zero_primitive_components must be a list")
|
| 541 |
+
names = tuple(raw_names)
|
| 542 |
+
if not all(isinstance(name, str) and name for name in names):
|
| 543 |
+
raise ConfigError(
|
| 544 |
+
f"{path}.zero_primitive_components must contain non-empty component names"
|
| 545 |
+
)
|
| 546 |
+
if len(set(names)) != len(names):
|
| 547 |
+
raise ConfigError(f"{path}.zero_primitive_components contains a duplicate")
|
| 548 |
+
unknown = sorted(set(names) - set(physical.components))
|
| 549 |
+
if unknown:
|
| 550 |
+
raise ConfigError(
|
| 551 |
+
f"{path}.zero_primitive_components contains unknown component {unknown[0]!r}"
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
selected = set(names)
|
| 555 |
+
for name, component in physical.components.items():
|
| 556 |
+
if component.space == "additive":
|
| 557 |
+
if component.primitive_order not in {0, 1, 2}:
|
| 558 |
+
raise ConfigError(
|
| 559 |
+
f"{path} cannot train additive primitive_order="
|
| 560 |
+
f"{component.primitive_order} for component {name!r}"
|
| 561 |
+
)
|
| 562 |
+
domain_order = component.primitive_order
|
| 563 |
+
if name in selected and component.primitive_order == 0:
|
| 564 |
+
raise ConfigError(
|
| 565 |
+
f"{path}.zero_primitive_components cannot contain absolute "
|
| 566 |
+
f"component {name!r}"
|
| 567 |
+
)
|
| 568 |
+
else:
|
| 569 |
+
domain_order = component.primitive_order
|
| 570 |
+
if f"order_{domain_order}" not in component.constraints:
|
| 571 |
+
raise ConfigError(
|
| 572 |
+
f"physical component {name!r} needs constraints.order_{domain_order} "
|
| 573 |
+
"to train its primitives"
|
| 574 |
+
)
|
| 575 |
+
return cls(zero_primitive_components=names)
|
| 576 |
+
|
| 577 |
+
def to_dict(self) -> dict[str, object]:
|
| 578 |
+
return {"zero_primitive_components": list(self.zero_primitive_components)}
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
@dataclass(frozen=True)
|
| 582 |
+
class BPETrainingConfig:
|
| 583 |
+
"""Spatial and final vocabulary budgets used for whole-trajectory BPE.
|
| 584 |
+
|
| 585 |
+
``spatial_vocab_size`` splits the budget between the two Set-BPE stages and
|
| 586 |
+
is unused by the plain-BPE scheme, which has a single merge stage.
|
| 587 |
+
"""
|
| 588 |
+
|
| 589 |
+
spatial_vocab_size: int
|
| 590 |
+
vocab_size: int
|
| 591 |
+
|
| 592 |
+
@classmethod
|
| 593 |
+
def from_dict(
|
| 594 |
+
cls, payload: object, *, path: str = "train.bpe"
|
| 595 |
+
) -> BPETrainingConfig:
|
| 596 |
+
values = _mapping(payload, path)
|
| 597 |
+
_reject_unknown(values, {"spatial_vocab_size", "vocab_size"}, path)
|
| 598 |
+
spatial_vocab_size = _integer(
|
| 599 |
+
_required(values, "spatial_vocab_size", path),
|
| 600 |
+
f"{path}.spatial_vocab_size",
|
| 601 |
+
positive=True,
|
| 602 |
+
)
|
| 603 |
+
vocab_size = _integer(
|
| 604 |
+
_required(values, "vocab_size", path), f"{path}.vocab_size", positive=True
|
| 605 |
+
)
|
| 606 |
+
if spatial_vocab_size > vocab_size:
|
| 607 |
+
raise ConfigError(f"{path}.spatial_vocab_size must not exceed vocab_size")
|
| 608 |
+
return cls(spatial_vocab_size=spatial_vocab_size, vocab_size=vocab_size)
|
| 609 |
+
|
| 610 |
+
def to_dict(self) -> dict[str, object]:
|
| 611 |
+
return {
|
| 612 |
+
"spatial_vocab_size": self.spatial_vocab_size,
|
| 613 |
+
"vocab_size": self.vocab_size,
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
|
| 617 |
+
@dataclass(frozen=True)
|
| 618 |
+
class QualityControlConfig:
|
| 619 |
+
"""Pretraining-only policy for extreme first-order discontinuities.
|
| 620 |
+
|
| 621 |
+
``constraints.order_1`` remains the primitive domain and therefore the
|
| 622 |
+
region with the configured distortion guarantee. Values outside that
|
| 623 |
+
domain are still encodable through saturated primitives and causal error
|
| 624 |
+
feedback. Only steps beyond ``hard_order_1_scale`` times the primitive
|
| 625 |
+
domain are treated as data discontinuities and split before BPE fitting.
|
| 626 |
+
"""
|
| 627 |
+
|
| 628 |
+
hard_order_1_scale: float
|
| 629 |
+
min_segment_length: int
|
| 630 |
+
|
| 631 |
+
@classmethod
|
| 632 |
+
def from_dict(
|
| 633 |
+
cls, payload: object, *, path: str = "train.quality_control"
|
| 634 |
+
) -> QualityControlConfig:
|
| 635 |
+
values = _mapping(payload, path)
|
| 636 |
+
_reject_unknown(values, {"hard_order_1_scale", "min_segment_length"}, path)
|
| 637 |
+
scale = _number(
|
| 638 |
+
_required(values, "hard_order_1_scale", path),
|
| 639 |
+
f"{path}.hard_order_1_scale",
|
| 640 |
+
)
|
| 641 |
+
if scale <= 1.0:
|
| 642 |
+
raise ConfigError(f"{path}.hard_order_1_scale must be greater than 1")
|
| 643 |
+
return cls(
|
| 644 |
+
hard_order_1_scale=scale,
|
| 645 |
+
min_segment_length=_integer(
|
| 646 |
+
_required(values, "min_segment_length", path),
|
| 647 |
+
f"{path}.min_segment_length",
|
| 648 |
+
positive=True,
|
| 649 |
+
),
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
+
def to_dict(self) -> dict[str, object]:
|
| 653 |
+
return {
|
| 654 |
+
"hard_order_1_scale": self.hard_order_1_scale,
|
| 655 |
+
"min_segment_length": self.min_segment_length,
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
@dataclass(frozen=True)
|
| 660 |
+
class TrainConfig:
|
| 661 |
+
"""Non-derivable fit choices and BPE budgets.
|
| 662 |
+
|
| 663 |
+
Physical domains and distortion limits are intentionally absent: primitive
|
| 664 |
+
construction derives them from :class:`PhysicalConfig`, preventing two
|
| 665 |
+
conflicting sources of truth.
|
| 666 |
+
"""
|
| 667 |
+
|
| 668 |
+
primitives: PrimitiveTrainingConfig
|
| 669 |
+
bpe: BPETrainingConfig
|
| 670 |
+
quality_control: QualityControlConfig | None = None
|
| 671 |
+
|
| 672 |
+
@classmethod
|
| 673 |
+
def from_dict(
|
| 674 |
+
cls,
|
| 675 |
+
payload: object,
|
| 676 |
+
*,
|
| 677 |
+
physical: PhysicalConfig,
|
| 678 |
+
path: str = "train",
|
| 679 |
+
) -> TrainConfig:
|
| 680 |
+
values = _mapping(payload, path)
|
| 681 |
+
_reject_unknown(values, {"primitives", "bpe", "quality_control"}, path)
|
| 682 |
+
raw_quality_control = values.get("quality_control")
|
| 683 |
+
return cls(
|
| 684 |
+
primitives=PrimitiveTrainingConfig.from_dict(
|
| 685 |
+
_required(values, "primitives", path), physical=physical
|
| 686 |
+
),
|
| 687 |
+
bpe=BPETrainingConfig.from_dict(_required(values, "bpe", path)),
|
| 688 |
+
quality_control=(
|
| 689 |
+
None
|
| 690 |
+
if raw_quality_control is None
|
| 691 |
+
else QualityControlConfig.from_dict(raw_quality_control)
|
| 692 |
+
),
|
| 693 |
+
)
|
| 694 |
+
|
| 695 |
+
def to_dict(self) -> dict[str, object]:
|
| 696 |
+
payload = {"primitives": self.primitives.to_dict(), "bpe": self.bpe.to_dict()}
|
| 697 |
+
if self.quality_control is not None:
|
| 698 |
+
payload["quality_control"] = self.quality_control.to_dict()
|
| 699 |
+
return payload
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
@dataclass(frozen=True)
|
| 703 |
+
class ActionCodec2Config:
|
| 704 |
+
"""Strict YAML configuration for an ActionCodec2 processor artifact.
|
| 705 |
+
|
| 706 |
+
``physical`` and ``set_bpe`` are required at deployment. ``train`` is an
|
| 707 |
+
optional reproducibility recipe and is never consulted by encode/decode.
|
| 708 |
+
This is a processor configuration, not a neural ``PretrainedConfig``;
|
| 709 |
+
``config.json`` exists only as Hugging Face AutoProcessor discovery metadata.
|
| 710 |
+
"""
|
| 711 |
+
|
| 712 |
+
physical: PhysicalConfig
|
| 713 |
+
set_bpe: SetBPEConfig
|
| 714 |
+
train: TrainConfig | None = None
|
| 715 |
+
model_type: str = MODEL_TYPE
|
| 716 |
+
config_version: int = CONFIG_VERSION
|
| 717 |
+
|
| 718 |
+
@classmethod
|
| 719 |
+
def from_dict(cls, payload: object) -> ActionCodec2Config:
|
| 720 |
+
values = _mapping(payload, "config")
|
| 721 |
+
_reject_unknown(
|
| 722 |
+
values,
|
| 723 |
+
{"model_type", "config_version", "physical", "set_bpe", "train"},
|
| 724 |
+
"config",
|
| 725 |
+
)
|
| 726 |
+
model_type = _required(values, "model_type", "config")
|
| 727 |
+
if model_type != MODEL_TYPE:
|
| 728 |
+
raise ConfigError(f"config.model_type must be {MODEL_TYPE!r}")
|
| 729 |
+
config_version = _integer(
|
| 730 |
+
_required(values, "config_version", "config"), "config.config_version"
|
| 731 |
+
)
|
| 732 |
+
if config_version != CONFIG_VERSION:
|
| 733 |
+
raise ConfigError(
|
| 734 |
+
f"unsupported config_version {config_version}; expected {CONFIG_VERSION}"
|
| 735 |
+
)
|
| 736 |
+
physical = PhysicalConfig.from_dict(_required(values, "physical", "config"))
|
| 737 |
+
raw_train = values.get("train")
|
| 738 |
+
return cls(
|
| 739 |
+
physical=physical,
|
| 740 |
+
set_bpe=SetBPEConfig.from_dict(_required(values, "set_bpe", "config")),
|
| 741 |
+
train=(
|
| 742 |
+
None
|
| 743 |
+
if raw_train is None
|
| 744 |
+
else TrainConfig.from_dict(raw_train, physical=physical)
|
| 745 |
+
),
|
| 746 |
+
model_type=str(model_type),
|
| 747 |
+
config_version=config_version,
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
@classmethod
|
| 751 |
+
def from_yaml(cls, path: str | Path) -> ActionCodec2Config:
|
| 752 |
+
source = Path(path)
|
| 753 |
+
try:
|
| 754 |
+
payload = yaml.load(
|
| 755 |
+
source.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader
|
| 756 |
+
)
|
| 757 |
+
except OSError:
|
| 758 |
+
raise
|
| 759 |
+
except yaml.YAMLError as error:
|
| 760 |
+
raise ConfigError(f"cannot parse {source}: {error}") from error
|
| 761 |
+
return cls.from_dict(payload)
|
| 762 |
+
|
| 763 |
+
@classmethod
|
| 764 |
+
def from_pretrained(cls, directory: str | Path) -> ActionCodec2Config:
|
| 765 |
+
"""Load the strict numerical configuration from ``config.yaml``."""
|
| 766 |
+
return cls.from_yaml(Path(directory) / CONFIG_NAME)
|
| 767 |
+
|
| 768 |
+
def to_dict(self) -> dict[str, object]:
|
| 769 |
+
payload: dict[str, object] = {
|
| 770 |
+
"model_type": self.model_type,
|
| 771 |
+
"config_version": self.config_version,
|
| 772 |
+
"physical": self.physical.to_dict(),
|
| 773 |
+
"set_bpe": self.set_bpe.to_dict(),
|
| 774 |
+
}
|
| 775 |
+
if self.train is not None:
|
| 776 |
+
payload["train"] = self.train.to_dict()
|
| 777 |
+
return payload
|
| 778 |
+
|
| 779 |
+
def save_yaml(self, path: str | Path) -> None:
|
| 780 |
+
destination = Path(path)
|
| 781 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 782 |
+
destination.write_text(
|
| 783 |
+
yaml.safe_dump(
|
| 784 |
+
self.to_dict(),
|
| 785 |
+
sort_keys=False,
|
| 786 |
+
allow_unicode=True,
|
| 787 |
+
default_flow_style=False,
|
| 788 |
+
),
|
| 789 |
+
encoding="utf-8",
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
def save_pretrained(self, directory: str | Path) -> None:
|
| 793 |
+
"""Write ``config.yaml`` without embedding primitive or BPE arrays."""
|
| 794 |
+
self.save_yaml(Path(directory) / CONFIG_NAME)
|
runtime/physical/physical_codec.py
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration-driven physical quantization.
|
| 2 |
+
|
| 3 |
+
This module owns only the lossy physical stage. Components receive transformed
|
| 4 |
+
actions and emit one primitive cell per global action dimension; Set-BPE never
|
| 5 |
+
appears here. Batch dimensions are vectorized, while the short time recursion
|
| 6 |
+
remains explicit because error feedback is inherently causal.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import itertools
|
| 12 |
+
import os
|
| 13 |
+
from typing import Protocol
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from ._so3 import conjugate_quaternion as _conjugate
|
| 18 |
+
from ._so3 import euler_xyz_to_quaternion as _euler_xyz_to_quaternion
|
| 19 |
+
from ._so3 import multiply_quaternions as _multiply
|
| 20 |
+
from ._so3 import quaternion_to_euler_xyz as _quaternion_to_euler_xyz
|
| 21 |
+
from ._so3 import quaternion_to_rotvec as _quaternion_to_rotvec
|
| 22 |
+
from ._so3 import rotvec_to_quaternion as _rotvec_to_quaternion
|
| 23 |
+
from ..common._validation import broadcast_vector
|
| 24 |
+
from .configuration import ActionCodec2Config, ComponentConfig, ConfigError
|
| 25 |
+
from ..backend.native_backend import require_native, resolve_backend
|
| 26 |
+
from .primitives import PrimitiveStore
|
| 27 |
+
|
| 28 |
+
_FLOAT32_SLACK = np.float32(8 * np.finfo(np.float32).eps)
|
| 29 |
+
_IDENTITY = np.asarray((0.0, 0.0, 0.0, 1.0), dtype=np.float64)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _constraint(
|
| 33 |
+
component: ComponentConfig, order: int
|
| 34 |
+
) -> tuple[np.ndarray, np.ndarray] | None:
|
| 35 |
+
value = component.constraints.get(f"order_{order}")
|
| 36 |
+
if value is None:
|
| 37 |
+
return None
|
| 38 |
+
return (
|
| 39 |
+
np.asarray(value.minimum, dtype=np.float32),
|
| 40 |
+
np.asarray(value.maximum, dtype=np.float32),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _scale(component: ComponentConfig) -> np.ndarray:
|
| 45 |
+
return broadcast_vector(
|
| 46 |
+
component.input_transform.scale,
|
| 47 |
+
len(component.dims),
|
| 48 |
+
dtype=np.float32,
|
| 49 |
+
name="input_transform.scale",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _transform(component: ComponentConfig, actions: np.ndarray) -> np.ndarray:
|
| 54 |
+
"""Apply the component's unit conversion and encoder-only clipping."""
|
| 55 |
+
values = np.asarray(actions, dtype=np.float32) * _scale(component)
|
| 56 |
+
if component.input_transform.clip:
|
| 57 |
+
bounds = _constraint(component, component.input_order)
|
| 58 |
+
if bounds is None: # defensive: configuration validation normally catches this
|
| 59 |
+
raise ConfigError(
|
| 60 |
+
f"clipping requires constraints.order_{component.input_order}"
|
| 61 |
+
)
|
| 62 |
+
values = np.clip(values, bounds[0], bounds[1])
|
| 63 |
+
return np.asarray(values, dtype=np.float32)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _integrate_additive(values: np.ndarray, order: int) -> np.ndarray:
|
| 67 |
+
"""Integrate along the penultimate (time) axis from a zero boundary."""
|
| 68 |
+
result = np.asarray(values, dtype=np.float32)
|
| 69 |
+
for _ in range(order):
|
| 70 |
+
result = np.cumsum(result, axis=-2, dtype=np.float32)
|
| 71 |
+
return result
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _difference_additive(values: np.ndarray, order: int) -> np.ndarray:
|
| 75 |
+
"""Take zero-anchored finite differences along the time axis."""
|
| 76 |
+
result = np.asarray(values, dtype=np.float32)
|
| 77 |
+
for _ in range(order):
|
| 78 |
+
previous = np.concatenate(
|
| 79 |
+
(np.zeros_like(result[..., :1, :]), result[..., :-1, :]), axis=-2
|
| 80 |
+
)
|
| 81 |
+
result = result - previous
|
| 82 |
+
return result
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class _ComponentRuntime(Protocol):
|
| 86 |
+
"""Internal interface shared by additive and SO(3) components."""
|
| 87 |
+
|
| 88 |
+
def encode_batch(
|
| 89 |
+
self, actions: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 90 |
+
) -> np.ndarray: ...
|
| 91 |
+
|
| 92 |
+
def decode_batch(
|
| 93 |
+
self, cells: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 94 |
+
) -> np.ndarray: ...
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class _AdditiveRuntime:
|
| 98 |
+
"""Causal Euclidean error-feedback quantizer for one component."""
|
| 99 |
+
|
| 100 |
+
def __init__(
|
| 101 |
+
self, component: ComponentConfig, primitives: tuple[np.ndarray, ...]
|
| 102 |
+
) -> None:
|
| 103 |
+
self.config = component
|
| 104 |
+
self.primitives = tuple(
|
| 105 |
+
np.asarray(value, dtype=np.float32) for value in primitives
|
| 106 |
+
)
|
| 107 |
+
self.width = len(component.dims)
|
| 108 |
+
self.order = component.primitive_order
|
| 109 |
+
self._second_order_axes = (
|
| 110 |
+
tuple(axis.astype(np.float64) for axis in self.primitives)
|
| 111 |
+
if self.order == 2
|
| 112 |
+
else ()
|
| 113 |
+
)
|
| 114 |
+
self._second_order_sorted = tuple(
|
| 115 |
+
bool(np.all(axis[1:] > axis[:-1])) for axis in self._second_order_axes
|
| 116 |
+
)
|
| 117 |
+
self._second_order_zero_bounds = tuple(
|
| 118 |
+
(
|
| 119 |
+
int(np.searchsorted(axis, 0)),
|
| 120 |
+
int(np.searchsorted(axis, 0, side="right")) - 1,
|
| 121 |
+
)
|
| 122 |
+
if ordered
|
| 123 |
+
else (0, 0)
|
| 124 |
+
for axis, ordered in zip(
|
| 125 |
+
self._second_order_axes, self._second_order_sorted, strict=True
|
| 126 |
+
)
|
| 127 |
+
)
|
| 128 |
+
raw_position_bounds = _constraint(component, 0)
|
| 129 |
+
self.position_bounds = (
|
| 130 |
+
None
|
| 131 |
+
if raw_position_bounds is None
|
| 132 |
+
else (
|
| 133 |
+
np.broadcast_to(raw_position_bounds[0], (self.width,)).copy(),
|
| 134 |
+
np.broadcast_to(raw_position_bounds[1], (self.width,)).copy(),
|
| 135 |
+
)
|
| 136 |
+
)
|
| 137 |
+
self.limit = broadcast_vector(
|
| 138 |
+
component.distortion_limit,
|
| 139 |
+
self.width,
|
| 140 |
+
dtype=np.float32,
|
| 141 |
+
name="distortion_limit",
|
| 142 |
+
)
|
| 143 |
+
table_width = max(len(values) for values in self.primitives)
|
| 144 |
+
table = np.full((self.width, table_width), np.inf, dtype=np.float32)
|
| 145 |
+
for coordinate, values in enumerate(self.primitives):
|
| 146 |
+
table[coordinate, : len(values)] = values
|
| 147 |
+
self._native_table = np.ascontiguousarray(table)
|
| 148 |
+
self._native_bins = np.asarray(
|
| 149 |
+
[len(values) for values in self.primitives], dtype=np.int32
|
| 150 |
+
)
|
| 151 |
+
self._native_binary = np.zeros(self.width, dtype=np.uint8)
|
| 152 |
+
|
| 153 |
+
def _pick_batch(self, request: np.ndarray, direction: np.ndarray) -> np.ndarray:
|
| 154 |
+
"""Select cells for an arbitrary leading batch shape."""
|
| 155 |
+
flat_request = np.asarray(request, dtype=np.float32).reshape(-1, self.width)
|
| 156 |
+
flat_direction = np.asarray(direction, dtype=np.float32).reshape(-1, self.width)
|
| 157 |
+
choice = np.empty(flat_request.shape, dtype=np.int64)
|
| 158 |
+
for coordinate, primitives in enumerate(self.primitives):
|
| 159 |
+
distance = np.abs(flat_request[:, coordinate, None] - primitives[None, :])
|
| 160 |
+
nearest = np.argmin(distance, axis=1)
|
| 161 |
+
if self.config.primitive_selection == "nearest":
|
| 162 |
+
choice[:, coordinate] = nearest
|
| 163 |
+
continue
|
| 164 |
+
feasible = distance <= self.limit[coordinate] + _FLOAT32_SLACK
|
| 165 |
+
preferred = feasible & (
|
| 166 |
+
np.sign(primitives)[None, :] * flat_direction[:, coordinate, None]
|
| 167 |
+
>= 0.0
|
| 168 |
+
)
|
| 169 |
+
candidates = np.where(
|
| 170 |
+
np.any(preferred, axis=1, keepdims=True), preferred, feasible
|
| 171 |
+
)
|
| 172 |
+
smooth = np.argmin(np.where(candidates, distance, np.inf), axis=1)
|
| 173 |
+
choice[:, coordinate] = np.where(np.any(feasible, axis=1), smooth, nearest)
|
| 174 |
+
return choice.reshape(request.shape)
|
| 175 |
+
|
| 176 |
+
def _advance_batch(self, state: np.ndarray, primitive: np.ndarray) -> None:
|
| 177 |
+
"""Advance all integrator states in-place by one timestep."""
|
| 178 |
+
higher = primitive
|
| 179 |
+
for level in range(self.order - 1, -1, -1):
|
| 180 |
+
state[:, level] += higher
|
| 181 |
+
higher = state[:, level]
|
| 182 |
+
if self.order == 1 and self.position_bounds is not None:
|
| 183 |
+
state[:, 0] = np.clip(
|
| 184 |
+
state[:, 0], self.position_bounds[0], self.position_bounds[1]
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
def encode_batch(
|
| 188 |
+
self, actions: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 189 |
+
) -> np.ndarray:
|
| 190 |
+
"""Quantize ``[B,T,d]`` actions, vectorizing all trajectories per step."""
|
| 191 |
+
if self.order == 2:
|
| 192 |
+
return self._encode_second_order(actions, previous_first_order)
|
| 193 |
+
transformed = _transform(self.config, actions)
|
| 194 |
+
target = _integrate_additive(transformed, self.config.input_order)
|
| 195 |
+
batch, steps, _ = target.shape
|
| 196 |
+
if self.order == 0:
|
| 197 |
+
return self._pick_batch(target, np.zeros_like(target))
|
| 198 |
+
if self.position_bounds is not None and resolve_backend("auto") == "native":
|
| 199 |
+
starts = np.zeros((batch, self.width), dtype=np.float32)
|
| 200 |
+
cells = require_native().physical_encode_batch(
|
| 201 |
+
self._native_table,
|
| 202 |
+
self._native_bins,
|
| 203 |
+
self._native_binary,
|
| 204 |
+
np.ascontiguousarray(self.position_bounds[0], dtype=np.float32),
|
| 205 |
+
np.ascontiguousarray(self.position_bounds[1], dtype=np.float32),
|
| 206 |
+
np.ascontiguousarray(target, dtype=np.float32),
|
| 207 |
+
starts,
|
| 208 |
+
starts,
|
| 209 |
+
self.order,
|
| 210 |
+
min(batch, os.cpu_count() or 1),
|
| 211 |
+
np.ascontiguousarray(self.limit, dtype=np.float32),
|
| 212 |
+
self.config.primitive_selection == "smooth",
|
| 213 |
+
)
|
| 214 |
+
return np.asarray(cells, dtype=np.int64)
|
| 215 |
+
|
| 216 |
+
cells = np.empty(target.shape, dtype=np.int64)
|
| 217 |
+
state = np.zeros((batch, self.order, self.width), dtype=np.float32)
|
| 218 |
+
direction = np.zeros((batch, self.width), dtype=np.float32)
|
| 219 |
+
zero = np.zeros((batch, self.width), dtype=np.float32)
|
| 220 |
+
for step in range(steps):
|
| 221 |
+
baseline = state.copy()
|
| 222 |
+
self._advance_batch(baseline, zero)
|
| 223 |
+
request = target[:, step] - baseline[:, 0]
|
| 224 |
+
choice = self._pick_batch(request, direction)
|
| 225 |
+
primitive = np.stack(
|
| 226 |
+
[
|
| 227 |
+
self.primitives[index][choice[:, index]]
|
| 228 |
+
for index in range(self.width)
|
| 229 |
+
],
|
| 230 |
+
axis=1,
|
| 231 |
+
)
|
| 232 |
+
self._advance_batch(state, primitive)
|
| 233 |
+
moved = primitive != 0.0
|
| 234 |
+
direction = np.where(moved, np.sign(primitive), direction)
|
| 235 |
+
cells[:, step] = choice
|
| 236 |
+
return cells
|
| 237 |
+
|
| 238 |
+
def decode_batch(
|
| 239 |
+
self, cells: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 240 |
+
) -> np.ndarray:
|
| 241 |
+
"""Decode ``[B,T,d]`` cells to actions in the component's input units."""
|
| 242 |
+
if self.order == 2:
|
| 243 |
+
return self._decode_second_order(cells, previous_first_order)
|
| 244 |
+
indices = np.asarray(cells, dtype=np.int64)
|
| 245 |
+
if self.order == 0:
|
| 246 |
+
positions = np.stack(
|
| 247 |
+
[
|
| 248 |
+
self.primitives[index][indices[:, :, index]]
|
| 249 |
+
for index in range(self.width)
|
| 250 |
+
],
|
| 251 |
+
axis=2,
|
| 252 |
+
)
|
| 253 |
+
elif self.position_bounds is not None and resolve_backend("auto") == "native":
|
| 254 |
+
batch = len(indices)
|
| 255 |
+
starts = np.zeros((batch, self.width), dtype=np.float32)
|
| 256 |
+
positions = require_native().physical_decode_batch(
|
| 257 |
+
self._native_table,
|
| 258 |
+
self._native_bins,
|
| 259 |
+
self._native_binary,
|
| 260 |
+
np.ascontiguousarray(self.position_bounds[0], dtype=np.float32),
|
| 261 |
+
np.ascontiguousarray(self.position_bounds[1], dtype=np.float32),
|
| 262 |
+
np.ascontiguousarray(indices, dtype=np.int32),
|
| 263 |
+
starts,
|
| 264 |
+
starts,
|
| 265 |
+
self.order,
|
| 266 |
+
min(batch, os.cpu_count() or 1),
|
| 267 |
+
)
|
| 268 |
+
else:
|
| 269 |
+
picked = np.stack(
|
| 270 |
+
[
|
| 271 |
+
self.primitives[index][indices[:, :, index]]
|
| 272 |
+
for index in range(self.width)
|
| 273 |
+
],
|
| 274 |
+
axis=2,
|
| 275 |
+
)
|
| 276 |
+
batch, steps, _ = indices.shape
|
| 277 |
+
positions = np.empty(indices.shape, dtype=np.float32)
|
| 278 |
+
state = np.zeros((batch, self.order, self.width), dtype=np.float32)
|
| 279 |
+
for step in range(steps):
|
| 280 |
+
self._advance_batch(state, picked[:, step])
|
| 281 |
+
positions[:, step] = state[:, 0]
|
| 282 |
+
actions = _difference_additive(positions, self.config.input_order)
|
| 283 |
+
return np.asarray(actions / _scale(self.config), dtype=np.float32)
|
| 284 |
+
|
| 285 |
+
def _encode_second_order(
|
| 286 |
+
self, actions: np.ndarray, previous_first_order: np.ndarray
|
| 287 |
+
) -> np.ndarray:
|
| 288 |
+
"""Track positions from a supplied ``[B,d]`` reconstructed displacement.
|
| 289 |
+
|
| 290 |
+
Double integration uses float64 to avoid horizon-dependent float32
|
| 291 |
+
cancellation. Source clipping never changes the shared boundary.
|
| 292 |
+
"""
|
| 293 |
+
transformed = _transform(self.config, actions).astype(np.float64)
|
| 294 |
+
velocity = previous_first_order.astype(np.float64) * _scale(self.config)
|
| 295 |
+
target = transformed
|
| 296 |
+
if self.config.input_order == 2:
|
| 297 |
+
target = np.cumsum(target, axis=1) + velocity[:, None]
|
| 298 |
+
if self.config.input_order >= 1:
|
| 299 |
+
target = np.cumsum(target, axis=1)
|
| 300 |
+
if resolve_backend("auto") == "native" and hasattr(
|
| 301 |
+
require_native(), "additive_second_order_encode_batch"
|
| 302 |
+
):
|
| 303 |
+
return np.asarray(
|
| 304 |
+
require_native().additive_second_order_encode_batch(
|
| 305 |
+
self._native_table,
|
| 306 |
+
self._native_bins,
|
| 307 |
+
np.ascontiguousarray(target),
|
| 308 |
+
np.ascontiguousarray(velocity),
|
| 309 |
+
self.limit,
|
| 310 |
+
self.config.primitive_selection == "smooth",
|
| 311 |
+
min(
|
| 312 |
+
len(actions),
|
| 313 |
+
max(
|
| 314 |
+
1,
|
| 315 |
+
int(os.environ.get("ACTIONCODEC2_PHYSICAL_THREADS", "16")),
|
| 316 |
+
),
|
| 317 |
+
),
|
| 318 |
+
),
|
| 319 |
+
dtype=np.int64,
|
| 320 |
+
)
|
| 321 |
+
position = np.zeros_like(velocity)
|
| 322 |
+
direction = np.zeros_like(velocity)
|
| 323 |
+
cells = np.empty(actions.shape, dtype=np.int64)
|
| 324 |
+
for step in range(actions.shape[1]):
|
| 325 |
+
request = target[:, step] - (position + velocity)
|
| 326 |
+
# Keep the feedback calculation in double precision, including
|
| 327 |
+
# distances near the boundary of the admissible error interval.
|
| 328 |
+
choice = self._pick_second_order(request, direction)
|
| 329 |
+
primitive = np.stack(
|
| 330 |
+
[axis[choice[:, j]] for j, axis in enumerate(self.primitives)], axis=1
|
| 331 |
+
)
|
| 332 |
+
velocity += primitive
|
| 333 |
+
position += velocity
|
| 334 |
+
direction = np.where(primitive != 0, np.sign(primitive), direction)
|
| 335 |
+
cells[:, step] = choice
|
| 336 |
+
return cells
|
| 337 |
+
|
| 338 |
+
def _pick_second_order(
|
| 339 |
+
self, request: np.ndarray, direction: np.ndarray
|
| 340 |
+
) -> np.ndarray:
|
| 341 |
+
choice = np.empty(request.shape, dtype=np.int64)
|
| 342 |
+
for j, axis in enumerate(self._second_order_axes):
|
| 343 |
+
# A tiny nearest-only table is faster as one NumPy reduction.
|
| 344 |
+
if self._second_order_sorted[j] and (
|
| 345 |
+
self.config.primitive_selection == "smooth" or len(axis) > 32
|
| 346 |
+
):
|
| 347 |
+
right = np.searchsorted(axis, request[:, j])
|
| 348 |
+
left = np.clip(right - 1, 0, len(axis) - 1)
|
| 349 |
+
right = np.minimum(right, len(axis) - 1)
|
| 350 |
+
nearest = np.where(
|
| 351 |
+
np.abs(request[:, j] - axis[left])
|
| 352 |
+
<= np.abs(request[:, j] - axis[right]),
|
| 353 |
+
left,
|
| 354 |
+
right,
|
| 355 |
+
)
|
| 356 |
+
# Very large requests can round several distances to the same
|
| 357 |
+
# float64 value. Preserve argmin's first-cell tie-break there.
|
| 358 |
+
tied = (nearest > 0) & (
|
| 359 |
+
np.abs(request[:, j] - axis[np.maximum(nearest - 1, 0)])
|
| 360 |
+
== np.abs(request[:, j] - axis[nearest])
|
| 361 |
+
)
|
| 362 |
+
if np.any(tied):
|
| 363 |
+
nearest[tied] = np.argmin(
|
| 364 |
+
np.abs(request[tied, j, None] - axis), axis=1
|
| 365 |
+
)
|
| 366 |
+
if self.config.primitive_selection == "smooth":
|
| 367 |
+
nonnegative, nonpositive = self._second_order_zero_bounds[j]
|
| 368 |
+
preferred = np.where(
|
| 369 |
+
direction[:, j] > 0,
|
| 370 |
+
np.maximum(nearest, min(nonnegative, len(axis) - 1)),
|
| 371 |
+
np.where(
|
| 372 |
+
direction[:, j] < 0,
|
| 373 |
+
np.minimum(nearest, max(nonpositive, 0)),
|
| 374 |
+
nearest,
|
| 375 |
+
),
|
| 376 |
+
)
|
| 377 |
+
admissible = (np.sign(axis[preferred]) * direction[:, j] >= 0) & (
|
| 378 |
+
np.abs(request[:, j] - axis[preferred]) <= self.limit[j]
|
| 379 |
+
)
|
| 380 |
+
nearest = np.where(admissible, preferred, nearest)
|
| 381 |
+
choice[:, j] = nearest
|
| 382 |
+
continue
|
| 383 |
+
# Arbitrary stored cell order retains the original ID tie-break.
|
| 384 |
+
distance = np.abs(request[:, j, None] - axis)
|
| 385 |
+
nearest = np.argmin(distance, axis=1)
|
| 386 |
+
if self.config.primitive_selection == "nearest":
|
| 387 |
+
choice[:, j] = nearest
|
| 388 |
+
continue
|
| 389 |
+
feasible = distance <= self.limit[j]
|
| 390 |
+
preferred = feasible & (np.sign(axis)[None] * direction[:, j, None] >= 0)
|
| 391 |
+
candidates = np.where(
|
| 392 |
+
preferred.any(axis=1, keepdims=True), preferred, feasible
|
| 393 |
+
)
|
| 394 |
+
smooth = np.argmin(np.where(candidates, distance, np.inf), axis=1)
|
| 395 |
+
choice[:, j] = np.where(feasible.any(axis=1), smooth, nearest)
|
| 396 |
+
return choice
|
| 397 |
+
|
| 398 |
+
def _decode_second_order(
|
| 399 |
+
self, cells: np.ndarray, previous_first_order: np.ndarray
|
| 400 |
+
) -> np.ndarray:
|
| 401 |
+
"""Reconstruct ``[B,T,d]`` at the configured input differential order."""
|
| 402 |
+
picked = np.stack(
|
| 403 |
+
[axis[cells[:, :, j]] for j, axis in enumerate(self.primitives)], axis=2
|
| 404 |
+
).astype(np.float64)
|
| 405 |
+
if self.config.input_order == 2:
|
| 406 |
+
result = picked
|
| 407 |
+
else:
|
| 408 |
+
result = np.cumsum(picked, axis=1) + previous_first_order[:, None] * _scale(
|
| 409 |
+
self.config
|
| 410 |
+
)
|
| 411 |
+
if self.config.input_order == 0:
|
| 412 |
+
result = np.cumsum(result, axis=1)
|
| 413 |
+
return np.asarray(result / _scale(self.config), dtype=np.float32)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
class _SO3Runtime:
|
| 417 |
+
"""Intrinsic rotation quantizer with left- or right-multiplicative dynamics."""
|
| 418 |
+
|
| 419 |
+
def __init__(
|
| 420 |
+
self, component: ComponentConfig, primitives: tuple[np.ndarray, ...]
|
| 421 |
+
) -> None:
|
| 422 |
+
self.config = component
|
| 423 |
+
self.primitives = tuple(
|
| 424 |
+
np.asarray(value, dtype=np.float64) for value in primitives
|
| 425 |
+
)
|
| 426 |
+
self.left = component.multiplication == "left"
|
| 427 |
+
self.limit = float(component.distortion_limit)
|
| 428 |
+
if component.primitive_order == 2:
|
| 429 |
+
bounds = component.constraints["order_2"]
|
| 430 |
+
corner = np.maximum(np.abs(bounds.minimum), np.abs(bounds.maximum))
|
| 431 |
+
corner = np.broadcast_to(corner, (3,))
|
| 432 |
+
radius = (
|
| 433 |
+
np.linalg.norm(corner)
|
| 434 |
+
if component.representation == "rotation_vector"
|
| 435 |
+
else np.sum(corner)
|
| 436 |
+
)
|
| 437 |
+
self.second_order_radius = min(np.pi, float(radius) + 3 * self.limit)
|
| 438 |
+
table_width = max(len(values) for values in self.primitives)
|
| 439 |
+
table = np.full((3, table_width), np.inf, dtype=np.float64)
|
| 440 |
+
for coordinate, values in enumerate(self.primitives):
|
| 441 |
+
table[coordinate, : len(values)] = values
|
| 442 |
+
self._native_table = table
|
| 443 |
+
self._native_bins = np.asarray(
|
| 444 |
+
[len(values) for values in self.primitives], dtype=np.int32
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
def _from_representation(self, values: np.ndarray) -> np.ndarray:
|
| 448 |
+
shape = values.shape[:-1]
|
| 449 |
+
flat = np.asarray(values).reshape(-1, 3)
|
| 450 |
+
converted = (
|
| 451 |
+
_rotvec_to_quaternion(flat)
|
| 452 |
+
if self.config.representation == "rotation_vector"
|
| 453 |
+
else _euler_xyz_to_quaternion(flat)
|
| 454 |
+
)
|
| 455 |
+
return converted.reshape(*shape, 4)
|
| 456 |
+
|
| 457 |
+
def _to_representation(self, values: np.ndarray) -> np.ndarray:
|
| 458 |
+
shape = values.shape[:-1]
|
| 459 |
+
flat = np.asarray(values).reshape(-1, 4)
|
| 460 |
+
converted = (
|
| 461 |
+
_quaternion_to_rotvec(flat)
|
| 462 |
+
if self.config.representation == "rotation_vector"
|
| 463 |
+
else _quaternion_to_euler_xyz(flat)
|
| 464 |
+
)
|
| 465 |
+
return converted.reshape(*shape, 3)
|
| 466 |
+
|
| 467 |
+
def _residual(self, target: np.ndarray, reconstruction: np.ndarray) -> np.ndarray:
|
| 468 |
+
return (
|
| 469 |
+
_multiply(target, _conjugate(reconstruction))
|
| 470 |
+
if self.left
|
| 471 |
+
else _multiply(_conjugate(reconstruction), target)
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
def _apply(self, state: np.ndarray, increment: np.ndarray) -> np.ndarray:
|
| 475 |
+
return _multiply(increment, state) if self.left else _multiply(state, increment)
|
| 476 |
+
|
| 477 |
+
def _nearest_batch(self, request: np.ndarray) -> np.ndarray:
|
| 478 |
+
return np.stack(
|
| 479 |
+
[
|
| 480 |
+
np.argmin(np.abs(request[:, index, None] - values[None, :]), axis=1)
|
| 481 |
+
for index, values in enumerate(self.primitives)
|
| 482 |
+
],
|
| 483 |
+
axis=1,
|
| 484 |
+
).astype(np.int64, copy=False)
|
| 485 |
+
|
| 486 |
+
def _smooth(
|
| 487 |
+
self,
|
| 488 |
+
request: np.ndarray,
|
| 489 |
+
reconstruction: np.ndarray,
|
| 490 |
+
target: np.ndarray,
|
| 491 |
+
direction: np.ndarray,
|
| 492 |
+
nearest: np.ndarray,
|
| 493 |
+
) -> np.ndarray:
|
| 494 |
+
"""Apply the exact admissible/non-reversing SO(3) selection rule.
|
| 495 |
+
|
| 496 |
+
On the principal rotation-vector ball, the logarithm's co-Lipschitz
|
| 497 |
+
bound implies that a geodesically admissible primitive lies within
|
| 498 |
+
``2*limit`` in every log coordinate. This prunes the Cartesian lattice
|
| 499 |
+
without the former arbitrary candidate-count cap. Every retained tuple
|
| 500 |
+
is then checked with the actual geodesic error.
|
| 501 |
+
"""
|
| 502 |
+
axes = []
|
| 503 |
+
for coordinate, values in enumerate(self.primitives):
|
| 504 |
+
candidates = np.flatnonzero(
|
| 505 |
+
np.abs(values - request[coordinate]) <= 2.0 * self.limit + 1e-12
|
| 506 |
+
)
|
| 507 |
+
if nearest[coordinate] not in candidates:
|
| 508 |
+
candidates = np.append(candidates, nearest[coordinate])
|
| 509 |
+
axes.append(candidates)
|
| 510 |
+
|
| 511 |
+
best_any: tuple[float, tuple[int, int, int]] | None = None
|
| 512 |
+
best_preferred: tuple[float, tuple[int, int, int]] | None = None
|
| 513 |
+
for raw_choice in itertools.product(*axes):
|
| 514 |
+
choice = tuple(int(value) for value in raw_choice)
|
| 515 |
+
rotvec = np.asarray(
|
| 516 |
+
[self.primitives[index][choice[index]] for index in range(3)]
|
| 517 |
+
)
|
| 518 |
+
rebuilt = self._apply(reconstruction, _rotvec_to_quaternion(rotvec))
|
| 519 |
+
error = float(
|
| 520 |
+
np.linalg.norm(_quaternion_to_rotvec(self._residual(target, rebuilt)))
|
| 521 |
+
)
|
| 522 |
+
if error > self.limit + 1e-12:
|
| 523 |
+
continue
|
| 524 |
+
candidate = (error, choice)
|
| 525 |
+
if best_any is None or candidate < best_any:
|
| 526 |
+
best_any = candidate
|
| 527 |
+
# Orthogonal moves can land a few ULP below zero depending on the
|
| 528 |
+
# BLAS reduction order. Treat that numerical tie as non-reversing.
|
| 529 |
+
if np.dot(rotvec, direction) >= -1e-15 and (
|
| 530 |
+
best_preferred is None or candidate < best_preferred
|
| 531 |
+
):
|
| 532 |
+
best_preferred = candidate
|
| 533 |
+
selected = best_preferred or best_any
|
| 534 |
+
return nearest if selected is None else np.asarray(selected[1], dtype=np.int64)
|
| 535 |
+
|
| 536 |
+
def encode_batch(
|
| 537 |
+
self, actions: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 538 |
+
) -> np.ndarray:
|
| 539 |
+
"""Quantize ``[B,T,3]`` rotations with a vectorized batch state."""
|
| 540 |
+
if self.config.primitive_order == 2:
|
| 541 |
+
return self._encode_second_order(actions, previous_first_order)
|
| 542 |
+
transformed = _transform(self.config, actions).astype(np.float64)
|
| 543 |
+
if resolve_backend("auto") == "native":
|
| 544 |
+
cells = require_native().so3_encode_batch(
|
| 545 |
+
self._native_table,
|
| 546 |
+
self._native_bins,
|
| 547 |
+
np.ascontiguousarray(transformed, dtype=np.float32),
|
| 548 |
+
self.config.input_order,
|
| 549 |
+
self.config.representation == "rotation_vector",
|
| 550 |
+
self.left,
|
| 551 |
+
self.limit,
|
| 552 |
+
self.config.primitive_selection == "smooth",
|
| 553 |
+
min(len(transformed), os.cpu_count() or 1),
|
| 554 |
+
)
|
| 555 |
+
return np.asarray(cells, dtype=np.int64)
|
| 556 |
+
converted = self._from_representation(transformed)
|
| 557 |
+
batch, steps = transformed.shape[:2]
|
| 558 |
+
true = np.broadcast_to(_IDENTITY, (batch, 4)).copy()
|
| 559 |
+
reconstruction = true.copy()
|
| 560 |
+
direction = np.zeros((batch, 3), dtype=np.float64)
|
| 561 |
+
cells = np.empty((batch, steps, 3), dtype=np.int64)
|
| 562 |
+
for step in range(steps):
|
| 563 |
+
value = converted[:, step]
|
| 564 |
+
true = value if self.config.input_order == 0 else self._apply(true, value)
|
| 565 |
+
residual = _quaternion_to_rotvec(self._residual(true, reconstruction))
|
| 566 |
+
choice = self._nearest_batch(residual)
|
| 567 |
+
if self.config.primitive_selection == "smooth":
|
| 568 |
+
choice = np.stack(
|
| 569 |
+
[
|
| 570 |
+
self._smooth(
|
| 571 |
+
residual[index],
|
| 572 |
+
reconstruction[index],
|
| 573 |
+
true[index],
|
| 574 |
+
direction[index],
|
| 575 |
+
choice[index],
|
| 576 |
+
)
|
| 577 |
+
for index in range(batch)
|
| 578 |
+
]
|
| 579 |
+
)
|
| 580 |
+
primitive = np.stack(
|
| 581 |
+
[self.primitives[index][choice[:, index]] for index in range(3)], axis=1
|
| 582 |
+
)
|
| 583 |
+
reconstruction = self._apply(
|
| 584 |
+
reconstruction, _rotvec_to_quaternion(primitive)
|
| 585 |
+
)
|
| 586 |
+
moved = np.linalg.norm(primitive, axis=1) > 0.0
|
| 587 |
+
direction[moved] = primitive[moved]
|
| 588 |
+
cells[:, step] = choice
|
| 589 |
+
return cells
|
| 590 |
+
|
| 591 |
+
def decode_batch(
|
| 592 |
+
self, cells: np.ndarray, previous_first_order: np.ndarray | None = None
|
| 593 |
+
) -> np.ndarray:
|
| 594 |
+
"""Decode ``[B,T,3]`` cells in the configured boundary representation."""
|
| 595 |
+
if self.config.primitive_order == 2:
|
| 596 |
+
return self._decode_second_order(cells, previous_first_order)
|
| 597 |
+
indices = np.asarray(cells, dtype=np.int64)
|
| 598 |
+
rotvec = np.stack(
|
| 599 |
+
[self.primitives[index][indices[:, :, index]] for index in range(3)], axis=2
|
| 600 |
+
)
|
| 601 |
+
if self.config.input_order == 1:
|
| 602 |
+
represented = (
|
| 603 |
+
rotvec
|
| 604 |
+
if self.config.representation == "rotation_vector"
|
| 605 |
+
else self._to_representation(
|
| 606 |
+
_rotvec_to_quaternion(rotvec.astype(np.float32))
|
| 607 |
+
)
|
| 608 |
+
)
|
| 609 |
+
actions = np.asarray(represented / _scale(self.config), dtype=np.float32)
|
| 610 |
+
# Existing artifacts expose first-order actions through a zero-
|
| 611 |
+
# anchored position adapter. The float32 round trip is observable
|
| 612 |
+
# and therefore intentionally preserved for exact compatibility.
|
| 613 |
+
return _difference_additive(np.cumsum(actions, axis=1, dtype=np.float32), 1)
|
| 614 |
+
|
| 615 |
+
batch, steps = indices.shape[:2]
|
| 616 |
+
reconstruction = np.broadcast_to(_IDENTITY, (batch, 4)).copy()
|
| 617 |
+
quaternion = np.empty((batch, steps, 4), dtype=np.float64)
|
| 618 |
+
primitive_quaternion = _rotvec_to_quaternion(rotvec)
|
| 619 |
+
for step in range(steps):
|
| 620 |
+
reconstruction = self._apply(reconstruction, primitive_quaternion[:, step])
|
| 621 |
+
quaternion[:, step] = reconstruction
|
| 622 |
+
represented = self._to_representation(quaternion)
|
| 623 |
+
return np.asarray(represented / _scale(self.config), dtype=np.float32)
|
| 624 |
+
|
| 625 |
+
def _encode_second_order(
|
| 626 |
+
self, actions: np.ndarray, previous_first_order: np.ndarray
|
| 627 |
+
) -> np.ndarray:
|
| 628 |
+
"""Quantize increment changes on SO(3), respecting multiplication order."""
|
| 629 |
+
converted = self._from_representation(
|
| 630 |
+
_transform(self.config, actions).astype(np.float64)
|
| 631 |
+
)
|
| 632 |
+
increment = self._from_representation(
|
| 633 |
+
previous_first_order.astype(np.float64) * _scale(self.config)
|
| 634 |
+
)
|
| 635 |
+
if resolve_backend("auto") == "native" and hasattr(
|
| 636 |
+
require_native(), "so3_second_order_encode_batch"
|
| 637 |
+
):
|
| 638 |
+
return np.asarray(
|
| 639 |
+
require_native().so3_second_order_encode_batch(
|
| 640 |
+
self._native_table,
|
| 641 |
+
self._native_bins,
|
| 642 |
+
np.ascontiguousarray(converted),
|
| 643 |
+
np.ascontiguousarray(increment),
|
| 644 |
+
self.config.input_order,
|
| 645 |
+
self.left,
|
| 646 |
+
self.limit,
|
| 647 |
+
self.second_order_radius,
|
| 648 |
+
self.config.primitive_selection == "smooth",
|
| 649 |
+
min(
|
| 650 |
+
len(actions),
|
| 651 |
+
max(
|
| 652 |
+
1,
|
| 653 |
+
int(os.environ.get("ACTIONCODEC2_PHYSICAL_THREADS", "16")),
|
| 654 |
+
),
|
| 655 |
+
),
|
| 656 |
+
),
|
| 657 |
+
dtype=np.int64,
|
| 658 |
+
)
|
| 659 |
+
batch, steps = actions.shape[:2]
|
| 660 |
+
true = np.broadcast_to(_IDENTITY, (batch, 4)).copy()
|
| 661 |
+
reconstruction = true.copy()
|
| 662 |
+
direction = np.zeros((batch, 3), dtype=np.float64)
|
| 663 |
+
cells = np.empty((batch, steps, 3), dtype=np.int64)
|
| 664 |
+
for step in range(steps):
|
| 665 |
+
true = (
|
| 666 |
+
converted[:, step]
|
| 667 |
+
if self.config.input_order == 0
|
| 668 |
+
else self._apply(true, converted[:, step])
|
| 669 |
+
)
|
| 670 |
+
desired = self._residual(true, reconstruction)
|
| 671 |
+
correction = self._residual(desired, increment)
|
| 672 |
+
request = _quaternion_to_rotvec(correction)
|
| 673 |
+
choice = self._nearest_batch(request)
|
| 674 |
+
if self.config.primitive_selection == "smooth":
|
| 675 |
+
choice = self._smooth_second_order_batch(
|
| 676 |
+
request, correction, direction, choice
|
| 677 |
+
)
|
| 678 |
+
primitive = np.stack(
|
| 679 |
+
[axis[choice[:, j]] for j, axis in enumerate(self.primitives)], axis=1
|
| 680 |
+
)
|
| 681 |
+
primitive = self._second_order_primitive(primitive)
|
| 682 |
+
increment = self._apply(increment, _rotvec_to_quaternion(primitive))
|
| 683 |
+
reconstruction = self._apply(reconstruction, increment)
|
| 684 |
+
moved = np.linalg.norm(primitive, axis=1) > 0
|
| 685 |
+
direction[moved] = primitive[moved]
|
| 686 |
+
cells[:, step] = choice
|
| 687 |
+
return cells
|
| 688 |
+
|
| 689 |
+
def _second_order_primitive(self, rotvec: np.ndarray) -> np.ndarray:
|
| 690 |
+
"""Project the lattice to the declared increment-change ball.
|
| 691 |
+
|
| 692 |
+
Euclidean projection cannot increase distance to any log in the ball;
|
| 693 |
+
Exp is nonexpansive. Thus the Cartesian epsilon cover remains valid,
|
| 694 |
+
while every executable primitive respects c_R + 3*epsilon_R.
|
| 695 |
+
"""
|
| 696 |
+
norm = np.linalg.norm(rotvec, axis=-1, keepdims=True)
|
| 697 |
+
return rotvec * np.minimum(
|
| 698 |
+
1.0, self.second_order_radius / np.maximum(norm, np.finfo(float).tiny)
|
| 699 |
+
)
|
| 700 |
+
|
| 701 |
+
def _smooth_second_order_batch(
|
| 702 |
+
self,
|
| 703 |
+
request: np.ndarray,
|
| 704 |
+
correction: np.ndarray,
|
| 705 |
+
direction: np.ndarray,
|
| 706 |
+
nearest: np.ndarray,
|
| 707 |
+
) -> np.ndarray:
|
| 708 |
+
"""Evaluate local Cartesian candidates in batches with intrinsic errors.
|
| 709 |
+
|
| 710 |
+
Search windows use the same geometric pruning as ``_smooth``. Blocks
|
| 711 |
+
cap scratch memory even for unusually dense user-supplied grids.
|
| 712 |
+
"""
|
| 713 |
+
if len(request) > 64:
|
| 714 |
+
return np.concatenate(
|
| 715 |
+
[
|
| 716 |
+
self._smooth_second_order_batch(
|
| 717 |
+
request[i : i + 64],
|
| 718 |
+
correction[i : i + 64],
|
| 719 |
+
direction[i : i + 64],
|
| 720 |
+
nearest[i : i + 64],
|
| 721 |
+
)
|
| 722 |
+
for i in range(0, len(request), 64)
|
| 723 |
+
]
|
| 724 |
+
)
|
| 725 |
+
axes, counts = [], []
|
| 726 |
+
for j, axis in enumerate(self.primitives):
|
| 727 |
+
mask = np.abs(axis[None] - request[:, j, None]) <= 2 * self.limit + 1e-12
|
| 728 |
+
mask[np.arange(len(request)), nearest[:, j]] = True
|
| 729 |
+
count = mask.sum(axis=1)
|
| 730 |
+
rows, columns = np.nonzero(mask)
|
| 731 |
+
slots = np.arange(len(rows)) - np.repeat(np.cumsum(count) - count, count)
|
| 732 |
+
candidates = np.zeros((len(request), int(count.max())), dtype=np.int64)
|
| 733 |
+
candidates[rows, slots] = columns
|
| 734 |
+
axes.append(candidates)
|
| 735 |
+
counts.append(count)
|
| 736 |
+
counts = np.stack(counts, axis=1)
|
| 737 |
+
widths = counts.max(axis=0)
|
| 738 |
+
total = int(np.prod(widths))
|
| 739 |
+
best = nearest.copy()
|
| 740 |
+
best_error = np.full(len(request), np.inf)
|
| 741 |
+
best_preferred = np.zeros(len(request), dtype=bool)
|
| 742 |
+
for offset in range(0, total, 4096):
|
| 743 |
+
lattice = np.stack(
|
| 744 |
+
np.unravel_index(
|
| 745 |
+
np.arange(offset, min(total, offset + 4096)), tuple(widths)
|
| 746 |
+
),
|
| 747 |
+
axis=1,
|
| 748 |
+
)
|
| 749 |
+
valid = np.all(lattice[None] < counts[:, None], axis=2)
|
| 750 |
+
choices = np.stack(
|
| 751 |
+
[
|
| 752 |
+
axis[
|
| 753 |
+
np.arange(len(request))[:, None],
|
| 754 |
+
np.minimum(lattice[None, :, j], counts[:, j, None] - 1),
|
| 755 |
+
]
|
| 756 |
+
for j, axis in enumerate(axes)
|
| 757 |
+
],
|
| 758 |
+
axis=2,
|
| 759 |
+
)
|
| 760 |
+
rotvec = np.stack(
|
| 761 |
+
[axis[choices[:, :, j]] for j, axis in enumerate(self.primitives)],
|
| 762 |
+
axis=2,
|
| 763 |
+
)
|
| 764 |
+
rotvec = self._second_order_primitive(rotvec)
|
| 765 |
+
primitive = _rotvec_to_quaternion(rotvec)
|
| 766 |
+
residual = _multiply(_conjugate(primitive), correction[:, None])
|
| 767 |
+
error = np.linalg.norm(_quaternion_to_rotvec(residual), axis=2)
|
| 768 |
+
valid &= error <= self.limit + 1e-12
|
| 769 |
+
preferred = valid & (np.sum(rotvec * direction[:, None], axis=2) >= -1e-15)
|
| 770 |
+
has_preferred = preferred.any(axis=1)
|
| 771 |
+
eligible = np.where(has_preferred[:, None], preferred, valid)
|
| 772 |
+
scores = np.where(eligible, error, np.inf)
|
| 773 |
+
index = np.argmin(scores, axis=1)
|
| 774 |
+
score = scores[np.arange(len(request)), index]
|
| 775 |
+
improve = np.isfinite(score) & (
|
| 776 |
+
(has_preferred & ~best_preferred)
|
| 777 |
+
| ((has_preferred == best_preferred) & (score < best_error))
|
| 778 |
+
)
|
| 779 |
+
best[improve] = choices[np.arange(len(request)), index][improve]
|
| 780 |
+
best_error[improve] = score[improve]
|
| 781 |
+
best_preferred[improve] = has_preferred[improve]
|
| 782 |
+
return best
|
| 783 |
+
|
| 784 |
+
def _decode_second_order(
|
| 785 |
+
self, cells: np.ndarray, previous_first_order: np.ndarray
|
| 786 |
+
) -> np.ndarray:
|
| 787 |
+
"""Integrate rotation changes from a shared ``[B,3]`` first-order state."""
|
| 788 |
+
rotvec = np.stack(
|
| 789 |
+
[axis[cells[:, :, j]] for j, axis in enumerate(self.primitives)], axis=2
|
| 790 |
+
)
|
| 791 |
+
rotvec = self._second_order_primitive(rotvec)
|
| 792 |
+
changes = _rotvec_to_quaternion(rotvec)
|
| 793 |
+
increment = self._from_representation(
|
| 794 |
+
previous_first_order.astype(np.float64) * _scale(self.config)
|
| 795 |
+
)
|
| 796 |
+
reconstruction = np.broadcast_to(_IDENTITY, increment.shape).copy()
|
| 797 |
+
output = np.empty((*cells.shape[:2], 4), dtype=np.float64)
|
| 798 |
+
for step in range(cells.shape[1]):
|
| 799 |
+
increment = self._apply(increment, changes[:, step])
|
| 800 |
+
reconstruction = self._apply(reconstruction, increment)
|
| 801 |
+
output[:, step] = (
|
| 802 |
+
increment if self.config.input_order == 1 else reconstruction
|
| 803 |
+
)
|
| 804 |
+
return np.asarray(
|
| 805 |
+
self._to_representation(output) / _scale(self.config), dtype=np.float32
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
|
| 809 |
+
class PhysicalCodec:
|
| 810 |
+
"""Compose independent physical components in global action-dimension order.
|
| 811 |
+
|
| 812 |
+
The public contract is ``[B,T,D] -> [B,T,D]``. Components may be declared
|
| 813 |
+
in any YAML order and may use arbitrary non-overlapping dimension lists;
|
| 814 |
+
scattering always restores global dimension ``j`` to slot ``j``.
|
| 815 |
+
"""
|
| 816 |
+
|
| 817 |
+
def __init__(self, config: ActionCodec2Config, primitives: PrimitiveStore) -> None:
|
| 818 |
+
if primitives.dimension != config.physical.input_dim:
|
| 819 |
+
raise ConfigError(
|
| 820 |
+
f"primitive dimension {primitives.dimension} does not match input_dim "
|
| 821 |
+
f"{config.physical.input_dim}"
|
| 822 |
+
)
|
| 823 |
+
self.config = config
|
| 824 |
+
self.primitives = primitives
|
| 825 |
+
runtimes: list[tuple[ComponentConfig, _ComponentRuntime]] = []
|
| 826 |
+
for component in config.physical.components.values():
|
| 827 |
+
component_primitives = tuple(
|
| 828 |
+
primitives.values[index] for index in component.dims
|
| 829 |
+
)
|
| 830 |
+
runtime: _ComponentRuntime = (
|
| 831 |
+
_AdditiveRuntime(component, component_primitives)
|
| 832 |
+
if component.space == "additive"
|
| 833 |
+
else _SO3Runtime(component, component_primitives)
|
| 834 |
+
)
|
| 835 |
+
runtimes.append((component, runtime))
|
| 836 |
+
self._runtimes = tuple(runtimes)
|
| 837 |
+
self.bins = primitives.bins
|
| 838 |
+
self.requires_previous_first_order = any(
|
| 839 |
+
c.primitive_order == 2 for c, _ in runtimes
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
def _check_previous_first_order(
|
| 843 |
+
self, value: np.ndarray | None, batch: int
|
| 844 |
+
) -> np.ndarray | None:
|
| 845 |
+
if value is None:
|
| 846 |
+
if self.requires_previous_first_order:
|
| 847 |
+
raise ValueError(
|
| 848 |
+
"second-order tokenization requires previous_first_order; no zero initialization is inferred"
|
| 849 |
+
)
|
| 850 |
+
return None
|
| 851 |
+
array = np.asarray(value, dtype=np.float32)
|
| 852 |
+
shape = (batch, self.config.physical.input_dim)
|
| 853 |
+
if array.shape != shape or not np.all(np.isfinite(array)):
|
| 854 |
+
raise ValueError(f"previous_first_order must be finite with shape {shape}")
|
| 855 |
+
return array
|
| 856 |
+
|
| 857 |
+
def _check_actions(self, actions: np.ndarray) -> np.ndarray:
|
| 858 |
+
values = np.asarray(actions, dtype=np.float32)
|
| 859 |
+
expected = self.config.physical.input_dim
|
| 860 |
+
if values.ndim != 3 or values.shape[2] != expected or values.shape[1] < 1:
|
| 861 |
+
raise ValueError(f"actions must have non-empty shape [B, T, {expected}]")
|
| 862 |
+
if not np.all(np.isfinite(values)):
|
| 863 |
+
raise ValueError("actions must contain only finite values")
|
| 864 |
+
return values
|
| 865 |
+
|
| 866 |
+
def final_first_order(
|
| 867 |
+
self, cells: np.ndarray, *, previous_first_order: np.ndarray
|
| 868 |
+
) -> np.ndarray:
|
| 869 |
+
"""Return ``[B,D]`` state after these cells, without position differencing.
|
| 870 |
+
|
| 871 |
+
Only second-order component slots carry state; other slots are zero.
|
| 872 |
+
Pass a cell prefix when only part of the generated chunk was executed.
|
| 873 |
+
"""
|
| 874 |
+
values = self._check_cells(cells)
|
| 875 |
+
previous = self._check_previous_first_order(previous_first_order, len(values))
|
| 876 |
+
result = np.zeros(
|
| 877 |
+
(len(values), self.config.physical.input_dim), dtype=np.float32
|
| 878 |
+
)
|
| 879 |
+
for component, runtime in self._runtimes:
|
| 880 |
+
if component.primitive_order != 2:
|
| 881 |
+
continue
|
| 882 |
+
local = np.take(values, component.dims, axis=2)
|
| 883 |
+
boundary = np.take(previous, component.dims, axis=1)
|
| 884 |
+
picked = np.stack(
|
| 885 |
+
[axis[local[:, :, j]] for j, axis in enumerate(runtime.primitives)],
|
| 886 |
+
axis=2,
|
| 887 |
+
)
|
| 888 |
+
if component.space == "additive":
|
| 889 |
+
final = boundary.astype(np.float64) + np.sum(
|
| 890 |
+
picked.astype(np.float64), axis=1
|
| 891 |
+
) / _scale(component)
|
| 892 |
+
else:
|
| 893 |
+
increment = runtime._from_representation(
|
| 894 |
+
boundary.astype(np.float64) * _scale(component)
|
| 895 |
+
)
|
| 896 |
+
changes = _rotvec_to_quaternion(runtime._second_order_primitive(picked))
|
| 897 |
+
for step in range(local.shape[1]):
|
| 898 |
+
increment = runtime._apply(increment, changes[:, step])
|
| 899 |
+
final = runtime._to_representation(increment) / _scale(component)
|
| 900 |
+
result[:, component.dims] = final
|
| 901 |
+
return result
|
| 902 |
+
|
| 903 |
+
def _check_cells(self, cells: np.ndarray) -> np.ndarray:
|
| 904 |
+
values = np.asarray(cells, dtype=np.int64)
|
| 905 |
+
expected = self.config.physical.input_dim
|
| 906 |
+
if values.ndim != 3 or values.shape[2] != expected or values.shape[1] < 1:
|
| 907 |
+
raise ValueError(f"cells must have non-empty shape [B, T, {expected}]")
|
| 908 |
+
counts = np.asarray(self.bins, dtype=np.int64)
|
| 909 |
+
if values.size and (
|
| 910 |
+
np.any(values < 0) or np.any(values >= counts[None, None, :])
|
| 911 |
+
):
|
| 912 |
+
raise ValueError("a cell index is outside its dimension's primitive set")
|
| 913 |
+
return values
|
| 914 |
+
|
| 915 |
+
def encode_batch(
|
| 916 |
+
self, actions: np.ndarray, *, previous_first_order: np.ndarray | None = None
|
| 917 |
+
) -> np.ndarray:
|
| 918 |
+
"""Quantize ``[B,T,D]`` with a required ``[B,D]`` boundary for order 2.
|
| 919 |
+
|
| 920 |
+
The boundary is the preceding reconstructed first-order quantity, in
|
| 921 |
+
input units and representation, before scaling (never source-clipped).
|
| 922 |
+
Position/orientation coordinates are local to zero/identity per call.
|
| 923 |
+
"""
|
| 924 |
+
values = self._check_actions(actions)
|
| 925 |
+
previous = self._check_previous_first_order(previous_first_order, len(values))
|
| 926 |
+
cells = np.empty(values.shape, dtype=np.int64)
|
| 927 |
+
for component, runtime in self._runtimes:
|
| 928 |
+
component_cells = runtime.encode_batch(
|
| 929 |
+
np.take(values, component.dims, axis=2),
|
| 930 |
+
None if previous is None else np.take(previous, component.dims, axis=1),
|
| 931 |
+
)
|
| 932 |
+
for local, global_dimension in enumerate(component.dims):
|
| 933 |
+
cells[:, :, global_dimension] = component_cells[:, :, local]
|
| 934 |
+
return cells
|
| 935 |
+
|
| 936 |
+
def decode_batch(
|
| 937 |
+
self, cells: np.ndarray, *, previous_first_order: np.ndarray | None = None
|
| 938 |
+
) -> np.ndarray:
|
| 939 |
+
"""Decode ``[B,T,D]`` using the same ``[B,D]`` boundary as encoding."""
|
| 940 |
+
values = self._check_cells(cells)
|
| 941 |
+
previous = self._check_previous_first_order(previous_first_order, len(values))
|
| 942 |
+
actions = np.empty(values.shape, dtype=np.float32)
|
| 943 |
+
for component, runtime in self._runtimes:
|
| 944 |
+
component_actions = runtime.decode_batch(
|
| 945 |
+
np.take(values, component.dims, axis=2),
|
| 946 |
+
None if previous is None else np.take(previous, component.dims, axis=1),
|
| 947 |
+
)
|
| 948 |
+
for local, global_dimension in enumerate(component.dims):
|
| 949 |
+
actions[:, :, global_dimension] = component_actions[:, :, local]
|
| 950 |
+
return actions
|
runtime/physical/physical_processor.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public ActionCodec2 processor and constrained-generation grammars.
|
| 2 |
+
|
| 3 |
+
The processor is scheme-agnostic: ``config.set_bpe.scheme`` picks the
|
| 4 |
+
serializer and the matching grammar, and everything else -- physical
|
| 5 |
+
quantization, batching, second-order boundaries, artifact layout -- is shared.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
from collections.abc import Sequence
|
| 13 |
+
from numbers import Integral
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
from transformers.processing_utils import ProcessorMixin
|
| 19 |
+
|
| 20 |
+
from ..common._validation import active_dimensions
|
| 21 |
+
from ..tokenization.bpe import (
|
| 22 |
+
BPE_NAME,
|
| 23 |
+
BPESerializer,
|
| 24 |
+
SetBPESerializer,
|
| 25 |
+
serializer_class,
|
| 26 |
+
)
|
| 27 |
+
from ..routing.cache import BoundedCache
|
| 28 |
+
from .configuration import CONFIG_NAME, MODEL_TYPE, ActionCodec2Config
|
| 29 |
+
from ..tokenization.grammar import ActionCodec2BPEGrammar as ActionCodec2BPEGrammar
|
| 30 |
+
from ..tokenization.grammar import ActionCodec2Grammar as ActionCodec2Grammar
|
| 31 |
+
from ..integration.hf import (
|
| 32 |
+
resolve_pretrained_directory,
|
| 33 |
+
save_huggingface_metadata,
|
| 34 |
+
upload_pretrained_directory,
|
| 35 |
+
)
|
| 36 |
+
from .physical_codec import PhysicalCodec
|
| 37 |
+
from .primitives import PRIMITIVES_NAME, PrimitiveStore
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class ActionCodec2Processor(ProcessorMixin):
|
| 41 |
+
"""Hugging Face-style processor for continuous robot action trajectories.
|
| 42 |
+
|
| 43 |
+
ActionCodec2 is an ``AutoProcessor``-class object, not an ``AutoModel``: it
|
| 44 |
+
has no neural ``forward`` method or trainable weights. Its fitted state is a
|
| 45 |
+
physical primitive vocabulary plus lossless Set-BPE rules. Encoding maps
|
| 46 |
+
``[B,T,D]`` continuous actions to ragged token-ID streams; decoding performs
|
| 47 |
+
the inverse token/cell path and the configured lossy physical reconstruction.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
config: Physical quantization and tokenization scheme configuration.
|
| 51 |
+
primitives: Fitted primitive values for every physical coordinate.
|
| 52 |
+
serializer: Fitted BPE rules matching the configured scheme, primitive
|
| 53 |
+
bin counts, and lookahead.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
model_type = MODEL_TYPE
|
| 57 |
+
name = "actioncodec2"
|
| 58 |
+
is_exact = False
|
| 59 |
+
set_bpe_is_lossless = True
|
| 60 |
+
model_input_names = ["input_ids"]
|
| 61 |
+
attributes = []
|
| 62 |
+
|
| 63 |
+
def __init__(
|
| 64 |
+
self,
|
| 65 |
+
config: ActionCodec2Config,
|
| 66 |
+
primitives: PrimitiveStore,
|
| 67 |
+
serializer: SetBPESerializer | BPESerializer,
|
| 68 |
+
) -> None:
|
| 69 |
+
if serializer.scheme != config.set_bpe.scheme:
|
| 70 |
+
raise ValueError("serializer scheme does not match config.yaml")
|
| 71 |
+
if tuple(serializer.bins) != tuple(primitives.bins):
|
| 72 |
+
raise ValueError("BPE bins do not match primitives.npz")
|
| 73 |
+
if serializer.lookahead != config.set_bpe.lookahead:
|
| 74 |
+
raise ValueError("BPE lookahead does not match config.yaml")
|
| 75 |
+
# ProcessorMixin is designed around modality sub-processors. This
|
| 76 |
+
# processor is atomic, so it deliberately owns and serializes its three
|
| 77 |
+
# artifact objects directly instead of invoking ProcessorMixin.__init__.
|
| 78 |
+
self.config = config
|
| 79 |
+
self.primitives = primitives
|
| 80 |
+
self.serializer = serializer
|
| 81 |
+
self._grammar_cache = BoundedCache(8)
|
| 82 |
+
self.physical = PhysicalCodec(config, primitives)
|
| 83 |
+
self.action_dim = config.physical.input_dim
|
| 84 |
+
self.bins = primitives.bins
|
| 85 |
+
self.vocab_size = serializer.vocab_size
|
| 86 |
+
self.lookahead = serializer.lookahead
|
| 87 |
+
self.requires_previous_first_order = self.physical.requires_previous_first_order
|
| 88 |
+
zero_cells = []
|
| 89 |
+
for dimension, axis in enumerate(primitives.values):
|
| 90 |
+
matches = np.flatnonzero(np.asarray(axis) == 0)
|
| 91 |
+
if not len(matches):
|
| 92 |
+
raise ValueError(
|
| 93 |
+
"ActionCodec2 artifacts require an exact zero primitive in every axis; "
|
| 94 |
+
f"dimension {dimension} has none"
|
| 95 |
+
)
|
| 96 |
+
zero_cells.append(int(matches[0]))
|
| 97 |
+
self._zero_cells = np.asarray(zero_cells, dtype=np.int64)
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def from_pretrained(
|
| 101 |
+
cls,
|
| 102 |
+
pretrained_model_name_or_path: str | Path,
|
| 103 |
+
**kwargs: Any,
|
| 104 |
+
) -> ActionCodec2Processor:
|
| 105 |
+
"""Load a local directory or Hugging Face Hub snapshot.
|
| 106 |
+
|
| 107 |
+
Standard Hub keywords such as ``revision``, ``cache_dir``, ``token``,
|
| 108 |
+
and ``local_files_only`` are supported. Numerical files remain the
|
| 109 |
+
strict physical artifact rather than being embedded in JSON metadata.
|
| 110 |
+
"""
|
| 111 |
+
directory = resolve_pretrained_directory(
|
| 112 |
+
pretrained_model_name_or_path, **kwargs
|
| 113 |
+
)
|
| 114 |
+
config = ActionCodec2Config.from_pretrained(directory)
|
| 115 |
+
primitives = PrimitiveStore.from_pretrained(
|
| 116 |
+
directory, input_dim=config.physical.input_dim
|
| 117 |
+
)
|
| 118 |
+
serializer = serializer_class(config).from_pretrained(
|
| 119 |
+
directory,
|
| 120 |
+
bins=primitives.bins,
|
| 121 |
+
lookahead=config.set_bpe.lookahead,
|
| 122 |
+
)
|
| 123 |
+
return cls(config, primitives, serializer)
|
| 124 |
+
|
| 125 |
+
def save_pretrained(
|
| 126 |
+
self,
|
| 127 |
+
save_directory: str | Path,
|
| 128 |
+
push_to_hub: bool = False,
|
| 129 |
+
**kwargs: Any,
|
| 130 |
+
) -> list[str]:
|
| 131 |
+
"""Save numerical artifacts plus standard AutoProcessor metadata."""
|
| 132 |
+
if not push_to_hub and kwargs:
|
| 133 |
+
raise TypeError(
|
| 134 |
+
f"unused save_pretrained keyword(s): {', '.join(sorted(kwargs))}"
|
| 135 |
+
)
|
| 136 |
+
directory = Path(save_directory)
|
| 137 |
+
self.config.save_pretrained(directory)
|
| 138 |
+
self.primitives.save_pretrained(directory)
|
| 139 |
+
self.serializer.save_pretrained(directory)
|
| 140 |
+
metadata = save_huggingface_metadata(directory)
|
| 141 |
+
files = [
|
| 142 |
+
directory / CONFIG_NAME,
|
| 143 |
+
directory / PRIMITIVES_NAME,
|
| 144 |
+
directory / BPE_NAME,
|
| 145 |
+
*metadata,
|
| 146 |
+
]
|
| 147 |
+
if push_to_hub:
|
| 148 |
+
upload_pretrained_directory(directory, **kwargs)
|
| 149 |
+
return [str(path) for path in files]
|
| 150 |
+
|
| 151 |
+
def to_dict(self, legacy_serialization: bool = True) -> dict[str, object]:
|
| 152 |
+
"""Return metadata without embedding numerical artifact arrays.
|
| 153 |
+
|
| 154 |
+
``legacy_serialization`` is retained because Transformers passes it to
|
| 155 |
+
processor serializers in older releases; ActionCodec2 has one format.
|
| 156 |
+
"""
|
| 157 |
+
del legacy_serialization
|
| 158 |
+
return {
|
| 159 |
+
"processor_class": self.__class__.__name__,
|
| 160 |
+
"model_type": self.model_type,
|
| 161 |
+
"action_dim": self.action_dim,
|
| 162 |
+
"vocab_size": self.vocab_size,
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
@property
|
| 166 |
+
def artifact_fingerprint(self) -> str:
|
| 167 |
+
"""Content hash over config, primitive axes, and ordered BPE rules."""
|
| 168 |
+
digest = hashlib.sha256()
|
| 169 |
+
digest.update(
|
| 170 |
+
json.dumps(
|
| 171 |
+
self.config.to_dict(), sort_keys=True, separators=(",", ":")
|
| 172 |
+
).encode()
|
| 173 |
+
)
|
| 174 |
+
for axis in self.primitives.values:
|
| 175 |
+
values = np.ascontiguousarray(axis)
|
| 176 |
+
digest.update(str(values.dtype).encode())
|
| 177 |
+
digest.update(np.asarray(values.shape, dtype=np.int64).tobytes())
|
| 178 |
+
digest.update(values.tobytes())
|
| 179 |
+
digest.update(
|
| 180 |
+
json.dumps(
|
| 181 |
+
self.serializer.to_dict(), sort_keys=True, separators=(",", ":")
|
| 182 |
+
).encode()
|
| 183 |
+
)
|
| 184 |
+
return digest.hexdigest()
|
| 185 |
+
|
| 186 |
+
def quantize_batch(
|
| 187 |
+
self, actions: np.ndarray, *, previous_first_order: np.ndarray | None = None
|
| 188 |
+
) -> np.ndarray:
|
| 189 |
+
"""Quantize ``[B,T,D]`` using a ``[B,D]`` first-order boundary if required."""
|
| 190 |
+
return self.physical.encode_batch(
|
| 191 |
+
actions, previous_first_order=previous_first_order
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
def _dense_previous_first_order(
|
| 195 |
+
self, previous_first_order: np.ndarray | None, dims: tuple[int, ...], batch: int
|
| 196 |
+
) -> np.ndarray | None:
|
| 197 |
+
"""Expand a compact ``[B,D_active]`` boundary, validating active slots."""
|
| 198 |
+
active = set(dims)
|
| 199 |
+
for component in self.config.physical.components.values():
|
| 200 |
+
present = active.intersection(component.dims)
|
| 201 |
+
if (
|
| 202 |
+
component.primitive_order == 2
|
| 203 |
+
and component.space == "so3"
|
| 204 |
+
and present
|
| 205 |
+
and present != set(component.dims)
|
| 206 |
+
):
|
| 207 |
+
raise ValueError(
|
| 208 |
+
"second-order SO(3) requires all three active component dimensions"
|
| 209 |
+
)
|
| 210 |
+
if previous_first_order is None:
|
| 211 |
+
if any(
|
| 212 |
+
c.primitive_order == 2 and active.intersection(c.dims)
|
| 213 |
+
for c in self.config.physical.components.values()
|
| 214 |
+
):
|
| 215 |
+
raise ValueError(
|
| 216 |
+
f"second-order tokenization requires previous_first_order with shape {(batch, len(dims))} in canonical input units"
|
| 217 |
+
)
|
| 218 |
+
return (
|
| 219 |
+
np.zeros((batch, self.action_dim), dtype=np.float32)
|
| 220 |
+
if self.requires_previous_first_order
|
| 221 |
+
else None
|
| 222 |
+
)
|
| 223 |
+
values = np.asarray(previous_first_order, dtype=np.float32)
|
| 224 |
+
if values.shape != (batch, len(dims)) or not np.all(np.isfinite(values)):
|
| 225 |
+
raise ValueError(
|
| 226 |
+
f"previous_first_order must be finite with shape {(batch, len(dims))}"
|
| 227 |
+
)
|
| 228 |
+
dense = np.zeros((batch, self.action_dim), dtype=np.float32)
|
| 229 |
+
dense[:, dims] = values
|
| 230 |
+
return dense
|
| 231 |
+
|
| 232 |
+
def tokens_from_cells(
|
| 233 |
+
self,
|
| 234 |
+
cells: np.ndarray,
|
| 235 |
+
*,
|
| 236 |
+
active_dims: Sequence[int] | None = None,
|
| 237 |
+
) -> list[int]:
|
| 238 |
+
"""Apply the lossless tokenizer to one compact or dense cell trajectory."""
|
| 239 |
+
return self.serializer.encode(cells, active_dims=active_dims)
|
| 240 |
+
|
| 241 |
+
def cells_from_tokens(
|
| 242 |
+
self,
|
| 243 |
+
tokens: Sequence[int],
|
| 244 |
+
*,
|
| 245 |
+
active_dims: Sequence[int] | None = None,
|
| 246 |
+
) -> np.ndarray:
|
| 247 |
+
"""Losslessly recover dense cells with ``-1`` in absent dimensions."""
|
| 248 |
+
return self.serializer.decode(tokens, active_dims=active_dims)
|
| 249 |
+
|
| 250 |
+
def _fill_inactive_cells(self, cells: np.ndarray) -> np.ndarray:
|
| 251 |
+
"""Replace semantic absence with each physical axis's exact zero cell."""
|
| 252 |
+
values = np.asarray(cells, dtype=np.int64)
|
| 253 |
+
return np.where(values >= 0, values, self._zero_cells)
|
| 254 |
+
|
| 255 |
+
def encode_batch(
|
| 256 |
+
self,
|
| 257 |
+
actions: np.ndarray,
|
| 258 |
+
*,
|
| 259 |
+
active_dims: Sequence[int] | None = None,
|
| 260 |
+
previous_first_order: np.ndarray | None = None,
|
| 261 |
+
) -> list[list[int]]:
|
| 262 |
+
"""Encode a dense or compact ``[B,T,D]`` batch into ragged streams.
|
| 263 |
+
|
| 264 |
+
``active_dims`` maps compact coordinates into this processor's physical
|
| 265 |
+
profile. Supplying the whole batch at once preserves native physical
|
| 266 |
+
quantization across the batch dimension.
|
| 267 |
+
``previous_first_order[B,D_active]`` is required for active second-order
|
| 268 |
+
components and follows the same compact mapping, in input units.
|
| 269 |
+
"""
|
| 270 |
+
values = np.asarray(actions, dtype=np.float32)
|
| 271 |
+
if (
|
| 272 |
+
values.ndim != 3
|
| 273 |
+
or len(values) < 1
|
| 274 |
+
or values.shape[1] < 1
|
| 275 |
+
or values.shape[2] < 1
|
| 276 |
+
):
|
| 277 |
+
raise ValueError("actions must have non-empty shape [B, T, D_active]")
|
| 278 |
+
dims = active_dimensions(active_dims, self.action_dim)
|
| 279 |
+
if len(dims) != values.shape[2]:
|
| 280 |
+
raise ValueError("active_dims must have one entry per action coordinate")
|
| 281 |
+
full_width = dims == tuple(range(self.action_dim))
|
| 282 |
+
if full_width:
|
| 283 |
+
dense = values
|
| 284 |
+
else:
|
| 285 |
+
dense = np.zeros(
|
| 286 |
+
(len(values), values.shape[1], self.action_dim), dtype=np.float32
|
| 287 |
+
)
|
| 288 |
+
dense[:, :, dims] = values
|
| 289 |
+
previous = self._dense_previous_first_order(
|
| 290 |
+
previous_first_order, dims, len(values)
|
| 291 |
+
)
|
| 292 |
+
cells = self.quantize_batch(dense, previous_first_order=previous)
|
| 293 |
+
if not full_width:
|
| 294 |
+
cells = np.take(cells, dims, axis=2)
|
| 295 |
+
return [self.tokens_from_cells(row, active_dims=dims) for row in cells]
|
| 296 |
+
|
| 297 |
+
def decode_batch(
|
| 298 |
+
self,
|
| 299 |
+
streams: Sequence[Sequence[int]],
|
| 300 |
+
*,
|
| 301 |
+
active_dims: Sequence[int] | None = None,
|
| 302 |
+
previous_first_order: np.ndarray | None = None,
|
| 303 |
+
) -> np.ndarray:
|
| 304 |
+
"""Decode equal-length streams into one batched physical array.
|
| 305 |
+
|
| 306 |
+
With ``active_dims``, the last output dimension is compact and follows
|
| 307 |
+
that mapping. Inactive physical-profile slots never appear at the
|
| 308 |
+
semantic boundary.
|
| 309 |
+
``previous_first_order[B,D_active]`` must match the encoding boundary
|
| 310 |
+
for active second-order components.
|
| 311 |
+
"""
|
| 312 |
+
dims = active_dimensions(active_dims, self.action_dim)
|
| 313 |
+
cells = [self.cells_from_tokens(stream, active_dims=dims) for stream in streams]
|
| 314 |
+
if not cells:
|
| 315 |
+
raise ValueError("streams must not be empty")
|
| 316 |
+
lengths = {len(value) for value in cells}
|
| 317 |
+
if len(lengths) != 1:
|
| 318 |
+
raise ValueError(
|
| 319 |
+
"all token streams in a batch must decode to the same length"
|
| 320 |
+
)
|
| 321 |
+
stacked_cells = np.stack(cells)
|
| 322 |
+
dense_cells = self._fill_inactive_cells(stacked_cells)
|
| 323 |
+
previous = self._dense_previous_first_order(
|
| 324 |
+
previous_first_order, dims, len(cells)
|
| 325 |
+
)
|
| 326 |
+
decoded = self.physical.decode_batch(dense_cells, previous_first_order=previous)
|
| 327 |
+
return np.take(decoded, dims, axis=2)
|
| 328 |
+
|
| 329 |
+
def final_first_order(
|
| 330 |
+
self,
|
| 331 |
+
streams: Sequence[Sequence[int]],
|
| 332 |
+
*,
|
| 333 |
+
previous_first_order: np.ndarray,
|
| 334 |
+
active_dims: Sequence[int] | None = None,
|
| 335 |
+
executed_steps: int | None = None,
|
| 336 |
+
) -> np.ndarray:
|
| 337 |
+
"""Return the next compact ``[B,D_active]`` reconstructed boundary.
|
| 338 |
+
|
| 339 |
+
``executed_steps`` selects the executed prefix in codec timesteps;
|
| 340 |
+
omitted means the full chunk. Tokens may span the prefix boundary.
|
| 341 |
+
"""
|
| 342 |
+
dims = active_dimensions(active_dims, self.action_dim)
|
| 343 |
+
cells = [self.cells_from_tokens(stream, active_dims=dims) for stream in streams]
|
| 344 |
+
if not cells or len({len(row) for row in cells}) != 1:
|
| 345 |
+
raise ValueError(
|
| 346 |
+
"streams must be non-empty and have equal decoded horizons"
|
| 347 |
+
)
|
| 348 |
+
if executed_steps is not None:
|
| 349 |
+
if (
|
| 350 |
+
isinstance(executed_steps, bool)
|
| 351 |
+
or not isinstance(executed_steps, Integral)
|
| 352 |
+
or not 1 <= executed_steps <= len(cells[0])
|
| 353 |
+
):
|
| 354 |
+
raise ValueError("executed_steps must be within the decoded horizon")
|
| 355 |
+
cells = [row[:executed_steps] for row in cells]
|
| 356 |
+
previous = self._dense_previous_first_order(
|
| 357 |
+
previous_first_order, dims, len(cells)
|
| 358 |
+
)
|
| 359 |
+
result = self.physical.final_first_order(
|
| 360 |
+
self._fill_inactive_cells(np.stack(cells)), previous_first_order=previous
|
| 361 |
+
)
|
| 362 |
+
return np.take(result, dims, axis=1)
|
| 363 |
+
|
| 364 |
+
def grammar(
|
| 365 |
+
self,
|
| 366 |
+
horizon: int,
|
| 367 |
+
*,
|
| 368 |
+
active_dims: Sequence[int] | None = None,
|
| 369 |
+
) -> ActionCodec2Grammar | ActionCodec2BPEGrammar:
|
| 370 |
+
"""Build a constrained grammar for one horizon and route support."""
|
| 371 |
+
if isinstance(horizon, bool) or not isinstance(horizon, int) or horizon < 1:
|
| 372 |
+
raise ValueError("horizon must be a positive integer")
|
| 373 |
+
dims = active_dimensions(active_dims, self.action_dim)
|
| 374 |
+
key = (horizon, dims)
|
| 375 |
+
grammar = self._grammar_cache.get(key)
|
| 376 |
+
if grammar is None:
|
| 377 |
+
grammar_class = (
|
| 378 |
+
ActionCodec2BPEGrammar
|
| 379 |
+
if isinstance(self.serializer, BPESerializer)
|
| 380 |
+
else ActionCodec2Grammar
|
| 381 |
+
)
|
| 382 |
+
grammar = grammar_class(self.serializer, horizon=horizon, active_dims=dims)
|
| 383 |
+
self._grammar_cache[key] = grammar
|
| 384 |
+
return grammar
|
runtime/physical/primitives.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Numerical primitive parameters stored in ``primitives.npz``."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Sequence
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from .configuration import ConfigError
|
| 11 |
+
|
| 12 |
+
PRIMITIVES_NAME = "primitives.npz"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _key(dimension: int) -> str:
|
| 16 |
+
return f"dimension_{dimension:04d}"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PrimitiveStore:
|
| 20 |
+
"""Immutable numerical primitive axes in global action-dimension order.
|
| 21 |
+
|
| 22 |
+
Array ``values[j]`` is the decoder vocabulary selected by cell slot ``j``.
|
| 23 |
+
NPZ key names are internal and deliberately absent from ``config.yaml``.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, values: Sequence[np.ndarray | Sequence[float]]) -> None:
|
| 27 |
+
if not values:
|
| 28 |
+
raise ConfigError("primitives must contain at least one dimension")
|
| 29 |
+
arrays = []
|
| 30 |
+
for dimension, raw in enumerate(values):
|
| 31 |
+
array = np.asarray(raw)
|
| 32 |
+
if array.dtype.hasobject:
|
| 33 |
+
raise ConfigError(
|
| 34 |
+
f"primitive dimension {dimension} must not use object dtype"
|
| 35 |
+
)
|
| 36 |
+
if array.ndim != 1 or not len(array):
|
| 37 |
+
raise ConfigError(
|
| 38 |
+
f"primitive dimension {dimension} must be a non-empty vector"
|
| 39 |
+
)
|
| 40 |
+
real_numeric = np.issubdtype(array.dtype, np.integer) or np.issubdtype(
|
| 41 |
+
array.dtype, np.floating
|
| 42 |
+
)
|
| 43 |
+
if not real_numeric or not np.all(np.isfinite(array)):
|
| 44 |
+
raise ConfigError(
|
| 45 |
+
f"primitive dimension {dimension} must contain finite numbers"
|
| 46 |
+
)
|
| 47 |
+
arrays.append(np.array(array, copy=True))
|
| 48 |
+
self.values = tuple(arrays)
|
| 49 |
+
self.bins = tuple(len(array) for array in arrays)
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def dimension(self) -> int:
|
| 53 |
+
return len(self.values)
|
| 54 |
+
|
| 55 |
+
@classmethod
|
| 56 |
+
def from_pretrained(
|
| 57 |
+
cls, directory: str | Path, *, input_dim: int
|
| 58 |
+
) -> PrimitiveStore:
|
| 59 |
+
"""Load exactly one safe numeric vector per expected global dimension."""
|
| 60 |
+
path = Path(directory) / PRIMITIVES_NAME
|
| 61 |
+
with np.load(path, allow_pickle=False) as payload:
|
| 62 |
+
expected = {_key(index) for index in range(input_dim)}
|
| 63 |
+
actual = set(payload.files)
|
| 64 |
+
missing = sorted(expected - actual)
|
| 65 |
+
unknown = sorted(actual - expected)
|
| 66 |
+
if missing:
|
| 67 |
+
raise ConfigError(
|
| 68 |
+
f"{PRIMITIVES_NAME} is missing array(s): {', '.join(missing)}"
|
| 69 |
+
)
|
| 70 |
+
if unknown:
|
| 71 |
+
raise ConfigError(
|
| 72 |
+
f"{PRIMITIVES_NAME} contains unknown array(s): {', '.join(unknown)}"
|
| 73 |
+
)
|
| 74 |
+
values = [payload[_key(index)] for index in range(input_dim)]
|
| 75 |
+
return cls(values)
|
| 76 |
+
|
| 77 |
+
def save_pretrained(self, directory: str | Path) -> None:
|
| 78 |
+
"""Write ``primitives.npz`` without pickled objects or path metadata."""
|
| 79 |
+
path = Path(directory) / PRIMITIVES_NAME
|
| 80 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
np.savez(
|
| 82 |
+
path, **{_key(index): value for index, value in enumerate(self.values)}
|
| 83 |
+
)
|
runtime/physical/semantic.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical physical preprocessing for routed ActionCodec2 profiles.
|
| 2 |
+
|
| 3 |
+
Dataset presets may select columns, convert units, and resample observations,
|
| 4 |
+
but they may not define primitives or BPE rules. This boundary keeps robot
|
| 5 |
+
storage conventions outside the shared physical vocabularies.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from scipy.interpolate import CubicSpline, PchipInterpolator
|
| 14 |
+
from scipy.spatial.transform import Rotation, RotationSpline
|
| 15 |
+
|
| 16 |
+
from ._so3 import euler_xyz_to_quaternion as _euler_xyz_to_quaternion
|
| 17 |
+
from ._so3 import multiply_quaternions as _quaternion_multiply
|
| 18 |
+
from ._so3 import normalize_quaternion as _normalize_quaternion
|
| 19 |
+
from ._so3 import quaternion_to_rotvec as _quaternion_to_rotvec
|
| 20 |
+
from ._so3 import rotvec_to_quaternion as _rotvec_to_quaternion
|
| 21 |
+
|
| 22 |
+
_INTERPOLATION_METHODS = {"linear", "pchip", "cubic"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _validate_rate(value: float, name: str) -> float:
|
| 26 |
+
rate = float(value)
|
| 27 |
+
if not math.isfinite(rate) or rate <= 0.0:
|
| 28 |
+
raise ValueError(f"{name} must be positive and finite")
|
| 29 |
+
return rate
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _quaternion_slerp(
|
| 33 |
+
left: np.ndarray, right: np.ndarray, weight: np.ndarray
|
| 34 |
+
) -> np.ndarray:
|
| 35 |
+
"""Shortest-path SLERP over matching ``(N, 4)`` xyzw arrays."""
|
| 36 |
+
first = _normalize_quaternion(np.asarray(left, dtype=np.float64))
|
| 37 |
+
second = _normalize_quaternion(np.asarray(right, dtype=np.float64))
|
| 38 |
+
dot = np.sum(first * second, axis=1)
|
| 39 |
+
flip = dot < 0.0
|
| 40 |
+
second[flip] *= -1.0
|
| 41 |
+
dot = np.clip(np.abs(dot), 0.0, 1.0)
|
| 42 |
+
output = np.empty_like(first)
|
| 43 |
+
near = dot > 0.9995
|
| 44 |
+
output[near] = first[near] + weight[near, None] * (second[near] - first[near])
|
| 45 |
+
regular = ~near
|
| 46 |
+
if np.any(regular):
|
| 47 |
+
angle = np.arccos(dot[regular])
|
| 48 |
+
denominator = np.sin(angle)
|
| 49 |
+
w = weight[regular]
|
| 50 |
+
output[regular] = (
|
| 51 |
+
np.sin((1.0 - w) * angle)[:, None] / denominator[:, None] * first[regular]
|
| 52 |
+
+ np.sin(w * angle)[:, None] / denominator[:, None] * second[regular]
|
| 53 |
+
)
|
| 54 |
+
return _normalize_quaternion(output)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def resampled_horizon(length: int, *, source_hz: float, target_hz: float) -> int:
|
| 58 |
+
"""Return the target control-step count for a regular source horizon.
|
| 59 |
+
|
| 60 |
+
A ``[T,D]`` action/state target represents ``T`` controller intervals, so
|
| 61 |
+
its duration is ``T / source_hz``. This differs intentionally from a
|
| 62 |
+
point-sampled signal whose first and last rows span only ``T-1`` periods.
|
| 63 |
+
The interval convention maps duration to a rounded number of target-rate
|
| 64 |
+
controller intervals, which is the runtime action-chunk contract.
|
| 65 |
+
"""
|
| 66 |
+
if (
|
| 67 |
+
isinstance(length, bool)
|
| 68 |
+
or not isinstance(length, (int, np.integer))
|
| 69 |
+
or length < 1
|
| 70 |
+
):
|
| 71 |
+
raise ValueError("a trajectory must contain at least one sample")
|
| 72 |
+
source_rate = _validate_rate(source_hz, "source_hz")
|
| 73 |
+
target_rate = _validate_rate(target_hz, "target_hz")
|
| 74 |
+
scaled = int(math.floor(int(length) * target_rate / source_rate + 0.5))
|
| 75 |
+
return max(1, scaled)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _target_times(
|
| 79 |
+
length: int,
|
| 80 |
+
source_hz: float,
|
| 81 |
+
target_hz: float,
|
| 82 |
+
*,
|
| 83 |
+
target_length: int | None,
|
| 84 |
+
) -> np.ndarray:
|
| 85 |
+
count = (
|
| 86 |
+
resampled_horizon(length, source_hz=source_hz, target_hz=target_hz)
|
| 87 |
+
if target_length is None
|
| 88 |
+
else target_length
|
| 89 |
+
)
|
| 90 |
+
if isinstance(count, bool) or not isinstance(count, (int, np.integer)) or count < 1:
|
| 91 |
+
raise ValueError("target_length must be a positive integer")
|
| 92 |
+
return np.arange(int(count), dtype=np.float64) / target_hz
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _guarded_natural_cubic(
|
| 96 |
+
source_time: np.ndarray,
|
| 97 |
+
values: np.ndarray,
|
| 98 |
+
target_time: np.ndarray,
|
| 99 |
+
) -> np.ndarray:
|
| 100 |
+
"""Use natural cubic unless an axis materially leaves its observed envelope.
|
| 101 |
+
|
| 102 |
+
Natural cubic is C2-continuous but can overshoot sparse control knots.
|
| 103 |
+
Falling back only the offending additive axes to PCHIP preserves the
|
| 104 |
+
smoother curve wherever it is supported by observations while preventing
|
| 105 |
+
interpolation from inventing a large new coordinate-wise extremum.
|
| 106 |
+
"""
|
| 107 |
+
sampled = np.asarray(
|
| 108 |
+
CubicSpline(source_time, values, axis=0, bc_type="natural")(target_time),
|
| 109 |
+
dtype=np.float64,
|
| 110 |
+
)
|
| 111 |
+
lower = np.min(values, axis=0)
|
| 112 |
+
upper = np.max(values, axis=0)
|
| 113 |
+
# Small cubic ringing can be the mechanism that removes velocity corners;
|
| 114 |
+
# treat only a material (>5% of the observed axis span) new extremum as an
|
| 115 |
+
# unsafe invention. Profile order-0/order-1 checks remain the hard gate.
|
| 116 |
+
tolerance = 1e-9 + 0.05 * (upper - lower)
|
| 117 |
+
fallback_axes = np.any(
|
| 118 |
+
(sampled < lower[None] - tolerance[None])
|
| 119 |
+
| (sampled > upper[None] + tolerance[None]),
|
| 120 |
+
axis=0,
|
| 121 |
+
)
|
| 122 |
+
if np.any(fallback_axes):
|
| 123 |
+
sampled[:, fallback_axes] = PchipInterpolator(
|
| 124 |
+
source_time, values[:, fallback_axes], axis=0
|
| 125 |
+
)(target_time)
|
| 126 |
+
return sampled
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def resample_uniform(
|
| 130 |
+
values: np.ndarray,
|
| 131 |
+
*,
|
| 132 |
+
source_hz: float,
|
| 133 |
+
target_hz: float,
|
| 134 |
+
kind: str = "linear",
|
| 135 |
+
target_length: int | None = None,
|
| 136 |
+
method: str = "linear",
|
| 137 |
+
) -> np.ndarray:
|
| 138 |
+
"""Resample one regular control-horizon trajectory onto a target clock.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
values (np.ndarray): ``(T, D)`` continuous/discrete state samples.
|
| 142 |
+
source_hz: Source frequency in Hz.
|
| 143 |
+
target_hz: Target frequency in Hz.
|
| 144 |
+
kind: ``linear``, ``hold``, ``pose_xyzw``, or ``pose_euler_xyz``.
|
| 145 |
+
Pose output is always ``(x, y, z, qx, qy, qz, qw)`` and uses
|
| 146 |
+
SLERP for orientation.
|
| 147 |
+
target_length: Explicit target horizon. By default the interval
|
| 148 |
+
duration ``len(values) / source_hz`` is preserved.
|
| 149 |
+
method: ``linear`` uses linear translation plus SLERP. ``pchip`` uses
|
| 150 |
+
shape-preserving cubic translation. ``cubic`` uses a natural cubic
|
| 151 |
+
translation spline, with per-axis PCHIP fallback if the spline
|
| 152 |
+
materially leaves the source trajectory's observed value envelope.
|
| 153 |
+
Both cubic methods use an intrinsic SO(3) ``RotationSpline`` for
|
| 154 |
+
orientation.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
np.ndarray: ``(T_target, D)`` regularly sampled targets. When
|
| 158 |
+
upsampling, times after the final source knot hold its value until
|
| 159 |
+
the final source control interval ends.
|
| 160 |
+
"""
|
| 161 |
+
source_rate = _validate_rate(source_hz, "source_hz")
|
| 162 |
+
target_rate = _validate_rate(target_hz, "target_hz")
|
| 163 |
+
array = np.asarray(values, dtype=np.float64)
|
| 164 |
+
if array.ndim != 2 or len(array) < 1 or not np.all(np.isfinite(array)):
|
| 165 |
+
raise ValueError("values must be a non-empty finite [T, D] array")
|
| 166 |
+
if kind not in {"linear", "hold", "pose_xyzw", "pose_euler_xyz"}:
|
| 167 |
+
raise ValueError("unsupported resampling kind")
|
| 168 |
+
if method not in _INTERPOLATION_METHODS:
|
| 169 |
+
raise ValueError(f"method must be one of {sorted(_INTERPOLATION_METHODS)}")
|
| 170 |
+
if kind == "pose_xyzw" and array.shape[1] != 7:
|
| 171 |
+
raise ValueError("pose_xyzw values must have shape [T, 7]")
|
| 172 |
+
if kind == "pose_euler_xyz" and array.shape[1] != 6:
|
| 173 |
+
raise ValueError("pose_euler_xyz values must have shape [T, 6]")
|
| 174 |
+
standard_length = resampled_horizon(
|
| 175 |
+
len(array), source_hz=source_rate, target_hz=target_rate
|
| 176 |
+
)
|
| 177 |
+
if source_rate == target_rate and target_length in {None, standard_length}:
|
| 178 |
+
if kind == "pose_euler_xyz":
|
| 179 |
+
array = np.concatenate(
|
| 180 |
+
(array[:, :3], _euler_xyz_to_quaternion(array[:, 3:6])), axis=1
|
| 181 |
+
)
|
| 182 |
+
return np.asarray(array, dtype=np.float32)
|
| 183 |
+
|
| 184 |
+
target_time = _target_times(
|
| 185 |
+
len(array), source_rate, target_rate, target_length=target_length
|
| 186 |
+
)
|
| 187 |
+
source_time = np.arange(len(array), dtype=np.float64) / source_rate
|
| 188 |
+
interpolation_time = np.minimum(target_time, source_time[-1])
|
| 189 |
+
source_position = target_time * source_rate
|
| 190 |
+
left = np.minimum(np.floor(source_position).astype(np.int64), len(array) - 1)
|
| 191 |
+
right = np.minimum(left + 1, len(array) - 1)
|
| 192 |
+
weight = source_position - left
|
| 193 |
+
if kind == "hold":
|
| 194 |
+
result = array[left]
|
| 195 |
+
else:
|
| 196 |
+
pose = kind in {"pose_xyzw", "pose_euler_xyz"}
|
| 197 |
+
continuous = array[:, :3] if pose else array
|
| 198 |
+
if len(array) == 1:
|
| 199 |
+
sampled = np.repeat(continuous, len(target_time), axis=0)
|
| 200 |
+
elif method == "pchip":
|
| 201 |
+
sampled = PchipInterpolator(source_time, continuous, axis=0)(
|
| 202 |
+
interpolation_time
|
| 203 |
+
)
|
| 204 |
+
elif method == "cubic":
|
| 205 |
+
sampled = _guarded_natural_cubic(
|
| 206 |
+
source_time, continuous, interpolation_time
|
| 207 |
+
)
|
| 208 |
+
else:
|
| 209 |
+
sampled = array[left, : continuous.shape[1]] + weight[:, None] * (
|
| 210 |
+
array[right, : continuous.shape[1]] - array[left, : continuous.shape[1]]
|
| 211 |
+
)
|
| 212 |
+
if not pose:
|
| 213 |
+
result = sampled
|
| 214 |
+
else:
|
| 215 |
+
quaternion = (
|
| 216 |
+
array[:, 3:7]
|
| 217 |
+
if kind == "pose_xyzw"
|
| 218 |
+
else _euler_xyz_to_quaternion(array[:, 3:6])
|
| 219 |
+
)
|
| 220 |
+
if len(array) == 1:
|
| 221 |
+
rotation = np.repeat(quaternion, len(target_time), axis=0)
|
| 222 |
+
elif method == "linear":
|
| 223 |
+
rotation = _quaternion_slerp(
|
| 224 |
+
quaternion[left], quaternion[right], weight
|
| 225 |
+
)
|
| 226 |
+
else:
|
| 227 |
+
rotation = RotationSpline(source_time, Rotation.from_quat(quaternion))(
|
| 228 |
+
interpolation_time
|
| 229 |
+
).as_quat()
|
| 230 |
+
result = np.concatenate((sampled, rotation), axis=1)
|
| 231 |
+
return np.asarray(result, dtype=np.float32)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def eef_pose_absolute(poses: np.ndarray) -> np.ndarray:
|
| 235 |
+
"""Convert canonical ``xyz + xyzw`` state to ``xyz + rotation_vector``."""
|
| 236 |
+
values = np.asarray(poses, dtype=np.float64)
|
| 237 |
+
if values.ndim != 2 or values.shape[1] != 7 or not np.all(np.isfinite(values)):
|
| 238 |
+
raise ValueError("poses must be a finite [T, 7] array")
|
| 239 |
+
return np.asarray(
|
| 240 |
+
np.concatenate((values[:, :3], _quaternion_to_rotvec(values[:, 3:7])), axis=1),
|
| 241 |
+
dtype=np.float32,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def eef_pose_components(poses: np.ndarray, representation: str) -> np.ndarray:
|
| 246 |
+
"""Convert canonical ``xyz + xyzw`` poses to one registered source layout."""
|
| 247 |
+
values = np.asarray(poses, dtype=np.float64)
|
| 248 |
+
if values.ndim != 2 or values.shape[1] != 7 or not np.all(np.isfinite(values)):
|
| 249 |
+
raise ValueError("poses must be a finite [T, 7] array")
|
| 250 |
+
quaternion = _normalize_quaternion(values[:, 3:7])
|
| 251 |
+
if representation == "rotation_vector":
|
| 252 |
+
rotation = _quaternion_to_rotvec(quaternion)
|
| 253 |
+
elif representation == "euler_xyz":
|
| 254 |
+
rotation = Rotation.from_quat(quaternion).as_euler("xyz")
|
| 255 |
+
elif representation == "quaternion_xyzw":
|
| 256 |
+
rotation = quaternion
|
| 257 |
+
else:
|
| 258 |
+
raise ValueError("unsupported EEF rotation representation")
|
| 259 |
+
return np.asarray(
|
| 260 |
+
np.concatenate((values[:, :3], rotation), axis=1), dtype=np.float32
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def eef_pose_from_components(
|
| 265 |
+
position: np.ndarray,
|
| 266 |
+
rotation: np.ndarray,
|
| 267 |
+
representation: str,
|
| 268 |
+
) -> np.ndarray:
|
| 269 |
+
"""Build canonical ``xyz + xyzw`` poses from an explicit source layout.
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
position (np.ndarray): ``(T, 3)`` translation in metres.
|
| 273 |
+
rotation (np.ndarray): ``(T, 3)`` rotation vectors/XYZ Euler angles or
|
| 274 |
+
``(T, 4)`` xyzw quaternions.
|
| 275 |
+
representation: ``rotation_vector``, ``euler_xyz``, or
|
| 276 |
+
``quaternion_xyzw``.
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
np.ndarray: Finite normalized poses with shape ``(T, 7)``.
|
| 280 |
+
"""
|
| 281 |
+
translation = np.asarray(position, dtype=np.float64)
|
| 282 |
+
orientation = np.asarray(rotation, dtype=np.float64)
|
| 283 |
+
if translation.ndim != 2 or translation.shape[1] != 3 or len(translation) < 1:
|
| 284 |
+
raise ValueError("position must have non-empty shape [T, 3]")
|
| 285 |
+
if len(orientation) != len(translation) or not np.all(np.isfinite(translation)):
|
| 286 |
+
raise ValueError("position and rotation must have matching finite trajectories")
|
| 287 |
+
if representation == "rotation_vector":
|
| 288 |
+
if orientation.ndim != 2 or orientation.shape[1] != 3:
|
| 289 |
+
raise ValueError("rotation_vector must have shape [T, 3]")
|
| 290 |
+
quaternion = _rotvec_to_quaternion(orientation)
|
| 291 |
+
elif representation == "euler_xyz":
|
| 292 |
+
if orientation.ndim != 2 or orientation.shape[1] != 3:
|
| 293 |
+
raise ValueError("euler_xyz must have shape [T, 3]")
|
| 294 |
+
quaternion = _euler_xyz_to_quaternion(orientation)
|
| 295 |
+
elif representation == "quaternion_xyzw":
|
| 296 |
+
if orientation.ndim != 2 or orientation.shape[1] != 4:
|
| 297 |
+
raise ValueError("quaternion_xyzw must have shape [T, 4]")
|
| 298 |
+
quaternion = _normalize_quaternion(orientation)
|
| 299 |
+
else:
|
| 300 |
+
raise ValueError("unsupported EEF rotation representation")
|
| 301 |
+
if not np.all(np.isfinite(orientation)):
|
| 302 |
+
raise ValueError("position and rotation must be finite")
|
| 303 |
+
return np.asarray(
|
| 304 |
+
np.concatenate((translation, quaternion), axis=1), dtype=np.float32
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def eef_pose_local_absolute(
|
| 309 |
+
poses: np.ndarray, *, initial_pose: np.ndarray | None = None
|
| 310 |
+
) -> np.ndarray:
|
| 311 |
+
"""Express measured poses relative to an explicit or inferred initial pose.
|
| 312 |
+
|
| 313 |
+
Translation is relative to the initial position. Orientation is the
|
| 314 |
+
right-multiplicative relative pose ``q_initial^-1 * q_t``, matching the
|
| 315 |
+
shared EEF profile. When ``initial_pose`` is omitted, the first observation
|
| 316 |
+
is the initial pose and row zero is exactly zero/identity. This is a local
|
| 317 |
+
position trajectory, not a per-step delta sequence.
|
| 318 |
+
"""
|
| 319 |
+
values = np.asarray(poses, dtype=np.float64)
|
| 320 |
+
if (
|
| 321 |
+
values.ndim != 2
|
| 322 |
+
or values.shape[1] != 7
|
| 323 |
+
or len(values) < 1
|
| 324 |
+
or not np.all(np.isfinite(values))
|
| 325 |
+
):
|
| 326 |
+
raise ValueError("poses must be a finite non-empty [T, 7] array")
|
| 327 |
+
if initial_pose is None:
|
| 328 |
+
initial = values[:1]
|
| 329 |
+
else:
|
| 330 |
+
initial = np.asarray(initial_pose, dtype=np.float64)
|
| 331 |
+
if initial.shape == (7,):
|
| 332 |
+
initial = initial[None]
|
| 333 |
+
if initial.shape != (1, 7) or not np.all(np.isfinite(initial)):
|
| 334 |
+
raise ValueError("initial_pose must be a finite [7] pose")
|
| 335 |
+
quaternion = _normalize_quaternion(values[:, 3:7])
|
| 336 |
+
initial_quaternion = _normalize_quaternion(initial[:, 3:7])
|
| 337 |
+
inverse_start = initial_quaternion.copy()
|
| 338 |
+
inverse_start[:, :3] *= -1.0
|
| 339 |
+
relative = _quaternion_multiply(
|
| 340 |
+
np.broadcast_to(inverse_start, quaternion.shape), quaternion
|
| 341 |
+
)
|
| 342 |
+
return np.asarray(
|
| 343 |
+
np.concatenate(
|
| 344 |
+
(values[:, :3] - initial[:, :3], _quaternion_to_rotvec(relative)), axis=1
|
| 345 |
+
),
|
| 346 |
+
dtype=np.float32,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def eef_pose_deltas(poses: np.ndarray) -> np.ndarray:
|
| 351 |
+
"""Differentiate a local ``xyz + xyzw`` path from zero/identity.
|
| 352 |
+
|
| 353 |
+
Translation uses fixed-axis differences. Rotation uses right-multiplicative
|
| 354 |
+
increments ``q_previous^-1 * q_t``. The returned ``(T, 6)`` array is the
|
| 355 |
+
exact inverse convention of :func:`integrate_eef_deltas` before floating
|
| 356 |
+
point roundoff.
|
| 357 |
+
"""
|
| 358 |
+
values = np.asarray(poses, dtype=np.float64)
|
| 359 |
+
if (
|
| 360 |
+
values.ndim != 2
|
| 361 |
+
or values.shape[1] != 7
|
| 362 |
+
or len(values) < 1
|
| 363 |
+
or not np.all(np.isfinite(values))
|
| 364 |
+
):
|
| 365 |
+
raise ValueError("poses must be a finite non-empty [T, 7] array")
|
| 366 |
+
quaternion = _normalize_quaternion(values[:, 3:7])
|
| 367 |
+
previous_position = np.concatenate((np.zeros((1, 3)), values[:-1, :3]), axis=0)
|
| 368 |
+
identity = np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float64)
|
| 369 |
+
previous_quaternion = np.concatenate((identity, quaternion[:-1]), axis=0)
|
| 370 |
+
inverse_previous = previous_quaternion.copy()
|
| 371 |
+
inverse_previous[:, :3] *= -1.0
|
| 372 |
+
increment = _quaternion_multiply(inverse_previous, quaternion)
|
| 373 |
+
return np.asarray(
|
| 374 |
+
np.concatenate(
|
| 375 |
+
(values[:, :3] - previous_position, _quaternion_to_rotvec(increment)),
|
| 376 |
+
axis=1,
|
| 377 |
+
),
|
| 378 |
+
dtype=np.float32,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def eef_pose_from_local_absolute(
|
| 383 |
+
poses: np.ndarray, *, initial_pose: np.ndarray
|
| 384 |
+
) -> np.ndarray:
|
| 385 |
+
"""Attach an external initial pose to a local ``xyz + xyzw`` path."""
|
| 386 |
+
values = np.asarray(poses, dtype=np.float64)
|
| 387 |
+
initial = np.asarray(initial_pose, dtype=np.float64)
|
| 388 |
+
if (
|
| 389 |
+
values.ndim != 2
|
| 390 |
+
or values.shape[1] != 7
|
| 391 |
+
or len(values) < 1
|
| 392 |
+
or not np.all(np.isfinite(values))
|
| 393 |
+
):
|
| 394 |
+
raise ValueError("poses must be a finite non-empty [T, 7] array")
|
| 395 |
+
if initial.shape == (7,):
|
| 396 |
+
initial = initial[None]
|
| 397 |
+
if initial.shape != (1, 7) or not np.all(np.isfinite(initial)):
|
| 398 |
+
raise ValueError("initial_pose must be a finite [7] pose")
|
| 399 |
+
local_quaternion = _normalize_quaternion(values[:, 3:7])
|
| 400 |
+
initial_quaternion = _normalize_quaternion(initial[:, 3:7])
|
| 401 |
+
quaternion = _quaternion_multiply(
|
| 402 |
+
np.broadcast_to(initial_quaternion, local_quaternion.shape), local_quaternion
|
| 403 |
+
)
|
| 404 |
+
return np.asarray(
|
| 405 |
+
np.concatenate((values[:, :3] + initial[:, :3], quaternion), axis=1),
|
| 406 |
+
dtype=np.float32,
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def integrate_eef_deltas(deltas: np.ndarray) -> np.ndarray:
|
| 411 |
+
"""Integrate ``[dx,dy,dz,body-rotvec]`` from a zero/identity pose.
|
| 412 |
+
|
| 413 |
+
Args:
|
| 414 |
+
deltas (np.ndarray): ``(T, 6)`` translation and right-multiplicative
|
| 415 |
+
rotation increments in canonical units.
|
| 416 |
+
|
| 417 |
+
Returns:
|
| 418 |
+
np.ndarray: ``(T, 7)`` absolute ``xyz + xyzw`` poses. Row ``t`` is the
|
| 419 |
+
pose after applying delta ``t``; the implicit boundary before row zero
|
| 420 |
+
is position zero and identity orientation.
|
| 421 |
+
"""
|
| 422 |
+
values = np.asarray(deltas, dtype=np.float64)
|
| 423 |
+
if values.ndim != 2 or values.shape[1] != 6 or not np.all(np.isfinite(values)):
|
| 424 |
+
raise ValueError("deltas must be a finite [T, 6] array")
|
| 425 |
+
if len(values) < 1:
|
| 426 |
+
raise ValueError("deltas must contain at least one step")
|
| 427 |
+
translation = np.cumsum(values[:, :3], axis=0)
|
| 428 |
+
increments = _rotvec_to_quaternion(values[:, 3:6])
|
| 429 |
+
orientation = np.empty((len(values), 4), dtype=np.float64)
|
| 430 |
+
current = np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float64)
|
| 431 |
+
for index, increment in enumerate(increments):
|
| 432 |
+
current = _quaternion_multiply(current, increment[None])
|
| 433 |
+
orientation[index] = current[0]
|
| 434 |
+
return np.asarray(
|
| 435 |
+
np.concatenate((translation, orientation), axis=1), dtype=np.float32
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def binarize_gripper_actions(
|
| 440 |
+
values: np.ndarray,
|
| 441 |
+
*,
|
| 442 |
+
threshold: float,
|
| 443 |
+
open_is_high: bool,
|
| 444 |
+
) -> np.ndarray:
|
| 445 |
+
"""Map one controller gripper channel to canonical ``0=closed, 1=open``."""
|
| 446 |
+
array = np.asarray(values, dtype=np.float32)
|
| 447 |
+
if array.ndim != 2 or array.shape[1] != 1 or not np.all(np.isfinite(array)):
|
| 448 |
+
raise ValueError("gripper action must be a finite [T, 1] array")
|
| 449 |
+
if not math.isfinite(threshold):
|
| 450 |
+
raise ValueError("gripper threshold must be finite")
|
| 451 |
+
opened = array >= threshold if open_is_high else array <= threshold
|
| 452 |
+
return opened.astype(np.float32)
|
runtime/physical/trajectory.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Variable-width trajectory contract shared by statistics and fitting."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Sequence
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from ..common._validation import active_dimensions
|
| 11 |
+
from .configuration import ActionCodec2Config
|
| 12 |
+
|
| 13 |
+
__all__ = ["ActiveTrajectory", "TrajectoryLike", "prepare_trajectory"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class ActiveTrajectory:
|
| 18 |
+
"""One complete trajectory over an explicit subset of codebook dimensions.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
values (np.ndarray): ``(T, D_active)`` finite canonical values. ``T`` and
|
| 22 |
+
``D_active`` may differ between trajectories.
|
| 23 |
+
active_dims: Strictly increasing global codebook dimension ids. This is
|
| 24 |
+
required when compact columns are not the global prefix, for example
|
| 25 |
+
``[j1..j6, gripper]`` in a seven-joint-plus-gripper codebook.
|
| 26 |
+
previous_first_order (np.ndarray | None): ``(D_active,)`` preceding
|
| 27 |
+
reconstructed first-order values in canonical input units. Required
|
| 28 |
+
by second-order fitting; never inferred from a zero boundary.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
values: np.ndarray
|
| 32 |
+
active_dims: tuple[int, ...]
|
| 33 |
+
previous_first_order: np.ndarray | None = None
|
| 34 |
+
|
| 35 |
+
def __init__(
|
| 36 |
+
self,
|
| 37 |
+
values: np.ndarray,
|
| 38 |
+
active_dims: Sequence[int],
|
| 39 |
+
*,
|
| 40 |
+
previous_first_order: np.ndarray | None = None,
|
| 41 |
+
) -> None:
|
| 42 |
+
object.__setattr__(self, "values", np.asarray(values))
|
| 43 |
+
object.__setattr__(self, "active_dims", tuple(active_dims))
|
| 44 |
+
previous = (
|
| 45 |
+
None
|
| 46 |
+
if previous_first_order is None
|
| 47 |
+
else np.asarray(previous_first_order, dtype=np.float32)
|
| 48 |
+
)
|
| 49 |
+
if previous is not None and (
|
| 50 |
+
previous.shape != (len(self.active_dims),)
|
| 51 |
+
or not np.all(np.isfinite(previous))
|
| 52 |
+
):
|
| 53 |
+
raise ValueError(
|
| 54 |
+
"previous_first_order must be finite with shape [D_active]"
|
| 55 |
+
)
|
| 56 |
+
object.__setattr__(self, "previous_first_order", previous)
|
| 57 |
+
|
| 58 |
+
@classmethod
|
| 59 |
+
def from_padded(
|
| 60 |
+
cls,
|
| 61 |
+
values: np.ndarray,
|
| 62 |
+
*,
|
| 63 |
+
active_dims: Sequence[int],
|
| 64 |
+
previous_first_order: np.ndarray | None = None,
|
| 65 |
+
) -> ActiveTrajectory:
|
| 66 |
+
"""Drop engineering-only dense padding before the semantic boundary.
|
| 67 |
+
|
| 68 |
+
Values in inactive columns are deliberately not inspected. They may be
|
| 69 |
+
zero, NaN, or any collator sentinel because they never reach physical
|
| 70 |
+
quantization, statistics, or Set-BPE.
|
| 71 |
+
"""
|
| 72 |
+
array = np.asarray(values)
|
| 73 |
+
dims = tuple(int(value) for value in active_dims)
|
| 74 |
+
if array.ndim != 2 or not dims or min(dims) < 0 or max(dims) >= array.shape[1]:
|
| 75 |
+
raise ValueError(
|
| 76 |
+
"padded values and active_dims do not define a valid trajectory"
|
| 77 |
+
)
|
| 78 |
+
previous = (
|
| 79 |
+
None
|
| 80 |
+
if previous_first_order is None
|
| 81 |
+
else np.take(previous_first_order, dims)
|
| 82 |
+
)
|
| 83 |
+
return cls(np.take(array, dims, axis=1), dims, previous_first_order=previous)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def trajectory_boundary(
|
| 87 |
+
trajectory: TrajectoryLike, config: ActionCodec2Config, active_dims: tuple[int, ...]
|
| 88 |
+
) -> np.ndarray | None:
|
| 89 |
+
"""Expand explicit compact state to ``[D]`` for fitting and validation."""
|
| 90 |
+
previous = getattr(trajectory, "previous_first_order", None)
|
| 91 |
+
active = set(active_dims)
|
| 92 |
+
required = any(
|
| 93 |
+
c.primitive_order == 2 and active.intersection(c.dims)
|
| 94 |
+
for c in config.physical.components.values()
|
| 95 |
+
)
|
| 96 |
+
if previous is None and required:
|
| 97 |
+
raise ValueError(
|
| 98 |
+
"second-order trajectories require explicit previous_first_order"
|
| 99 |
+
)
|
| 100 |
+
if previous is None and not any(
|
| 101 |
+
c.primitive_order == 2 for c in config.physical.components.values()
|
| 102 |
+
):
|
| 103 |
+
return None
|
| 104 |
+
dense = np.zeros(config.physical.input_dim, dtype=np.float32)
|
| 105 |
+
if previous is not None:
|
| 106 |
+
dense[list(active_dims)] = previous
|
| 107 |
+
return dense
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
TrajectoryLike = np.ndarray | ActiveTrajectory
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def prepare_trajectory(
|
| 114 |
+
trajectory: TrajectoryLike,
|
| 115 |
+
*,
|
| 116 |
+
dimension: int,
|
| 117 |
+
index: int,
|
| 118 |
+
dtype: np.dtype | type = np.float32,
|
| 119 |
+
) -> tuple[np.ndarray, tuple[int, ...]]:
|
| 120 |
+
"""Validate a trajectory and return compact values plus global dimension ids.
|
| 121 |
+
|
| 122 |
+
A plain ``(T,D)`` array uses prefix dimensions ``0..D-1``. Callers must use
|
| 123 |
+
:class:`ActiveTrajectory` for a non-prefix layout. No padding value is part
|
| 124 |
+
of this public contract.
|
| 125 |
+
"""
|
| 126 |
+
if isinstance(trajectory, ActiveTrajectory):
|
| 127 |
+
raw = trajectory.values
|
| 128 |
+
active_dims = trajectory.active_dims
|
| 129 |
+
else:
|
| 130 |
+
raw = trajectory
|
| 131 |
+
array = np.asarray(raw)
|
| 132 |
+
active_dims = tuple(range(array.shape[1])) if array.ndim == 2 else ()
|
| 133 |
+
|
| 134 |
+
values = np.asarray(raw, dtype=dtype)
|
| 135 |
+
if values.ndim != 2 or len(values) < 1 or values.shape[1] < 1:
|
| 136 |
+
raise ValueError(
|
| 137 |
+
f"trajectory {index} must have non-empty shape [T, D_active], got {values.shape}"
|
| 138 |
+
)
|
| 139 |
+
if len(active_dims) != values.shape[1]:
|
| 140 |
+
raise ValueError(
|
| 141 |
+
f"trajectory {index} active_dims has {len(active_dims)} entries for "
|
| 142 |
+
f"width {values.shape[1]}"
|
| 143 |
+
)
|
| 144 |
+
if any(isinstance(item, bool) or not isinstance(item, int) for item in active_dims):
|
| 145 |
+
raise ValueError(f"trajectory {index} active_dims must contain integers")
|
| 146 |
+
active_dims = active_dimensions(
|
| 147 |
+
active_dims, dimension, subject=f"trajectory {index} active_dims"
|
| 148 |
+
)
|
| 149 |
+
if not np.all(np.isfinite(values)):
|
| 150 |
+
raise ValueError(f"trajectory {index} contains a non-finite value")
|
| 151 |
+
return np.ascontiguousarray(values), active_dims
|
runtime/processing_actioncodec2.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face processor over the shared ActionCodec2 frontend implementation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from collections.abc import Mapping
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 13 |
+
from transformers.processing_utils import ProcessorMixin
|
| 14 |
+
|
| 15 |
+
from .routing.defaults import CodecBuilder, ProfileBundleBuilder, builtin_action_spaces
|
| 16 |
+
from .integration.hf import (
|
| 17 |
+
PROCESSOR_CONFIG_NAME,
|
| 18 |
+
ROUTER_ALLOW_PATTERNS,
|
| 19 |
+
ActionCodec2FrontendHFConfig,
|
| 20 |
+
resolve_pretrained_directory,
|
| 21 |
+
save_huggingface_metadata,
|
| 22 |
+
save_model_card,
|
| 23 |
+
upload_pretrained_directory,
|
| 24 |
+
)
|
| 25 |
+
from .routing.presets import PresetConfig
|
| 26 |
+
from .routing.routing import ActionCodec2Router
|
| 27 |
+
from .frontend.tokenizer import ActionCodec2Tokenizer
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _saved_metadata(directory: Path) -> dict[str, Any]:
|
| 31 |
+
metadata = directory / PROCESSOR_CONFIG_NAME
|
| 32 |
+
return (
|
| 33 |
+
json.loads(metadata.read_text(encoding="utf-8")) if metadata.is_file() else {}
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _fitted_status(profiles) -> str:
|
| 38 |
+
status = ", ".join(sorted(profiles)) or "none; call codec.fit(episodes)"
|
| 39 |
+
return f"Fitted profiles: {status}\n"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ActionCodec2(ActionCodec2Tokenizer, ProcessorMixin):
|
| 43 |
+
"""Action-space-bound processor with lossless token serialization.
|
| 44 |
+
|
| 45 |
+
``encode`` returns ragged token-ID rows; ``decode`` reconstructs a CPU
|
| 46 |
+
``torch.float32`` tensor in the original ``(B,T,D)`` column layout.
|
| 47 |
+
Calling the processor wraps those same rows in a ``BatchFeature`` under
|
| 48 |
+
``input_ids``. Physical quantization remains lossy as configured.
|
| 49 |
+
|
| 50 |
+
Released first- and second-order bundles both register 13 layouts: single
|
| 51 |
+
and dual EEF arms, single and dual arms with 6 or 7 joint coordinates, each
|
| 52 |
+
with absolute/delta variants and binary grippers, plus a six-continuous-
|
| 53 |
+
coordinate absolute layout without a separate binary gripper column.
|
| 54 |
+
The registered names and column meanings come from the loaded artifact.
|
| 55 |
+
``print(codec)`` lists all of them; ``print(cls.describe_pretrained(path))``
|
| 56 |
+
inspects them before binding. ``print_action_spaces(action_dim=D)`` only
|
| 57 |
+
filters candidates, because shape alone cannot identify physical meaning.
|
| 58 |
+
|
| 59 |
+
Inputs accept ``(T,D)`` or ``(B,T,D)`` with any positive T; unequal-length
|
| 60 |
+
episodes must be encoded separately. Absolute layouts require the same
|
| 61 |
+
``current_state(B,D)`` for encode/decode. Second-order physical profiles
|
| 62 |
+
additionally require ``previous_first_order: {component: (B,D_active)}``
|
| 63 |
+
at the action space's configured codec rate, independently of the
|
| 64 |
+
absolute/delta choice.
|
| 65 |
+
Boundary arrays retain B=1 for a single episode. Physical order does not
|
| 66 |
+
change the caller's action layout into an acceleration tensor.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
backend: Optional fitted router. Omit to construct a fresh codec for fit().
|
| 70 |
+
action_space: Registered name defining the input tensor's semantics.
|
| 71 |
+
primitive_order: Physical primitive order, 1 or 2, for fresh construction.
|
| 72 |
+
bpe_scheme: Optional 'set_bpe' or 'bpe' override for fresh construction.
|
| 73 |
+
profile_configs: Optional joint/EEF physical configs, mappings or YAML paths.
|
| 74 |
+
Defaults ship inside the package; no repository checkout is required.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
model_type = ActionCodec2FrontendHFConfig.model_type
|
| 78 |
+
model_input_names = ["input_ids"]
|
| 79 |
+
# Numerical artifacts are owned by the router, not modality sub-processors.
|
| 80 |
+
attributes = []
|
| 81 |
+
|
| 82 |
+
def __init__(
|
| 83 |
+
self,
|
| 84 |
+
backend: ActionCodec2Router | None = None,
|
| 85 |
+
*,
|
| 86 |
+
action_space: str,
|
| 87 |
+
primitive_order: int = 1,
|
| 88 |
+
bpe_scheme: str | None = None,
|
| 89 |
+
profile_configs: Mapping[str, object] | None = None,
|
| 90 |
+
) -> None:
|
| 91 |
+
fresh = backend is None
|
| 92 |
+
if fresh:
|
| 93 |
+
backend = CodecBuilder(
|
| 94 |
+
primitive_order=primitive_order,
|
| 95 |
+
bpe_scheme=bpe_scheme,
|
| 96 |
+
profile_configs=profile_configs,
|
| 97 |
+
).build()
|
| 98 |
+
elif (
|
| 99 |
+
primitive_order != 1
|
| 100 |
+
or bpe_scheme is not None
|
| 101 |
+
or profile_configs is not None
|
| 102 |
+
):
|
| 103 |
+
raise ValueError(
|
| 104 |
+
"profile construction options cannot accompany an existing backend"
|
| 105 |
+
)
|
| 106 |
+
super().__init__(backend, action_space=action_space)
|
| 107 |
+
ProcessorMixin.__init__(self)
|
| 108 |
+
self._fitted_profiles = frozenset() if fresh else frozenset(backend.profiles)
|
| 109 |
+
self.training_report = None
|
| 110 |
+
self._source_directory = Path(__file__).parent
|
| 111 |
+
|
| 112 |
+
def __dir__(self):
|
| 113 |
+
inapplicable = {
|
| 114 |
+
"action_tokenizer",
|
| 115 |
+
"attributes",
|
| 116 |
+
"audio_tokenizer",
|
| 117 |
+
"chat_template",
|
| 118 |
+
"check_argument_for_proper_class",
|
| 119 |
+
"encode_kwargs",
|
| 120 |
+
"feature_extractor_class",
|
| 121 |
+
"from_args_and_dict",
|
| 122 |
+
"get_possibly_dynamic_module",
|
| 123 |
+
"get_processor_dict",
|
| 124 |
+
"model_input_names",
|
| 125 |
+
"model_type",
|
| 126 |
+
"optional_attributes",
|
| 127 |
+
"optional_call_args",
|
| 128 |
+
"apply_chat_template",
|
| 129 |
+
"post_process_image_text_to_text",
|
| 130 |
+
"pad_action_id",
|
| 131 |
+
"bos_blk_id",
|
| 132 |
+
"parts_meta",
|
| 133 |
+
"BAR_SENTINEL_TOKENS",
|
| 134 |
+
"push_to_hub",
|
| 135 |
+
"register_for_auto_class",
|
| 136 |
+
"tokenizer_class",
|
| 137 |
+
"to_dict",
|
| 138 |
+
"to_json_file",
|
| 139 |
+
"to_json_string",
|
| 140 |
+
"valid_processor_kwargs",
|
| 141 |
+
"validate_init_kwargs",
|
| 142 |
+
}
|
| 143 |
+
return [name for name in super().__dir__() if name not in inapplicable]
|
| 144 |
+
|
| 145 |
+
def __repr__(self) -> str:
|
| 146 |
+
return (
|
| 147 |
+
f"ActionCodec2(action_space={self.action_space.name!r}, "
|
| 148 |
+
f"D={self.action_dim}, vocab={self.action_tokenizer.vocab_size})"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
def builtin_action_spaces():
|
| 153 |
+
"""Return the immutable named layout definitions shipped in the package."""
|
| 154 |
+
return builtin_action_spaces()
|
| 155 |
+
|
| 156 |
+
@classmethod
|
| 157 |
+
def from_profiles(cls, joint, eef, *, action_space: str, token_budgets=None):
|
| 158 |
+
"""Bind an artifact assembled from existing joint/EEF profile directories.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
joint: Fitted joint profile directory.
|
| 162 |
+
eef: Fitted EEF profile directory.
|
| 163 |
+
action_space: Explicit built-in tensor layout to bind.
|
| 164 |
+
token_budgets: Optional joint/EEF overrides; defaults to recorded
|
| 165 |
+
training budgets, or fitted vocabulary sizes if unavailable.
|
| 166 |
+
"""
|
| 167 |
+
router = ProfileBundleBuilder(joint, eef, token_budgets=token_budgets).build()
|
| 168 |
+
return cls(router, action_space=action_space)
|
| 169 |
+
|
| 170 |
+
def logits_processor(
|
| 171 |
+
self,
|
| 172 |
+
horizon: int,
|
| 173 |
+
*,
|
| 174 |
+
fps: float,
|
| 175 |
+
prompt_length: int,
|
| 176 |
+
eos_token_id: int,
|
| 177 |
+
token_offset: int = 0,
|
| 178 |
+
pad_token_id=None,
|
| 179 |
+
):
|
| 180 |
+
"""Create a Transformers LogitsProcessor for a fixed action horizon.
|
| 181 |
+
|
| 182 |
+
``prompt_length`` excludes leading model prompt IDs from the grammar;
|
| 183 |
+
``token_offset`` explicitly maps codec IDs to a contiguous model range.
|
| 184 |
+
EOS/padding IDs must be outside that range. No model vocabulary is changed.
|
| 185 |
+
"""
|
| 186 |
+
from .tokenization.generation import ActionCodec2LogitsProcessor
|
| 187 |
+
|
| 188 |
+
return ActionCodec2LogitsProcessor(
|
| 189 |
+
self,
|
| 190 |
+
horizon,
|
| 191 |
+
fps=fps,
|
| 192 |
+
prompt_length=prompt_length,
|
| 193 |
+
eos_token_id=eos_token_id,
|
| 194 |
+
token_offset=token_offset,
|
| 195 |
+
pad_token_id=pad_token_id,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def batch_decode(
|
| 199 |
+
self,
|
| 200 |
+
token_rows,
|
| 201 |
+
*,
|
| 202 |
+
fps=None,
|
| 203 |
+
current_state=None,
|
| 204 |
+
previous_first_order=None,
|
| 205 |
+
errors="raise",
|
| 206 |
+
):
|
| 207 |
+
"""Decode independent token rows, including different valid horizons.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
token_rows: Iterable of token-ID rows. Each must cover a complete
|
| 211 |
+
trajectory; intended generation horizon must be checked separately
|
| 212 |
+
with grammar.is_complete before decode.
|
| 213 |
+
fps: Output sampling rate, shared by all rows.
|
| 214 |
+
current_state: Optional (B,D) preceding states in original layout.
|
| 215 |
+
previous_first_order: Optional {component: (B,D_active)} boundaries.
|
| 216 |
+
errors: 'raise' stops at the first invalid row; 'return' returns the
|
| 217 |
+
ValueError at that position so other valid rows remain usable.
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
List of CPU float32 tensors shaped (1,T_i,D), or ValueError entries
|
| 221 |
+
when errors='return'. No truncated row is padded or executed partially.
|
| 222 |
+
"""
|
| 223 |
+
self._require_fitted()
|
| 224 |
+
if errors not in ("raise", "return"):
|
| 225 |
+
raise ValueError("errors must be 'raise' or 'return'")
|
| 226 |
+
rows = list(token_rows)
|
| 227 |
+
current = (
|
| 228 |
+
None
|
| 229 |
+
if current_state is None
|
| 230 |
+
else np.asarray(
|
| 231 |
+
current_state.detach().cpu()
|
| 232 |
+
if isinstance(current_state, torch.Tensor)
|
| 233 |
+
else current_state
|
| 234 |
+
)
|
| 235 |
+
)
|
| 236 |
+
if current is not None and current.shape != (len(rows), self.action_dim):
|
| 237 |
+
raise ValueError(
|
| 238 |
+
f"current_state must have shape {(len(rows), self.action_dim)}"
|
| 239 |
+
)
|
| 240 |
+
previous = self._numpy_boundary(previous_first_order)
|
| 241 |
+
if previous is not None and any(
|
| 242 |
+
np.ndim(v) != 2 or len(v) != len(rows) for v in previous.values()
|
| 243 |
+
):
|
| 244 |
+
raise ValueError(
|
| 245 |
+
"previous_first_order arrays must have batch dimension equal to token rows"
|
| 246 |
+
)
|
| 247 |
+
result = []
|
| 248 |
+
for index, row in enumerate(rows):
|
| 249 |
+
try:
|
| 250 |
+
decoded = self.decode(
|
| 251 |
+
[row],
|
| 252 |
+
fps=fps,
|
| 253 |
+
current_state=None
|
| 254 |
+
if current is None
|
| 255 |
+
else current[index : index + 1],
|
| 256 |
+
previous_first_order=None
|
| 257 |
+
if previous is None
|
| 258 |
+
else {k: v[index : index + 1] for k, v in previous.items()},
|
| 259 |
+
)
|
| 260 |
+
except ValueError as error:
|
| 261 |
+
error = ValueError(f"token row {index}: {error}")
|
| 262 |
+
if errors == "raise":
|
| 263 |
+
raise error from None
|
| 264 |
+
decoded = error
|
| 265 |
+
result.append(decoded)
|
| 266 |
+
return result
|
| 267 |
+
|
| 268 |
+
@property
|
| 269 |
+
def fitted_profiles(self) -> frozenset[str]:
|
| 270 |
+
"""Names of profiles fitted on data; persisted by save_pretrained()."""
|
| 271 |
+
return self._fitted_profiles
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def is_fitted(self) -> bool:
|
| 275 |
+
"""Whether every profile used by the selected action space is fitted."""
|
| 276 |
+
return all(
|
| 277 |
+
c.space in self.fitted_profiles for c in self.action_space.components
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
def _require_fitted(self):
|
| 281 |
+
missing = {c.space for c in self.action_space.components} - self.fitted_profiles
|
| 282 |
+
if missing:
|
| 283 |
+
raise RuntimeError(
|
| 284 |
+
f"Unfitted profiles {sorted(missing)}; call codec.fit(episodes) first"
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
def _describe_action_spaces(self, action_dim: int | None = None) -> str:
|
| 288 |
+
return _fitted_status(self.fitted_profiles) + super()._describe_action_spaces(
|
| 289 |
+
action_dim
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
def for_action_space(self, name: str) -> ActionCodec2:
|
| 293 |
+
"""Share current weights with a new binding; subsequent fit replaces only that instance."""
|
| 294 |
+
codec = super().for_action_space(name)
|
| 295 |
+
codec._fitted_profiles = self.fitted_profiles
|
| 296 |
+
codec.training_report = self.training_report
|
| 297 |
+
codec._source_directory = self._source_directory
|
| 298 |
+
return codec
|
| 299 |
+
|
| 300 |
+
def fit(
|
| 301 |
+
self,
|
| 302 |
+
episodes,
|
| 303 |
+
*,
|
| 304 |
+
fps: float | None = None,
|
| 305 |
+
current_state=None,
|
| 306 |
+
previous_first_order=None,
|
| 307 |
+
bootstrap_context: bool = False,
|
| 308 |
+
backend: str = "auto",
|
| 309 |
+
threads: int = 0,
|
| 310 |
+
batch_size: int = 256,
|
| 311 |
+
progress: bool = False,
|
| 312 |
+
max_cell_bytes: int | None = None,
|
| 313 |
+
work_dir: str | Path | None = None,
|
| 314 |
+
) -> ActionCodec2:
|
| 315 |
+
"""Fit physical vocabularies from actions in declared source layouts.
|
| 316 |
+
|
| 317 |
+
Args:
|
| 318 |
+
episodes: Array/tensor (T,D), (B,T,D), or iterable of variable-length
|
| 319 |
+
episodes. Records may contain ``actions``, ``action_space`` (a
|
| 320 |
+
registered name), ``fps``, ``current_state(B,D)``, and
|
| 321 |
+
``previous_first_order: {component: (B,D_active)}``. A record's
|
| 322 |
+
metadata must not duplicate the corresponding keyword argument.
|
| 323 |
+
fps: Source frequency override shared by all input episodes.
|
| 324 |
+
current_state: Preceding source state (B,D), required for absolute
|
| 325 |
+
inputs. Use per-record values when episodes have different states.
|
| 326 |
+
previous_first_order: Preceding canonical codec-rate increments by component,
|
| 327 |
+
with shape (B,D_active), required for second-order profiles.
|
| 328 |
+
bootstrap_context: Use two measured canonical frames as second-order
|
| 329 |
+
context, training on the remaining suffix. Mutually exclusive with
|
| 330 |
+
previous_first_order. Inference still requires explicit boundaries.
|
| 331 |
+
backend: Exact BPE implementation: 'auto', 'native', or 'python'.
|
| 332 |
+
threads: Native worker count; 0 uses the backend default.
|
| 333 |
+
batch_size: Maximum canonical trajectories per quantization batch.
|
| 334 |
+
progress: Display BPE training progress.
|
| 335 |
+
max_cell_bytes: Hard bound on retained quantized corpus bytes per profile.
|
| 336 |
+
work_dir: Parent scratch directory for sequential source staging files,
|
| 337 |
+
automatically removed on success or failure. Defaults to system temp.
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
This instance, with per-profile results in ``training_report``. Only
|
| 341 |
+
profiles present in the data are fitted; other weights are preserved.
|
| 342 |
+
Vocabulary replacement is atomic on success. Refit changes token IDs:
|
| 343 |
+
regenerate previously encoded datasets before using the new vocabulary.
|
| 344 |
+
"""
|
| 345 |
+
# Training is intentionally lazy: a loaded Hub artifact only needs the
|
| 346 |
+
# inference runtime. The full fitting pipeline remains available from
|
| 347 |
+
# the source package and is imported when the user calls fit().
|
| 348 |
+
from .training.fitting import ActionCodec2Fitter
|
| 349 |
+
|
| 350 |
+
router, report = ActionCodec2Fitter(
|
| 351 |
+
self, bootstrap_context=bootstrap_context
|
| 352 |
+
).fit(
|
| 353 |
+
episodes,
|
| 354 |
+
work_dir=work_dir,
|
| 355 |
+
metadata=dict(
|
| 356 |
+
fps=fps,
|
| 357 |
+
current_state=current_state,
|
| 358 |
+
previous_first_order=previous_first_order,
|
| 359 |
+
),
|
| 360 |
+
backend=backend,
|
| 361 |
+
threads=threads,
|
| 362 |
+
batch_size=batch_size,
|
| 363 |
+
progress=progress,
|
| 364 |
+
max_cell_bytes=max_cell_bytes,
|
| 365 |
+
)
|
| 366 |
+
fitted = self.fitted_profiles | report["profiles"].keys()
|
| 367 |
+
replacement = type(self)(router, action_space=self.action_space.name)
|
| 368 |
+
replacement._fitted_profiles = frozenset(fitted)
|
| 369 |
+
replacement.training_report = report
|
| 370 |
+
replacement._source_directory = self._source_directory
|
| 371 |
+
replacement._warned_default_fps = self._warned_default_fps
|
| 372 |
+
self.__dict__.update(replacement.__dict__)
|
| 373 |
+
return self
|
| 374 |
+
|
| 375 |
+
def _encode_action_indices(self, action, encode_kwargs=None):
|
| 376 |
+
self._require_fitted()
|
| 377 |
+
return super()._encode_action_indices(action, encode_kwargs)
|
| 378 |
+
|
| 379 |
+
def _decode_action_indices(self, action_indices, **kwargs):
|
| 380 |
+
self._require_fitted()
|
| 381 |
+
return super()._decode_action_indices(action_indices, **kwargs)
|
| 382 |
+
|
| 383 |
+
def grammar(self, horizon: int, *, fps: float | None = None):
|
| 384 |
+
self._require_fitted()
|
| 385 |
+
return super().grammar(horizon, fps=fps)
|
| 386 |
+
|
| 387 |
+
def final_first_order(
|
| 388 |
+
self, action_token_ids, *, previous_first_order, executed_steps=None
|
| 389 |
+
):
|
| 390 |
+
self._require_fitted()
|
| 391 |
+
return super().final_first_order(
|
| 392 |
+
action_token_ids,
|
| 393 |
+
previous_first_order=previous_first_order,
|
| 394 |
+
executed_steps=executed_steps,
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
@classmethod
|
| 398 |
+
def describe_pretrained(
|
| 399 |
+
cls,
|
| 400 |
+
pretrained_model_name_or_path: str | Path,
|
| 401 |
+
*,
|
| 402 |
+
action_dim: int | None = None,
|
| 403 |
+
**kwargs: Any,
|
| 404 |
+
) -> str:
|
| 405 |
+
"""Inspect a local/Hub artifact before choosing an action space.
|
| 406 |
+
|
| 407 |
+
Args:
|
| 408 |
+
pretrained_model_name_or_path: Saved processor/router directory or
|
| 409 |
+
Hugging Face Hub repository ID. Loads its fitted router for inspection.
|
| 410 |
+
action_dim: Optional D from ``actions.shape[-1]``. Only filters the
|
| 411 |
+
report; tensor shape cannot establish physical semantics.
|
| 412 |
+
**kwargs: Hub options such as ``revision``, ``subfolder``,
|
| 413 |
+
``cache_dir``, ``token``, and ``local_files_only``.
|
| 414 |
+
|
| 415 |
+
Returns:
|
| 416 |
+
Human-readable layout guide, suitable for ``print(...)``. No binding
|
| 417 |
+
is required or inferred, and no action data is encoded.
|
| 418 |
+
"""
|
| 419 |
+
directory = resolve_pretrained_directory(
|
| 420 |
+
pretrained_model_name_or_path,
|
| 421 |
+
allow_patterns=ROUTER_ALLOW_PATTERNS,
|
| 422 |
+
**kwargs,
|
| 423 |
+
)
|
| 424 |
+
router = ActionCodec2Router.from_pretrained(directory)
|
| 425 |
+
metadata = _saved_metadata(directory)
|
| 426 |
+
return _fitted_status(
|
| 427 |
+
metadata.get("fitted_profiles", router.profiles)
|
| 428 |
+
) + router.describe_action_spaces(
|
| 429 |
+
selected=metadata.get("action_space"), action_dim=action_dim
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
@classmethod
|
| 433 |
+
def from_pretrained(
|
| 434 |
+
cls,
|
| 435 |
+
pretrained_model_name_or_path: str | Path,
|
| 436 |
+
*,
|
| 437 |
+
action_space: str | Path | PresetConfig | Mapping[str, object] | None = None,
|
| 438 |
+
**kwargs: Any,
|
| 439 |
+
) -> ActionCodec2:
|
| 440 |
+
"""Load a local/Hub artifact and restore or override its action space.
|
| 441 |
+
|
| 442 |
+
Args:
|
| 443 |
+
pretrained_model_name_or_path: Saved processor or router directory,
|
| 444 |
+
or a Hugging Face Hub repository ID.
|
| 445 |
+
action_space: Registered name, preset YAML path, config, or mapping.
|
| 446 |
+
Required for an unbound router; otherwise defaults to the saved
|
| 447 |
+
binding. Custom definitions are saved in the router manifest.
|
| 448 |
+
**kwargs: Hub options including ``revision``, ``subfolder``,
|
| 449 |
+
``cache_dir``, ``token``, and ``local_files_only``.
|
| 450 |
+
"""
|
| 451 |
+
directory = resolve_pretrained_directory(
|
| 452 |
+
pretrained_model_name_or_path,
|
| 453 |
+
allow_patterns=ROUTER_ALLOW_PATTERNS,
|
| 454 |
+
**kwargs,
|
| 455 |
+
)
|
| 456 |
+
if action_space is None:
|
| 457 |
+
action_space = _saved_metadata(directory).get("action_space")
|
| 458 |
+
if action_space is None:
|
| 459 |
+
raise ValueError(
|
| 460 |
+
"action_space is required when loading an unbound router artifact. "
|
| 461 |
+
"First inspect its layouts with print(ActionCodec2.describe_pretrained(path)), "
|
| 462 |
+
"then pass action_space='NAME'; shape alone cannot identify physical meaning."
|
| 463 |
+
)
|
| 464 |
+
codec = super().from_pretrained(directory, action_space=action_space)
|
| 465 |
+
metadata = _saved_metadata(directory)
|
| 466 |
+
fitted = metadata.get("fitted_profiles", list(codec.action_tokenizer.profiles))
|
| 467 |
+
if not isinstance(fitted, list) or any(
|
| 468 |
+
not isinstance(name, str) or name not in codec.action_tokenizer.profiles
|
| 469 |
+
for name in fitted
|
| 470 |
+
):
|
| 471 |
+
raise ValueError("invalid fitted_profiles in processor_config.json")
|
| 472 |
+
codec._fitted_profiles = frozenset(fitted)
|
| 473 |
+
codec._source_directory = directory
|
| 474 |
+
report_path = directory / "fit_report.json"
|
| 475 |
+
if report_path.is_file():
|
| 476 |
+
codec.training_report = json.loads(report_path.read_text(encoding="utf-8"))
|
| 477 |
+
return codec
|
| 478 |
+
|
| 479 |
+
def to_dict(self, legacy_serialization: bool = True) -> dict[str, object]:
|
| 480 |
+
"""Return lightweight discovery metadata.
|
| 481 |
+
|
| 482 |
+
``legacy_serialization`` is accepted for ProcessorMixin signature
|
| 483 |
+
compatibility. ActionCodec2 has one serialization format for both values.
|
| 484 |
+
"""
|
| 485 |
+
del legacy_serialization
|
| 486 |
+
return {
|
| 487 |
+
"processor_class": type(self).__name__,
|
| 488 |
+
"model_type": self.model_type,
|
| 489 |
+
"action_space": self.action_space.name,
|
| 490 |
+
"fitted_profiles": sorted(self.fitted_profiles),
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
def save_pretrained(
|
| 494 |
+
self, save_directory: str | Path, push_to_hub: bool = False, **kwargs: Any
|
| 495 |
+
) -> list[str]:
|
| 496 |
+
"""Save the complete router and the selected action-space binding."""
|
| 497 |
+
if not push_to_hub and kwargs:
|
| 498 |
+
raise TypeError(
|
| 499 |
+
f"unused save_pretrained keyword(s): {', '.join(sorted(kwargs))}"
|
| 500 |
+
)
|
| 501 |
+
from .integration.exporting import ArtifactExporter
|
| 502 |
+
|
| 503 |
+
directory = Path(save_directory)
|
| 504 |
+
save_model_card(directory, str(self))
|
| 505 |
+
files = self.action_tokenizer.save_pretrained(directory, include_code=False)
|
| 506 |
+
files.extend(
|
| 507 |
+
ArtifactExporter(
|
| 508 |
+
directory, source_directory=self._source_directory
|
| 509 |
+
).export()
|
| 510 |
+
)
|
| 511 |
+
save_huggingface_metadata(
|
| 512 |
+
directory,
|
| 513 |
+
**self.to_dict(),
|
| 514 |
+
auto_map={"AutoProcessor": "processing_actioncodec2.ActionCodec2"},
|
| 515 |
+
)
|
| 516 |
+
report_path = directory / "fit_report.json"
|
| 517 |
+
if self.training_report is not None:
|
| 518 |
+
report_path.write_text(
|
| 519 |
+
json.dumps(self.training_report, indent=2) + "\n", encoding="utf-8"
|
| 520 |
+
)
|
| 521 |
+
files.append(str(report_path))
|
| 522 |
+
elif report_path.exists():
|
| 523 |
+
report_path.unlink()
|
| 524 |
+
if push_to_hub:
|
| 525 |
+
upload_pretrained_directory(directory, **kwargs)
|
| 526 |
+
return files
|
| 527 |
+
|
| 528 |
+
def __call__(
|
| 529 |
+
self,
|
| 530 |
+
action: torch.Tensor | np.ndarray,
|
| 531 |
+
*,
|
| 532 |
+
fps: float | None = None,
|
| 533 |
+
current_state: torch.Tensor | np.ndarray | None = None,
|
| 534 |
+
previous_first_order: Mapping[str, torch.Tensor | np.ndarray] | None = None,
|
| 535 |
+
encode_kwargs: dict | None = None,
|
| 536 |
+
return_tensors: str | None = None,
|
| 537 |
+
) -> BatchFeature:
|
| 538 |
+
"""Prepare ``input_ids`` from ``(B,T,D)`` or a single ``(T,D)`` action.
|
| 539 |
+
|
| 540 |
+
``current_state(B,D)`` is required for absolute actions. Second-order
|
| 541 |
+
artifacts require ``previous_first_order`` mapping component names to
|
| 542 |
+
``(B,D_active)`` canonical increments, as in :meth:`encode`.
|
| 543 |
+
``return_tensors`` may be ``"pt"`` or ``"np"`` for equal-length rows;
|
| 544 |
+
omitted, rows remain ragged lists without adding padding tokens.
|
| 545 |
+
"""
|
| 546 |
+
rows = self.encode(
|
| 547 |
+
action,
|
| 548 |
+
fps=fps,
|
| 549 |
+
current_state=current_state,
|
| 550 |
+
previous_first_order=previous_first_order,
|
| 551 |
+
encode_kwargs=encode_kwargs,
|
| 552 |
+
)
|
| 553 |
+
return BatchFeature({"input_ids": rows}, tensor_type=return_tensors)
|
runtime/routing/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Internal routing components for ActionCodec2."""
|
runtime/routing/cache.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small per-instance LRU caches; no global references to fitted codecs."""
|
| 2 |
+
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
from collections.abc import MutableMapping
|
| 5 |
+
from threading import RLock
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class BoundedCache(MutableMapping):
|
| 9 |
+
"""Evict least-recently-used entries above a fixed capacity.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
capacity: Maximum retained entries (positive integer).
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, capacity):
|
| 16 |
+
if type(capacity) is not int or capacity < 1:
|
| 17 |
+
raise ValueError("cache capacity must be a positive integer")
|
| 18 |
+
self.capacity = capacity
|
| 19 |
+
self._items = OrderedDict()
|
| 20 |
+
self._lock = RLock()
|
| 21 |
+
|
| 22 |
+
def __getitem__(self, key):
|
| 23 |
+
with self._lock:
|
| 24 |
+
value = self._items[key]
|
| 25 |
+
self._items.move_to_end(key)
|
| 26 |
+
return value
|
| 27 |
+
|
| 28 |
+
def __setitem__(self, key, value):
|
| 29 |
+
with self._lock:
|
| 30 |
+
self._items[key] = value
|
| 31 |
+
self._items.move_to_end(key)
|
| 32 |
+
while len(self._items) > self.capacity:
|
| 33 |
+
self._items.popitem(last=False)
|
| 34 |
+
|
| 35 |
+
def __delitem__(self, key):
|
| 36 |
+
with self._lock:
|
| 37 |
+
del self._items[key]
|
| 38 |
+
|
| 39 |
+
def __iter__(self):
|
| 40 |
+
with self._lock:
|
| 41 |
+
return iter(tuple(self._items))
|
| 42 |
+
|
| 43 |
+
def __len__(self):
|
| 44 |
+
with self._lock:
|
| 45 |
+
return len(self._items)
|
runtime/routing/defaults.py
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Package-owned configuration templates for fresh ActionCodec2 instances."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
|
| 7 |
+
from ..tokenization.bpe import BPESerializer, SetBPESerializer
|
| 8 |
+
from ..physical.configuration import ActionCodec2Config
|
| 9 |
+
from ..physical.physical_processor import ActionCodec2Processor
|
| 10 |
+
from .presets import PresetConfig
|
| 11 |
+
from .routing import ActionCodec2Router
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class CodecBuilder:
|
| 15 |
+
"""Build unfitted physical profiles from packaged defaults or explicit configs.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
primitive_order: First- or second-order default physical primitives.
|
| 19 |
+
bpe_scheme: Optional scheme override for both physical profiles.
|
| 20 |
+
profile_configs: Optional joint/EEF config objects, mappings, or YAML paths.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, *, primitive_order=1, bpe_scheme=None, profile_configs=None):
|
| 24 |
+
if type(primitive_order) is not int or primitive_order not in (1, 2):
|
| 25 |
+
raise ValueError("primitive_order must be 1 or 2")
|
| 26 |
+
if bpe_scheme not in (None, "set_bpe", "bpe"):
|
| 27 |
+
raise ValueError("bpe_scheme must be 'set_bpe' or 'bpe'")
|
| 28 |
+
if profile_configs is not None and not isinstance(profile_configs, Mapping):
|
| 29 |
+
raise TypeError(
|
| 30 |
+
"profile_configs must map 'joint'/'eef' to physical configurations"
|
| 31 |
+
)
|
| 32 |
+
self.profile_configs = dict(profile_configs or {})
|
| 33 |
+
if set(self.profile_configs) - {"joint", "eef"}:
|
| 34 |
+
raise ValueError("profile_configs keys must be 'joint' or 'eef'")
|
| 35 |
+
self.primitive_order = primitive_order
|
| 36 |
+
self.bpe_scheme = bpe_scheme
|
| 37 |
+
|
| 38 |
+
def _config(self, name: str) -> ActionCodec2Config:
|
| 39 |
+
suffix = "" if self.primitive_order == 1 else "_second_order_pretrain_v2"
|
| 40 |
+
raw = self.profile_configs.get(name, _PROFILE_DEFAULTS[f"{name}{suffix}"])
|
| 41 |
+
if isinstance(raw, ActionCodec2Config):
|
| 42 |
+
config = raw
|
| 43 |
+
elif isinstance(raw, Mapping):
|
| 44 |
+
config = ActionCodec2Config.from_dict(raw)
|
| 45 |
+
else:
|
| 46 |
+
config = ActionCodec2Config.from_yaml(raw)
|
| 47 |
+
if config.train is None:
|
| 48 |
+
raise ValueError(f"{name} profile config requires a train section")
|
| 49 |
+
if self.bpe_scheme is not None:
|
| 50 |
+
payload = config.to_dict()
|
| 51 |
+
payload["set_bpe"]["scheme"] = self.bpe_scheme
|
| 52 |
+
if self.bpe_scheme == "bpe":
|
| 53 |
+
payload["train"]["bpe"]["spatial_vocab_size"] = payload["train"]["bpe"][
|
| 54 |
+
"vocab_size"
|
| 55 |
+
]
|
| 56 |
+
config = ActionCodec2Config.from_dict(payload)
|
| 57 |
+
return config
|
| 58 |
+
|
| 59 |
+
def build(self) -> ActionCodec2Router:
|
| 60 |
+
"""Create primitive-only profiles; the public facade tracks fitted state."""
|
| 61 |
+
processors = {}
|
| 62 |
+
budgets = {}
|
| 63 |
+
for name in ("joint", "eef"):
|
| 64 |
+
config = self._config(name)
|
| 65 |
+
# Keep training-only modules out of the inference artifact. They
|
| 66 |
+
# are imported only when a fresh codec is constructed for fitting.
|
| 67 |
+
from ..training.training import build_primitives
|
| 68 |
+
|
| 69 |
+
primitives = build_primitives(config)
|
| 70 |
+
if config.set_bpe.scheme == "bpe":
|
| 71 |
+
serializer = BPESerializer(
|
| 72 |
+
bins=primitives.bins, lookahead=config.set_bpe.lookahead, merges=()
|
| 73 |
+
)
|
| 74 |
+
else:
|
| 75 |
+
serializer = SetBPESerializer(
|
| 76 |
+
bins=primitives.bins,
|
| 77 |
+
lookahead=config.set_bpe.lookahead,
|
| 78 |
+
spatial_merges=(),
|
| 79 |
+
temporal_merges=(),
|
| 80 |
+
)
|
| 81 |
+
processors[name] = ActionCodec2Processor(config, primitives, serializer)
|
| 82 |
+
budgets[name] = config.train.bpe.vocab_size
|
| 83 |
+
presets = list(builtin_action_spaces().values())
|
| 84 |
+
return ActionCodec2Router.from_processors(
|
| 85 |
+
processors, token_budgets=budgets, presets=presets
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def builtin_action_spaces():
|
| 90 |
+
"""Return a read-only name-to-preset mapping shipped in the installed package."""
|
| 91 |
+
from types import MappingProxyType
|
| 92 |
+
|
| 93 |
+
presets = [PresetConfig.from_dict(value) for value in _PRESET_DEFAULTS.values()]
|
| 94 |
+
return MappingProxyType({preset.name: preset for preset in presets})
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class ProfileBundleBuilder:
|
| 98 |
+
"""Assemble two existing physical artifacts using their recorded vocabulary budgets.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
joint: Local directory containing the fitted joint profile.
|
| 102 |
+
eef: Local directory containing the fitted EEF profile.
|
| 103 |
+
token_budgets: Optional explicit joint/EEF budget overrides. Defaults to
|
| 104 |
+
recorded training budgets, or the fitted vocabulary size if absent.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(self, joint, eef, *, token_budgets=None):
|
| 108 |
+
self.paths = {"joint": joint, "eef": eef}
|
| 109 |
+
self.budgets = dict(token_budgets or {})
|
| 110 |
+
if self.budgets.keys() - self.paths.keys():
|
| 111 |
+
raise ValueError("token_budgets accepts only 'joint' and 'eef'")
|
| 112 |
+
|
| 113 |
+
def build(self):
|
| 114 |
+
processors = {
|
| 115 |
+
name: ActionCodec2Processor.from_pretrained(path)
|
| 116 |
+
for name, path in self.paths.items()
|
| 117 |
+
}
|
| 118 |
+
budgets = {
|
| 119 |
+
name: self.budgets.get(
|
| 120 |
+
name,
|
| 121 |
+
processor.config.train.bpe.vocab_size
|
| 122 |
+
if processor.config.train is not None
|
| 123 |
+
else processor.vocab_size,
|
| 124 |
+
)
|
| 125 |
+
for name, processor in processors.items()
|
| 126 |
+
}
|
| 127 |
+
return ActionCodec2Router.from_processors(
|
| 128 |
+
processors,
|
| 129 |
+
token_budgets=budgets,
|
| 130 |
+
presets=list(builtin_action_spaces().values()),
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
_PROFILE_DEFAULTS = {
|
| 135 |
+
"eef": {
|
| 136 |
+
"model_type": "actioncodec2_unified",
|
| 137 |
+
"config_version": 1,
|
| 138 |
+
"physical": {
|
| 139 |
+
"input_dim": 7,
|
| 140 |
+
"components": {
|
| 141 |
+
"translation_m": {
|
| 142 |
+
"dims": {"start": 0, "stop": 3},
|
| 143 |
+
"space": "additive",
|
| 144 |
+
"input_order": 0,
|
| 145 |
+
"primitive_order": 1,
|
| 146 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 147 |
+
"constraints": {
|
| 148 |
+
"order_0": {"min": -2.0, "max": 2.0},
|
| 149 |
+
"order_1": {"min": -0.08, "max": 0.08},
|
| 150 |
+
},
|
| 151 |
+
"distortion_limit": 0.005,
|
| 152 |
+
"primitive_selection": "smooth",
|
| 153 |
+
},
|
| 154 |
+
"orientation_so3": {
|
| 155 |
+
"dims": {"start": 3, "stop": 6},
|
| 156 |
+
"space": "so3",
|
| 157 |
+
"input_order": 0,
|
| 158 |
+
"primitive_order": 1,
|
| 159 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 160 |
+
"constraints": {
|
| 161 |
+
"order_0": {
|
| 162 |
+
"min": -3.141592653589793,
|
| 163 |
+
"max": 3.141592653589793,
|
| 164 |
+
},
|
| 165 |
+
"order_1": {"min": -0.25, "max": 0.25},
|
| 166 |
+
},
|
| 167 |
+
"distortion_limit": 0.02,
|
| 168 |
+
"primitive_selection": "smooth",
|
| 169 |
+
"representation": "rotation_vector",
|
| 170 |
+
"multiplication": "right",
|
| 171 |
+
},
|
| 172 |
+
"gripper_open_command": {
|
| 173 |
+
"dims": [6],
|
| 174 |
+
"space": "additive",
|
| 175 |
+
"input_order": 0,
|
| 176 |
+
"primitive_order": 0,
|
| 177 |
+
"input_transform": {"scale": 1.0, "clip": True},
|
| 178 |
+
"constraints": {"order_0": {"min": 0.0, "max": 1.0}},
|
| 179 |
+
"distortion_limit": 0.5,
|
| 180 |
+
"primitive_selection": "nearest",
|
| 181 |
+
},
|
| 182 |
+
},
|
| 183 |
+
},
|
| 184 |
+
"set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
|
| 185 |
+
"train": {
|
| 186 |
+
"primitives": {
|
| 187 |
+
"zero_primitive_components": ["translation_m", "orientation_so3"]
|
| 188 |
+
},
|
| 189 |
+
"bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
|
| 190 |
+
"quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
|
| 191 |
+
},
|
| 192 |
+
},
|
| 193 |
+
"eef_second_order_pretrain_v2": {
|
| 194 |
+
"model_type": "actioncodec2_unified",
|
| 195 |
+
"config_version": 1,
|
| 196 |
+
"physical": {
|
| 197 |
+
"input_dim": 7,
|
| 198 |
+
"components": {
|
| 199 |
+
"translation_m": {
|
| 200 |
+
"dims": {"start": 0, "stop": 3},
|
| 201 |
+
"space": "additive",
|
| 202 |
+
"input_order": 0,
|
| 203 |
+
"primitive_order": 2,
|
| 204 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 205 |
+
"constraints": {
|
| 206 |
+
"order_0": {"min": -2.0, "max": 2.0},
|
| 207 |
+
"order_2": {"min": -0.025, "max": 0.025},
|
| 208 |
+
"order_1": {"min": -0.08, "max": 0.08},
|
| 209 |
+
},
|
| 210 |
+
"distortion_limit": 0.005,
|
| 211 |
+
"primitive_selection": "smooth",
|
| 212 |
+
},
|
| 213 |
+
"orientation_so3": {
|
| 214 |
+
"dims": {"start": 3, "stop": 6},
|
| 215 |
+
"space": "so3",
|
| 216 |
+
"input_order": 0,
|
| 217 |
+
"primitive_order": 2,
|
| 218 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 219 |
+
"constraints": {
|
| 220 |
+
"order_0": {
|
| 221 |
+
"min": -3.141592653589793,
|
| 222 |
+
"max": 3.141592653589793,
|
| 223 |
+
},
|
| 224 |
+
"order_2": {"min": -0.08, "max": 0.08},
|
| 225 |
+
"order_1": {"min": -0.25, "max": 0.25},
|
| 226 |
+
},
|
| 227 |
+
"distortion_limit": 0.02,
|
| 228 |
+
"primitive_selection": "smooth",
|
| 229 |
+
"representation": "rotation_vector",
|
| 230 |
+
"multiplication": "right",
|
| 231 |
+
},
|
| 232 |
+
"gripper_open_command": {
|
| 233 |
+
"dims": [6],
|
| 234 |
+
"space": "additive",
|
| 235 |
+
"input_order": 0,
|
| 236 |
+
"primitive_order": 0,
|
| 237 |
+
"input_transform": {"scale": 1.0, "clip": True},
|
| 238 |
+
"constraints": {"order_0": {"min": 0.0, "max": 1.0}},
|
| 239 |
+
"distortion_limit": 0.5,
|
| 240 |
+
"primitive_selection": "nearest",
|
| 241 |
+
},
|
| 242 |
+
},
|
| 243 |
+
},
|
| 244 |
+
"set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
|
| 245 |
+
"train": {
|
| 246 |
+
"primitives": {
|
| 247 |
+
"zero_primitive_components": ["translation_m", "orientation_so3"]
|
| 248 |
+
},
|
| 249 |
+
"bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
|
| 250 |
+
"quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
|
| 251 |
+
},
|
| 252 |
+
},
|
| 253 |
+
"joint": {
|
| 254 |
+
"model_type": "actioncodec2_unified",
|
| 255 |
+
"config_version": 1,
|
| 256 |
+
"physical": {
|
| 257 |
+
"input_dim": 8,
|
| 258 |
+
"components": {
|
| 259 |
+
"generalized_joint_position": {
|
| 260 |
+
"dims": {"start": 0, "stop": 7},
|
| 261 |
+
"space": "additive",
|
| 262 |
+
"input_order": 0,
|
| 263 |
+
"primitive_order": 1,
|
| 264 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 265 |
+
"constraints": {
|
| 266 |
+
"order_0": {
|
| 267 |
+
"min": -6.283185307179586,
|
| 268 |
+
"max": 6.283185307179586,
|
| 269 |
+
},
|
| 270 |
+
"order_1": {"min": -0.35, "max": 0.35},
|
| 271 |
+
},
|
| 272 |
+
"distortion_limit": [
|
| 273 |
+
0.004,
|
| 274 |
+
0.004,
|
| 275 |
+
0.006,
|
| 276 |
+
0.0075,
|
| 277 |
+
0.015,
|
| 278 |
+
0.02,
|
| 279 |
+
0.02,
|
| 280 |
+
],
|
| 281 |
+
"primitive_selection": "smooth",
|
| 282 |
+
},
|
| 283 |
+
"gripper_open_command": {
|
| 284 |
+
"dims": [7],
|
| 285 |
+
"space": "additive",
|
| 286 |
+
"input_order": 0,
|
| 287 |
+
"primitive_order": 0,
|
| 288 |
+
"input_transform": {"scale": 1.0, "clip": True},
|
| 289 |
+
"constraints": {"order_0": {"min": 0.0, "max": 1.0}},
|
| 290 |
+
"distortion_limit": 0.5,
|
| 291 |
+
"primitive_selection": "nearest",
|
| 292 |
+
},
|
| 293 |
+
},
|
| 294 |
+
},
|
| 295 |
+
"set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
|
| 296 |
+
"train": {
|
| 297 |
+
"primitives": {"zero_primitive_components": ["generalized_joint_position"]},
|
| 298 |
+
"bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
|
| 299 |
+
"quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
|
| 300 |
+
},
|
| 301 |
+
},
|
| 302 |
+
"joint_second_order_pretrain_v2": {
|
| 303 |
+
"model_type": "actioncodec2_unified",
|
| 304 |
+
"config_version": 1,
|
| 305 |
+
"physical": {
|
| 306 |
+
"input_dim": 8,
|
| 307 |
+
"components": {
|
| 308 |
+
"generalized_joint_position": {
|
| 309 |
+
"dims": {"start": 0, "stop": 7},
|
| 310 |
+
"space": "additive",
|
| 311 |
+
"input_order": 0,
|
| 312 |
+
"primitive_order": 2,
|
| 313 |
+
"input_transform": {"scale": 1.0, "clip": False},
|
| 314 |
+
"constraints": {
|
| 315 |
+
"order_0": {
|
| 316 |
+
"min": -6.283185307179586,
|
| 317 |
+
"max": 6.283185307179586,
|
| 318 |
+
},
|
| 319 |
+
"order_2": {"min": -0.25, "max": 0.25},
|
| 320 |
+
"order_1": {"min": -0.35, "max": 0.35},
|
| 321 |
+
},
|
| 322 |
+
"distortion_limit": [
|
| 323 |
+
0.004,
|
| 324 |
+
0.004,
|
| 325 |
+
0.006,
|
| 326 |
+
0.0075,
|
| 327 |
+
0.015,
|
| 328 |
+
0.02,
|
| 329 |
+
0.02,
|
| 330 |
+
],
|
| 331 |
+
"primitive_selection": "smooth",
|
| 332 |
+
},
|
| 333 |
+
"gripper_open_command": {
|
| 334 |
+
"dims": [7],
|
| 335 |
+
"space": "additive",
|
| 336 |
+
"input_order": 0,
|
| 337 |
+
"primitive_order": 0,
|
| 338 |
+
"input_transform": {"scale": 1.0, "clip": True},
|
| 339 |
+
"constraints": {"order_0": {"min": 0.0, "max": 1.0}},
|
| 340 |
+
"distortion_limit": 0.5,
|
| 341 |
+
"primitive_selection": "nearest",
|
| 342 |
+
},
|
| 343 |
+
},
|
| 344 |
+
},
|
| 345 |
+
"set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
|
| 346 |
+
"train": {
|
| 347 |
+
"primitives": {"zero_primitive_components": ["generalized_joint_position"]},
|
| 348 |
+
"bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
|
| 349 |
+
"quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
|
| 350 |
+
},
|
| 351 |
+
},
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
_PRESET_DEFAULTS = {
|
| 355 |
+
"dual_eef_abs": {
|
| 356 |
+
"name": "dual_eef_abs",
|
| 357 |
+
"input_dim": 14,
|
| 358 |
+
"input": "absolute",
|
| 359 |
+
"source_hz": 15.0,
|
| 360 |
+
"target_hz": 15.0,
|
| 361 |
+
"interpolation": "cubic",
|
| 362 |
+
"components": {
|
| 363 |
+
"left_arm": {
|
| 364 |
+
"space": "eef",
|
| 365 |
+
"position_dims": [0, 1, 2],
|
| 366 |
+
"rotation_dims": [3, 4, 5],
|
| 367 |
+
"rotation_format": "rotation_vector",
|
| 368 |
+
"gripper_dim": 6,
|
| 369 |
+
"gripper_threshold": 0.8,
|
| 370 |
+
"gripper_open_is_high": True,
|
| 371 |
+
},
|
| 372 |
+
"right_arm": {
|
| 373 |
+
"space": "eef",
|
| 374 |
+
"position_dims": [7, 8, 9],
|
| 375 |
+
"rotation_dims": [10, 11, 12],
|
| 376 |
+
"rotation_format": "rotation_vector",
|
| 377 |
+
"gripper_dim": 13,
|
| 378 |
+
"gripper_threshold": 0.8,
|
| 379 |
+
"gripper_open_is_high": True,
|
| 380 |
+
},
|
| 381 |
+
},
|
| 382 |
+
},
|
| 383 |
+
"dual_eef_delta": {
|
| 384 |
+
"name": "dual_eef_delta",
|
| 385 |
+
"input_dim": 14,
|
| 386 |
+
"input": "delta",
|
| 387 |
+
"source_hz": 15.0,
|
| 388 |
+
"target_hz": 15.0,
|
| 389 |
+
"interpolation": "cubic",
|
| 390 |
+
"components": {
|
| 391 |
+
"left_arm": {
|
| 392 |
+
"space": "eef",
|
| 393 |
+
"position_dims": [0, 1, 2],
|
| 394 |
+
"rotation_dims": [3, 4, 5],
|
| 395 |
+
"rotation_format": "rotation_vector",
|
| 396 |
+
"gripper_dim": 6,
|
| 397 |
+
"gripper_threshold": 0.8,
|
| 398 |
+
"gripper_open_is_high": True,
|
| 399 |
+
},
|
| 400 |
+
"right_arm": {
|
| 401 |
+
"space": "eef",
|
| 402 |
+
"position_dims": [7, 8, 9],
|
| 403 |
+
"rotation_dims": [10, 11, 12],
|
| 404 |
+
"rotation_format": "rotation_vector",
|
| 405 |
+
"gripper_dim": 13,
|
| 406 |
+
"gripper_threshold": 0.8,
|
| 407 |
+
"gripper_open_is_high": True,
|
| 408 |
+
},
|
| 409 |
+
},
|
| 410 |
+
},
|
| 411 |
+
"dual_joint6_abs": {
|
| 412 |
+
"name": "dual_joint6_abs",
|
| 413 |
+
"input_dim": 14,
|
| 414 |
+
"input": "absolute",
|
| 415 |
+
"source_hz": 15.0,
|
| 416 |
+
"target_hz": 15.0,
|
| 417 |
+
"interpolation": "cubic",
|
| 418 |
+
"components": {
|
| 419 |
+
"left_arm": {
|
| 420 |
+
"space": "joint",
|
| 421 |
+
"joint_dims": [0, 1, 2, 3, 4, 5],
|
| 422 |
+
"gripper_dim": 6,
|
| 423 |
+
"gripper_threshold": 0.8,
|
| 424 |
+
"gripper_open_is_high": True,
|
| 425 |
+
},
|
| 426 |
+
"right_arm": {
|
| 427 |
+
"space": "joint",
|
| 428 |
+
"joint_dims": [7, 8, 9, 10, 11, 12],
|
| 429 |
+
"gripper_dim": 13,
|
| 430 |
+
"gripper_threshold": 0.8,
|
| 431 |
+
"gripper_open_is_high": True,
|
| 432 |
+
},
|
| 433 |
+
},
|
| 434 |
+
},
|
| 435 |
+
"dual_joint6_delta": {
|
| 436 |
+
"name": "dual_joint6_delta",
|
| 437 |
+
"input_dim": 14,
|
| 438 |
+
"input": "delta",
|
| 439 |
+
"source_hz": 15.0,
|
| 440 |
+
"target_hz": 15.0,
|
| 441 |
+
"interpolation": "cubic",
|
| 442 |
+
"components": {
|
| 443 |
+
"left_arm": {
|
| 444 |
+
"space": "joint",
|
| 445 |
+
"joint_dims": [0, 1, 2, 3, 4, 5],
|
| 446 |
+
"gripper_dim": 6,
|
| 447 |
+
"gripper_threshold": 0.8,
|
| 448 |
+
"gripper_open_is_high": True,
|
| 449 |
+
},
|
| 450 |
+
"right_arm": {
|
| 451 |
+
"space": "joint",
|
| 452 |
+
"joint_dims": [7, 8, 9, 10, 11, 12],
|
| 453 |
+
"gripper_dim": 13,
|
| 454 |
+
"gripper_threshold": 0.8,
|
| 455 |
+
"gripper_open_is_high": True,
|
| 456 |
+
},
|
| 457 |
+
},
|
| 458 |
+
},
|
| 459 |
+
"dual_joint7_abs": {
|
| 460 |
+
"name": "dual_joint7_abs",
|
| 461 |
+
"input_dim": 16,
|
| 462 |
+
"input": "absolute",
|
| 463 |
+
"source_hz": 15.0,
|
| 464 |
+
"target_hz": 15.0,
|
| 465 |
+
"interpolation": "cubic",
|
| 466 |
+
"components": {
|
| 467 |
+
"left_arm": {
|
| 468 |
+
"space": "joint",
|
| 469 |
+
"joint_dims": [0, 1, 2, 3, 4, 5, 6],
|
| 470 |
+
"gripper_dim": 7,
|
| 471 |
+
"gripper_threshold": 0.8,
|
| 472 |
+
"gripper_open_is_high": True,
|
| 473 |
+
},
|
| 474 |
+
"right_arm": {
|
| 475 |
+
"space": "joint",
|
| 476 |
+
"joint_dims": [8, 9, 10, 11, 12, 13, 14],
|
| 477 |
+
"gripper_dim": 15,
|
| 478 |
+
"gripper_threshold": 0.8,
|
| 479 |
+
"gripper_open_is_high": True,
|
| 480 |
+
},
|
| 481 |
+
},
|
| 482 |
+
},
|
| 483 |
+
"dual_joint7_delta": {
|
| 484 |
+
"name": "dual_joint7_delta",
|
| 485 |
+
"input_dim": 16,
|
| 486 |
+
"input": "delta",
|
| 487 |
+
"source_hz": 15.0,
|
| 488 |
+
"target_hz": 15.0,
|
| 489 |
+
"interpolation": "cubic",
|
| 490 |
+
"components": {
|
| 491 |
+
"left_arm": {
|
| 492 |
+
"space": "joint",
|
| 493 |
+
"joint_dims": [0, 1, 2, 3, 4, 5, 6],
|
| 494 |
+
"gripper_dim": 7,
|
| 495 |
+
"gripper_threshold": 0.8,
|
| 496 |
+
"gripper_open_is_high": True,
|
| 497 |
+
},
|
| 498 |
+
"right_arm": {
|
| 499 |
+
"space": "joint",
|
| 500 |
+
"joint_dims": [8, 9, 10, 11, 12, 13, 14],
|
| 501 |
+
"gripper_dim": 15,
|
| 502 |
+
"gripper_threshold": 0.8,
|
| 503 |
+
"gripper_open_is_high": True,
|
| 504 |
+
},
|
| 505 |
+
},
|
| 506 |
+
},
|
| 507 |
+
"single_eef_abs": {
|
| 508 |
+
"name": "single_eef_abs",
|
| 509 |
+
"input_dim": 7,
|
| 510 |
+
"input": "absolute",
|
| 511 |
+
"source_hz": 15.0,
|
| 512 |
+
"target_hz": 15.0,
|
| 513 |
+
"interpolation": "cubic",
|
| 514 |
+
"components": {
|
| 515 |
+
"arm": {
|
| 516 |
+
"space": "eef",
|
| 517 |
+
"position_dims": [0, 1, 2],
|
| 518 |
+
"rotation_dims": [3, 4, 5],
|
| 519 |
+
"rotation_format": "rotation_vector",
|
| 520 |
+
"gripper_dim": 6,
|
| 521 |
+
"gripper_threshold": 0.8,
|
| 522 |
+
"gripper_open_is_high": True,
|
| 523 |
+
}
|
| 524 |
+
},
|
| 525 |
+
},
|
| 526 |
+
"single_eef_delta": {
|
| 527 |
+
"name": "single_eef_delta",
|
| 528 |
+
"input_dim": 7,
|
| 529 |
+
"input": "delta",
|
| 530 |
+
"source_hz": 15.0,
|
| 531 |
+
"target_hz": 15.0,
|
| 532 |
+
"interpolation": "cubic",
|
| 533 |
+
"components": {
|
| 534 |
+
"arm": {
|
| 535 |
+
"space": "eef",
|
| 536 |
+
"position_dims": [0, 1, 2],
|
| 537 |
+
"rotation_dims": [3, 4, 5],
|
| 538 |
+
"rotation_format": "rotation_vector",
|
| 539 |
+
"gripper_dim": 6,
|
| 540 |
+
"gripper_threshold": 0.8,
|
| 541 |
+
"gripper_open_is_high": True,
|
| 542 |
+
}
|
| 543 |
+
},
|
| 544 |
+
},
|
| 545 |
+
"single_joint6_abs": {
|
| 546 |
+
"name": "single_joint6_abs",
|
| 547 |
+
"input_dim": 7,
|
| 548 |
+
"input": "absolute",
|
| 549 |
+
"source_hz": 15.0,
|
| 550 |
+
"target_hz": 15.0,
|
| 551 |
+
"interpolation": "cubic",
|
| 552 |
+
"components": {
|
| 553 |
+
"arm": {
|
| 554 |
+
"space": "joint",
|
| 555 |
+
"joint_dims": [0, 1, 2, 3, 4, 5],
|
| 556 |
+
"gripper_dim": 6,
|
| 557 |
+
"gripper_threshold": 0.8,
|
| 558 |
+
"gripper_open_is_high": True,
|
| 559 |
+
}
|
| 560 |
+
},
|
| 561 |
+
},
|
| 562 |
+
"single_joint6_abs_continuous_gripper": {
|
| 563 |
+
"name": "single_joint6_abs_continuous_gripper",
|
| 564 |
+
"input_dim": 6,
|
| 565 |
+
"input": "absolute",
|
| 566 |
+
"source_hz": 15.0,
|
| 567 |
+
"target_hz": 15.0,
|
| 568 |
+
"interpolation": "cubic",
|
| 569 |
+
"components": {"arm": {"space": "joint", "joint_dims": [0, 1, 2, 3, 4, 5]}},
|
| 570 |
+
},
|
| 571 |
+
"single_joint6_delta": {
|
| 572 |
+
"name": "single_joint6_delta",
|
| 573 |
+
"input_dim": 7,
|
| 574 |
+
"input": "delta",
|
| 575 |
+
"source_hz": 15.0,
|
| 576 |
+
"target_hz": 15.0,
|
| 577 |
+
"interpolation": "cubic",
|
| 578 |
+
"components": {
|
| 579 |
+
"arm": {
|
| 580 |
+
"space": "joint",
|
| 581 |
+
"joint_dims": [0, 1, 2, 3, 4, 5],
|
| 582 |
+
"gripper_dim": 6,
|
| 583 |
+
"gripper_threshold": 0.8,
|
| 584 |
+
"gripper_open_is_high": True,
|
| 585 |
+
}
|
| 586 |
+
},
|
| 587 |
+
},
|
| 588 |
+
"single_joint7_abs": {
|
| 589 |
+
"name": "single_joint7_abs",
|
| 590 |
+
"input_dim": 8,
|
| 591 |
+
"input": "absolute",
|
| 592 |
+
"source_hz": 15.0,
|
| 593 |
+
"target_hz": 15.0,
|
| 594 |
+
"interpolation": "cubic",
|
| 595 |
+
"components": {
|
| 596 |
+
"arm": {
|
| 597 |
+
"space": "joint",
|
| 598 |
+
"joint_dims": [0, 1, 2, 3, 4, 5, 6],
|
| 599 |
+
"gripper_dim": 7,
|
| 600 |
+
"gripper_threshold": 0.8,
|
| 601 |
+
"gripper_open_is_high": True,
|
| 602 |
+
}
|
| 603 |
+
},
|
| 604 |
+
},
|
| 605 |
+
"single_joint7_delta": {
|
| 606 |
+
"name": "single_joint7_delta",
|
| 607 |
+
"input_dim": 8,
|
| 608 |
+
"input": "delta",
|
| 609 |
+
"source_hz": 15.0,
|
| 610 |
+
"target_hz": 15.0,
|
| 611 |
+
"interpolation": "cubic",
|
| 612 |
+
"components": {
|
| 613 |
+
"arm": {
|
| 614 |
+
"space": "joint",
|
| 615 |
+
"joint_dims": [0, 1, 2, 3, 4, 5, 6],
|
| 616 |
+
"gripper_dim": 7,
|
| 617 |
+
"gripper_threshold": 0.8,
|
| 618 |
+
"gripper_open_is_high": True,
|
| 619 |
+
}
|
| 620 |
+
},
|
| 621 |
+
},
|
| 622 |
+
}
|
runtime/routing/presets.py
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User-facing action layouts compiled into shared joint/EEF trajectories."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from collections.abc import Mapping, Sequence
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import yaml
|
| 12 |
+
|
| 13 |
+
from ..common._validation import optional_finite_array, validate_dimension_partition
|
| 14 |
+
from ..physical.configuration import (
|
| 15 |
+
ConfigError,
|
| 16 |
+
_integer,
|
| 17 |
+
_mapping,
|
| 18 |
+
_number,
|
| 19 |
+
_reject_unknown,
|
| 20 |
+
_required,
|
| 21 |
+
_UniqueKeyLoader,
|
| 22 |
+
)
|
| 23 |
+
from ..physical.semantic import (
|
| 24 |
+
binarize_gripper_actions,
|
| 25 |
+
eef_pose_absolute,
|
| 26 |
+
eef_pose_components,
|
| 27 |
+
eef_pose_deltas,
|
| 28 |
+
eef_pose_from_components,
|
| 29 |
+
eef_pose_from_local_absolute,
|
| 30 |
+
eef_pose_local_absolute,
|
| 31 |
+
integrate_eef_deltas,
|
| 32 |
+
resample_uniform,
|
| 33 |
+
)
|
| 34 |
+
from ..physical.trajectory import ActiveTrajectory
|
| 35 |
+
|
| 36 |
+
_NAME = re.compile(r"[a-z][a-z0-9_]*\Z")
|
| 37 |
+
_SPACES = {"eef", "joint"}
|
| 38 |
+
_INPUTS = {"absolute", "delta"}
|
| 39 |
+
_ROTATIONS = {"rotation_vector", "euler_xyz", "quaternion_xyzw"}
|
| 40 |
+
_INTERPOLATIONS = {"linear", "pchip", "cubic"}
|
| 41 |
+
_GRIPPER_DIM = {"eef": 6, "joint": 7}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _format_dims(dims: Sequence[int]) -> str:
|
| 45 |
+
values = tuple(dims)
|
| 46 |
+
if len(values) == 1:
|
| 47 |
+
return str(values[0])
|
| 48 |
+
if values == tuple(range(values[0], values[-1] + 1)):
|
| 49 |
+
return f"{values[0]}..{values[-1]}"
|
| 50 |
+
return ",".join(map(str, values))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _dims(value: object, path: str) -> tuple[int, ...]:
|
| 54 |
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
|
| 55 |
+
raise ConfigError(f"{path} must be a list of dimensions")
|
| 56 |
+
output = tuple(
|
| 57 |
+
_integer(item, f"{path}[{index}]") for index, item in enumerate(value)
|
| 58 |
+
)
|
| 59 |
+
if not output:
|
| 60 |
+
raise ConfigError(f"{path} must not be empty")
|
| 61 |
+
if len(set(output)) != len(output):
|
| 62 |
+
raise ConfigError(f"{path} contains a repeated dimension")
|
| 63 |
+
return output
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@dataclass(frozen=True)
|
| 67 |
+
class ComponentSpec:
|
| 68 |
+
"""One independently encoded arm/body component in an input tensor.
|
| 69 |
+
|
| 70 |
+
``dims`` always refer to source tensor columns. Component names separate
|
| 71 |
+
BPE boundaries and output ordering, but never select primitive/BPE state.
|
| 72 |
+
The ``space`` field alone routes values to the shared joint or EEF profile.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
name: Component label, such as ``arm``, ``left_arm``, or ``right_arm``;
|
| 76 |
+
also the key for component-wise first-order boundaries.
|
| 77 |
+
space: ``joint`` for generalized joint coordinates, or ``eef`` for
|
| 78 |
+
end-effector position and orientation.
|
| 79 |
+
joint_dims: Ordered source columns for 1..7 joint coordinates. Revolute
|
| 80 |
+
coordinates use radians; other quantities need a canonical mapping.
|
| 81 |
+
position_dims: Three source columns for EEF x, y, z translation in meters.
|
| 82 |
+
rotation_dims: Source rotation columns, in the order of ``rotation_format``.
|
| 83 |
+
rotation_format: ``rotation_vector`` (axis times angle in radians),
|
| 84 |
+
``euler_xyz`` (extrinsic XYZ angles in radians), or
|
| 85 |
+
``quaternion_xyzw`` (unit quaternion). EEF delta layouts require
|
| 86 |
+
rotation vectors interpreted as right-multiplicative increments.
|
| 87 |
+
gripper_dim: Optional binary controller-command column. Omitted means
|
| 88 |
+
no separate binary gripper channel; it does not add a source column.
|
| 89 |
+
gripper_threshold: Inclusive threshold for open. Defaults to 0.8 when
|
| 90 |
+
high means open, or 0.2 when low means open.
|
| 91 |
+
gripper_open_is_high: If true, values >= threshold mean open; otherwise
|
| 92 |
+
values <= threshold mean open. Decode uses 1=open and 0=closed in
|
| 93 |
+
both absolute and delta layouts.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
name: str
|
| 97 |
+
space: str
|
| 98 |
+
joint_dims: tuple[int, ...] | None = None
|
| 99 |
+
position_dims: tuple[int, ...] | None = None
|
| 100 |
+
rotation_dims: tuple[int, ...] | None = None
|
| 101 |
+
rotation_format: str = "rotation_vector"
|
| 102 |
+
gripper_dim: int | None = None
|
| 103 |
+
gripper_threshold: float | None = None
|
| 104 |
+
gripper_open_is_high: bool = True
|
| 105 |
+
|
| 106 |
+
def __post_init__(self) -> None:
|
| 107 |
+
if not isinstance(self.name, str) or _NAME.fullmatch(self.name) is None:
|
| 108 |
+
raise ConfigError(f"component name must match {_NAME.pattern!r}")
|
| 109 |
+
if self.space not in _SPACES:
|
| 110 |
+
raise ConfigError(f"component {self.name!r}.space must be joint or eef")
|
| 111 |
+
if self.rotation_format not in _ROTATIONS:
|
| 112 |
+
raise ConfigError(
|
| 113 |
+
f"component {self.name!r}.rotation_format must be one of {sorted(_ROTATIONS)}"
|
| 114 |
+
)
|
| 115 |
+
for label, dims in (
|
| 116 |
+
("joint_dims", self.joint_dims),
|
| 117 |
+
("position_dims", self.position_dims),
|
| 118 |
+
("rotation_dims", self.rotation_dims),
|
| 119 |
+
):
|
| 120 |
+
if dims is not None:
|
| 121 |
+
_dims(dims, f"component {self.name}.{label}")
|
| 122 |
+
self._validate_motion_dims()
|
| 123 |
+
raw_dims = self.source_dims
|
| 124 |
+
if self.gripper_dim is not None:
|
| 125 |
+
_integer(self.gripper_dim, f"component {self.name}.gripper_dim")
|
| 126 |
+
if self.gripper_dim in raw_dims:
|
| 127 |
+
raise ConfigError(
|
| 128 |
+
f"component {self.name!r}.gripper_dim overlaps motion dims"
|
| 129 |
+
)
|
| 130 |
+
if not isinstance(self.gripper_open_is_high, bool):
|
| 131 |
+
raise ConfigError(
|
| 132 |
+
f"component {self.name!r}.gripper_open_is_high must be boolean"
|
| 133 |
+
)
|
| 134 |
+
threshold = self.gripper_threshold
|
| 135 |
+
if threshold is None:
|
| 136 |
+
threshold = 0.8 if self.gripper_open_is_high else 0.2
|
| 137 |
+
object.__setattr__(self, "gripper_threshold", threshold)
|
| 138 |
+
_number(threshold, f"component {self.name}.gripper_threshold")
|
| 139 |
+
|
| 140 |
+
def _validate_motion_dims(self) -> None:
|
| 141 |
+
if self.space == "joint":
|
| 142 |
+
if self.joint_dims is None or not 1 <= len(self.joint_dims) <= 7:
|
| 143 |
+
raise ConfigError(
|
| 144 |
+
f"joint component {self.name!r} requires 1..7 joint_dims"
|
| 145 |
+
)
|
| 146 |
+
if self.position_dims is not None or self.rotation_dims is not None:
|
| 147 |
+
raise ConfigError(
|
| 148 |
+
f"joint component {self.name!r} cannot define EEF dims"
|
| 149 |
+
)
|
| 150 |
+
return
|
| 151 |
+
if self.joint_dims is not None:
|
| 152 |
+
raise ConfigError(f"EEF component {self.name!r} cannot define joint_dims")
|
| 153 |
+
if self.position_dims is None or len(self.position_dims) != 3:
|
| 154 |
+
raise ConfigError(
|
| 155 |
+
f"EEF component {self.name!r} requires three position_dims"
|
| 156 |
+
)
|
| 157 |
+
expected = 4 if self.rotation_format == "quaternion_xyzw" else 3
|
| 158 |
+
if self.rotation_dims is None or len(self.rotation_dims) != expected:
|
| 159 |
+
raise ConfigError(
|
| 160 |
+
f"EEF component {self.name!r} requires {expected} rotation_dims for "
|
| 161 |
+
f"{self.rotation_format}"
|
| 162 |
+
)
|
| 163 |
+
if set(self.position_dims).intersection(self.rotation_dims):
|
| 164 |
+
raise ConfigError(f"EEF component {self.name!r} reuses a pose dimension")
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def source_dims(self) -> tuple[int, ...]:
|
| 168 |
+
"""All source motion columns in canonical component order."""
|
| 169 |
+
if self.space == "joint":
|
| 170 |
+
assert self.joint_dims is not None
|
| 171 |
+
return self.joint_dims
|
| 172 |
+
assert self.position_dims is not None and self.rotation_dims is not None
|
| 173 |
+
return (*self.position_dims, *self.rotation_dims)
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def all_source_dims(self) -> tuple[int, ...]:
|
| 177 |
+
"""Motion plus optional binary-gripper source columns."""
|
| 178 |
+
return (
|
| 179 |
+
self.source_dims
|
| 180 |
+
if self.gripper_dim is None
|
| 181 |
+
else (*self.source_dims, self.gripper_dim)
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def active_dims(self) -> tuple[int, ...]:
|
| 186 |
+
"""Shared-profile slots emitted by this component."""
|
| 187 |
+
motion_width = len(self.joint_dims or ()) if self.space == "joint" else 6
|
| 188 |
+
dims = tuple(range(motion_width))
|
| 189 |
+
return dims if self.gripper_dim is None else (*dims, _GRIPPER_DIM[self.space])
|
| 190 |
+
|
| 191 |
+
@classmethod
|
| 192 |
+
def from_dict(cls, name: str, payload: object, *, path: str) -> ComponentSpec:
|
| 193 |
+
values = _mapping(payload, path)
|
| 194 |
+
allowed = {
|
| 195 |
+
"space",
|
| 196 |
+
"joint_dims",
|
| 197 |
+
"position_dims",
|
| 198 |
+
"rotation_dims",
|
| 199 |
+
"rotation_format",
|
| 200 |
+
"gripper_dim",
|
| 201 |
+
"gripper_threshold",
|
| 202 |
+
"gripper_open_is_high",
|
| 203 |
+
}
|
| 204 |
+
_reject_unknown(values, allowed, path)
|
| 205 |
+
gripper_dim = values.get("gripper_dim")
|
| 206 |
+
if gripper_dim is not None:
|
| 207 |
+
gripper_dim = _integer(gripper_dim, f"{path}.gripper_dim")
|
| 208 |
+
return cls(
|
| 209 |
+
name=name,
|
| 210 |
+
space=str(_required(values, "space", path)),
|
| 211 |
+
joint_dims=None
|
| 212 |
+
if "joint_dims" not in values
|
| 213 |
+
else _dims(values["joint_dims"], f"{path}.joint_dims"),
|
| 214 |
+
position_dims=None
|
| 215 |
+
if "position_dims" not in values
|
| 216 |
+
else _dims(values["position_dims"], f"{path}.position_dims"),
|
| 217 |
+
rotation_dims=None
|
| 218 |
+
if "rotation_dims" not in values
|
| 219 |
+
else _dims(values["rotation_dims"], f"{path}.rotation_dims"),
|
| 220 |
+
rotation_format=str(values.get("rotation_format", "rotation_vector")),
|
| 221 |
+
gripper_dim=gripper_dim,
|
| 222 |
+
gripper_threshold=(
|
| 223 |
+
None
|
| 224 |
+
if values.get("gripper_threshold") is None
|
| 225 |
+
else _number(values["gripper_threshold"], f"{path}.gripper_threshold")
|
| 226 |
+
),
|
| 227 |
+
gripper_open_is_high=values.get("gripper_open_is_high", True),
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
def to_dict(self) -> dict[str, object]:
|
| 231 |
+
payload: dict[str, object] = {"space": self.space}
|
| 232 |
+
if self.space == "joint":
|
| 233 |
+
payload["joint_dims"] = list(self.joint_dims or ())
|
| 234 |
+
else:
|
| 235 |
+
payload.update(
|
| 236 |
+
{
|
| 237 |
+
"position_dims": list(self.position_dims or ()),
|
| 238 |
+
"rotation_dims": list(self.rotation_dims or ()),
|
| 239 |
+
"rotation_format": self.rotation_format,
|
| 240 |
+
}
|
| 241 |
+
)
|
| 242 |
+
if self.gripper_dim is not None:
|
| 243 |
+
payload.update(
|
| 244 |
+
{
|
| 245 |
+
"gripper_dim": self.gripper_dim,
|
| 246 |
+
"gripper_threshold": self.gripper_threshold,
|
| 247 |
+
"gripper_open_is_high": self.gripper_open_is_high,
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
return payload
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@dataclass(frozen=True)
|
| 254 |
+
class PresetConfig:
|
| 255 |
+
"""A dataset- and embodiment-independent physical ``[B,T,D]`` contract.
|
| 256 |
+
|
| 257 |
+
Names describe physical structure and control semantics. Dataset field
|
| 258 |
+
selection, normalization, and benchmark identity belong outside presets.
|
| 259 |
+
|
| 260 |
+
Repeating components supports multi-arm layouts; their source columns must
|
| 261 |
+
partition all D columns exactly once. The same layout can be used with
|
| 262 |
+
first- or second-order physical processors; primitive order is not selected
|
| 263 |
+
by this preset. See ``describe()`` for the complete source-column contract.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
name: Registered layout name describing physical structure and semantics.
|
| 267 |
+
input_dim: Total D in a source tensor ``(B,T,D)``; includes any grippers.
|
| 268 |
+
input: ``absolute`` targets or per-step ``delta`` motion increments.
|
| 269 |
+
The public frontend requires ``current_state(B,D)`` for absolute
|
| 270 |
+
inputs and omits it for delta inputs. Grippers remain commands.
|
| 271 |
+
components: Ordered joint/EEF component definitions with disjoint source
|
| 272 |
+
columns, including their optional binary gripper columns.
|
| 273 |
+
source_hz: Default source sampling rate, overridden by runtime ``fps``.
|
| 274 |
+
target_hz: Physical codec clock used by the token stream.
|
| 275 |
+
interpolation: Motion resampling method: ``linear``, ``pchip`` or
|
| 276 |
+
``cubic``. Binary gripper commands use hold interpolation.
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
name: str
|
| 280 |
+
input_dim: int
|
| 281 |
+
input: str
|
| 282 |
+
components: tuple[ComponentSpec, ...]
|
| 283 |
+
source_hz: float = 15.0
|
| 284 |
+
target_hz: float = 15.0
|
| 285 |
+
interpolation: str = "cubic"
|
| 286 |
+
|
| 287 |
+
def __post_init__(self) -> None:
|
| 288 |
+
if not isinstance(self.name, str) or _NAME.fullmatch(self.name) is None:
|
| 289 |
+
raise ConfigError(f"preset.name must match {_NAME.pattern!r}")
|
| 290 |
+
_integer(self.input_dim, "preset.input_dim", positive=True)
|
| 291 |
+
if self.input not in _INPUTS:
|
| 292 |
+
raise ConfigError("preset.input must be absolute or delta")
|
| 293 |
+
if not self.components:
|
| 294 |
+
raise ConfigError("preset.components must not be empty")
|
| 295 |
+
if not all(
|
| 296 |
+
isinstance(component, ComponentSpec) for component in self.components
|
| 297 |
+
):
|
| 298 |
+
raise ConfigError("preset.components must contain ComponentSpec values")
|
| 299 |
+
names = [component.name for component in self.components]
|
| 300 |
+
if len(set(names)) != len(names):
|
| 301 |
+
raise ConfigError("preset.components contains a duplicate name")
|
| 302 |
+
self._validate_source_partition()
|
| 303 |
+
for rate, path in (
|
| 304 |
+
(self.source_hz, "source_hz"),
|
| 305 |
+
(self.target_hz, "target_hz"),
|
| 306 |
+
):
|
| 307 |
+
checked = _number(rate, f"preset.{path}")
|
| 308 |
+
if checked <= 0.0:
|
| 309 |
+
raise ConfigError(f"preset.{path} must be positive and finite")
|
| 310 |
+
if self.interpolation not in _INTERPOLATIONS:
|
| 311 |
+
raise ConfigError(
|
| 312 |
+
f"preset.interpolation must be one of {sorted(_INTERPOLATIONS)}"
|
| 313 |
+
)
|
| 314 |
+
if self.input == "delta" and any(
|
| 315 |
+
component.space == "eef" and component.rotation_format != "rotation_vector"
|
| 316 |
+
for component in self.components
|
| 317 |
+
):
|
| 318 |
+
raise ConfigError(
|
| 319 |
+
"EEF delta components require rotation_format=rotation_vector"
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
def _validate_source_partition(self) -> None:
|
| 323 |
+
validate_dimension_partition(
|
| 324 |
+
(
|
| 325 |
+
(component.name, component.all_source_dims)
|
| 326 |
+
for component in self.components
|
| 327 |
+
),
|
| 328 |
+
self.input_dim,
|
| 329 |
+
label="input dim",
|
| 330 |
+
error_type=ConfigError,
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
@classmethod
|
| 334 |
+
def from_dict(cls, payload: object, *, path: str = "preset") -> PresetConfig:
|
| 335 |
+
values = _mapping(payload, path)
|
| 336 |
+
allowed = {
|
| 337 |
+
"name",
|
| 338 |
+
"input_dim",
|
| 339 |
+
"input",
|
| 340 |
+
"source_hz",
|
| 341 |
+
"target_hz",
|
| 342 |
+
"interpolation",
|
| 343 |
+
"components",
|
| 344 |
+
}
|
| 345 |
+
_reject_unknown(values, allowed, path)
|
| 346 |
+
raw_components = _mapping(
|
| 347 |
+
_required(values, "components", path), f"{path}.components"
|
| 348 |
+
)
|
| 349 |
+
components = tuple(
|
| 350 |
+
ComponentSpec.from_dict(name, raw, path=f"{path}.components.{name}")
|
| 351 |
+
for name, raw in raw_components.items()
|
| 352 |
+
)
|
| 353 |
+
return cls(
|
| 354 |
+
name=str(_required(values, "name", path)),
|
| 355 |
+
input_dim=_integer(
|
| 356 |
+
_required(values, "input_dim", path), f"{path}.input_dim", positive=True
|
| 357 |
+
),
|
| 358 |
+
input=str(_required(values, "input", path)),
|
| 359 |
+
components=components,
|
| 360 |
+
source_hz=_number(values.get("source_hz", 15.0), f"{path}.source_hz"),
|
| 361 |
+
target_hz=_number(values.get("target_hz", 15.0), f"{path}.target_hz"),
|
| 362 |
+
interpolation=str(values.get("interpolation", "cubic")),
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
@classmethod
|
| 366 |
+
def from_yaml(cls, path: str | Path) -> PresetConfig:
|
| 367 |
+
"""Load one strict user preset without executing arbitrary code."""
|
| 368 |
+
source = Path(path)
|
| 369 |
+
try:
|
| 370 |
+
payload = yaml.load(
|
| 371 |
+
source.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader
|
| 372 |
+
)
|
| 373 |
+
except yaml.YAMLError as error:
|
| 374 |
+
raise ConfigError(f"cannot parse {source}: {error}") from error
|
| 375 |
+
return cls.from_dict(payload, path=str(source))
|
| 376 |
+
|
| 377 |
+
def save_yaml(self, path: str | Path) -> Path:
|
| 378 |
+
"""Save the complete reproducible preset and return its path."""
|
| 379 |
+
target = Path(path)
|
| 380 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 381 |
+
target.write_text(
|
| 382 |
+
yaml.safe_dump(self.to_dict(), sort_keys=False), encoding="utf-8"
|
| 383 |
+
)
|
| 384 |
+
return target
|
| 385 |
+
|
| 386 |
+
def to_dict(self) -> dict[str, object]:
|
| 387 |
+
payload = {
|
| 388 |
+
"name": self.name,
|
| 389 |
+
"input_dim": self.input_dim,
|
| 390 |
+
"input": self.input,
|
| 391 |
+
"source_hz": self.source_hz,
|
| 392 |
+
"target_hz": self.target_hz,
|
| 393 |
+
"interpolation": self.interpolation,
|
| 394 |
+
"components": {
|
| 395 |
+
component.name: component.to_dict() for component in self.components
|
| 396 |
+
},
|
| 397 |
+
}
|
| 398 |
+
return payload
|
| 399 |
+
|
| 400 |
+
def summary(self) -> str:
|
| 401 |
+
"""Return a compact action-space label for discovery UIs."""
|
| 402 |
+
spaces = "+".join(
|
| 403 |
+
dict.fromkeys(component.space for component in self.components)
|
| 404 |
+
)
|
| 405 |
+
return f"{self.name} (D={self.input_dim}, {self.input}, {spaces}, {self.source_hz:g} Hz input)"
|
| 406 |
+
|
| 407 |
+
def describe(self) -> str:
|
| 408 |
+
"""Describe every source tensor column and its physical interpretation."""
|
| 409 |
+
motion = "position" if self.input == "absolute" else "per-step delta"
|
| 410 |
+
lines = [self.summary()]
|
| 411 |
+
for component in self.components:
|
| 412 |
+
if component.space == "joint":
|
| 413 |
+
assert component.joint_dims is not None
|
| 414 |
+
lines.append(
|
| 415 |
+
f" dims {_format_dims(component.joint_dims)}: {component.name} "
|
| 416 |
+
f"generalized-joint {motion} [rad or registered canonical unit]"
|
| 417 |
+
)
|
| 418 |
+
else:
|
| 419 |
+
assert component.position_dims is not None
|
| 420 |
+
assert component.rotation_dims is not None
|
| 421 |
+
lines.append(
|
| 422 |
+
f" dims {_format_dims(component.position_dims)}: {component.name} "
|
| 423 |
+
f"EEF translation {motion} (x, y, z in listed order) [m]"
|
| 424 |
+
)
|
| 425 |
+
rotation_motion = (
|
| 426 |
+
"orientation" if self.input == "absolute" else "increment"
|
| 427 |
+
)
|
| 428 |
+
rotation_unit = (
|
| 429 |
+
"unit xyzw quaternion"
|
| 430 |
+
if component.rotation_format == "quaternion_xyzw"
|
| 431 |
+
else "rad"
|
| 432 |
+
)
|
| 433 |
+
lines.append(
|
| 434 |
+
f" dims {_format_dims(component.rotation_dims)}: {component.name} EEF "
|
| 435 |
+
f"{rotation_motion} [{component.rotation_format}, {rotation_unit}]"
|
| 436 |
+
)
|
| 437 |
+
if self.input == "delta":
|
| 438 |
+
lines.append(
|
| 439 |
+
" rotation_vector = axis * angle [rad], right-multiplicative "
|
| 440 |
+
"body-frame increment: R_next = R_previous @ Exp(rotvec)"
|
| 441 |
+
)
|
| 442 |
+
elif component.rotation_format == "rotation_vector":
|
| 443 |
+
lines.append(
|
| 444 |
+
" rotation_vector = axis * angle [rad], not Euler angles"
|
| 445 |
+
)
|
| 446 |
+
elif component.rotation_format == "euler_xyz":
|
| 447 |
+
lines.append(" euler_xyz = extrinsic x, y, z Euler angles [rad]")
|
| 448 |
+
if component.gripper_dim is not None:
|
| 449 |
+
comparison = ">=" if component.gripper_open_is_high else "<="
|
| 450 |
+
lines.append(
|
| 451 |
+
f" dim {component.gripper_dim}: {component.name} gripper command "
|
| 452 |
+
f"(open if value {comparison} {component.gripper_threshold:g}, otherwise closed; "
|
| 453 |
+
"decoded as absolute command: 1=open, 0=closed)"
|
| 454 |
+
)
|
| 455 |
+
else:
|
| 456 |
+
lines.append(
|
| 457 |
+
f" {component.name}: no binary gripper channel; all listed columns are motion"
|
| 458 |
+
)
|
| 459 |
+
if self.input == "absolute":
|
| 460 |
+
lines.append(
|
| 461 |
+
" encode/decode require the same current_state (B,D), immediately before row 0"
|
| 462 |
+
)
|
| 463 |
+
else:
|
| 464 |
+
lines.append(
|
| 465 |
+
" encode/decode: omit current_state; deltas are per-step increments, not velocities"
|
| 466 |
+
)
|
| 467 |
+
lines.append(
|
| 468 |
+
f" resampling: {self.source_hz:g} Hz default -> {self.target_hz:g} Hz codec "
|
| 469 |
+
f"clock ({self.interpolation})"
|
| 470 |
+
)
|
| 471 |
+
return "\n".join(lines)
|
| 472 |
+
|
| 473 |
+
def canonicalize(
|
| 474 |
+
self,
|
| 475 |
+
values: np.ndarray,
|
| 476 |
+
*,
|
| 477 |
+
source_hz: float | None = None,
|
| 478 |
+
current_state: np.ndarray | None = None,
|
| 479 |
+
) -> dict[str, list[ActiveTrajectory]]:
|
| 480 |
+
"""Convert a batch into local paths for the two shared profiles.
|
| 481 |
+
|
| 482 |
+
Args:
|
| 483 |
+
values (np.ndarray): ``(B,T,D)`` tensor interpreted by this preset.
|
| 484 |
+
source_hz: Sampling rate of ``values``.
|
| 485 |
+
current_state (np.ndarray | None): Optional ``(B,D)`` state before
|
| 486 |
+
row zero. For order-0 inputs it defines the first transition;
|
| 487 |
+
when omitted, each input's first row is its anchor. Order-1
|
| 488 |
+
inputs are already self-contained and do not use the value.
|
| 489 |
+
"""
|
| 490 |
+
array = np.asarray(values, dtype=np.float32)
|
| 491 |
+
if array.ndim != 3 or len(array) < 1 or array.shape[1] < 1:
|
| 492 |
+
raise ValueError("preset input must have non-empty shape [B, T, D]")
|
| 493 |
+
if array.shape[2] != self.input_dim:
|
| 494 |
+
raise ValueError(
|
| 495 |
+
f"preset {self.name!r} expects input_dim={self.input_dim}, got {array.shape[2]}"
|
| 496 |
+
)
|
| 497 |
+
if not np.all(np.isfinite(array)):
|
| 498 |
+
raise ValueError("preset input contains a non-finite value")
|
| 499 |
+
states: list[np.ndarray | None]
|
| 500 |
+
if current_state is None:
|
| 501 |
+
states = [None] * len(array)
|
| 502 |
+
else:
|
| 503 |
+
state_array = optional_finite_array(
|
| 504 |
+
current_state, shape=(len(array), self.input_dim)
|
| 505 |
+
)
|
| 506 |
+
assert state_array is not None
|
| 507 |
+
states = list(state_array)
|
| 508 |
+
return {
|
| 509 |
+
component.name: [
|
| 510 |
+
self._canonicalize_component(
|
| 511 |
+
episode,
|
| 512 |
+
component,
|
| 513 |
+
source_hz=self.source_hz if source_hz is None else source_hz,
|
| 514 |
+
current_state=state,
|
| 515 |
+
)
|
| 516 |
+
for episode, state in zip(array, states, strict=True)
|
| 517 |
+
]
|
| 518 |
+
for component in self.components
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
def canonicalize_episode(
|
| 522 |
+
self,
|
| 523 |
+
values: np.ndarray,
|
| 524 |
+
*,
|
| 525 |
+
source_hz: float | None = None,
|
| 526 |
+
current_state: np.ndarray | None = None,
|
| 527 |
+
) -> dict[str, ActiveTrajectory]:
|
| 528 |
+
"""Canonicalize one variable-length episode and optional ``(D,)`` anchor."""
|
| 529 |
+
episode = np.asarray(values, dtype=np.float32)
|
| 530 |
+
if episode.ndim != 2 or len(episode) < 1 or episode.shape[1] != self.input_dim:
|
| 531 |
+
raise ValueError(
|
| 532 |
+
f"preset {self.name!r} expects a non-empty [T, {self.input_dim}] episode"
|
| 533 |
+
)
|
| 534 |
+
if not np.all(np.isfinite(episode)):
|
| 535 |
+
raise ValueError("preset input contains a non-finite value")
|
| 536 |
+
state = optional_finite_array(current_state, shape=(self.input_dim,))
|
| 537 |
+
return {
|
| 538 |
+
component.name: self._canonicalize_component(
|
| 539 |
+
episode,
|
| 540 |
+
component,
|
| 541 |
+
source_hz=self.source_hz if source_hz is None else source_hz,
|
| 542 |
+
current_state=state,
|
| 543 |
+
)
|
| 544 |
+
for component in self.components
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
def profile_trajectories(
|
| 548 |
+
self, values: np.ndarray, *, current_state: np.ndarray | None = None
|
| 549 |
+
) -> dict[str, list[ActiveTrajectory]]:
|
| 550 |
+
"""Canonicalize one episode and group its components by joint/EEF profile."""
|
| 551 |
+
canonical = self.canonicalize_episode(values, current_state=current_state)
|
| 552 |
+
output: dict[str, list[ActiveTrajectory]] = {}
|
| 553 |
+
for component in self.components:
|
| 554 |
+
output.setdefault(component.space, []).append(canonical[component.name])
|
| 555 |
+
return output
|
| 556 |
+
|
| 557 |
+
def _canonicalize_component(
|
| 558 |
+
self,
|
| 559 |
+
episode: np.ndarray,
|
| 560 |
+
component: ComponentSpec,
|
| 561 |
+
*,
|
| 562 |
+
source_hz: float,
|
| 563 |
+
current_state: np.ndarray | None,
|
| 564 |
+
) -> ActiveTrajectory:
|
| 565 |
+
if component.space == "joint":
|
| 566 |
+
motion = self._joint_motion(
|
| 567 |
+
episode, component, source_hz=source_hz, current_state=current_state
|
| 568 |
+
)
|
| 569 |
+
else:
|
| 570 |
+
motion = self._eef_motion(
|
| 571 |
+
episode, component, source_hz=source_hz, current_state=current_state
|
| 572 |
+
)
|
| 573 |
+
if component.gripper_dim is None:
|
| 574 |
+
return ActiveTrajectory(motion, component.active_dims)
|
| 575 |
+
gripper = resample_uniform(
|
| 576 |
+
episode[:, component.gripper_dim : component.gripper_dim + 1],
|
| 577 |
+
source_hz=source_hz,
|
| 578 |
+
target_hz=self.target_hz,
|
| 579 |
+
kind="hold",
|
| 580 |
+
method=self.interpolation,
|
| 581 |
+
)
|
| 582 |
+
binary = binarize_gripper_actions(
|
| 583 |
+
gripper,
|
| 584 |
+
threshold=component.gripper_threshold,
|
| 585 |
+
open_is_high=component.gripper_open_is_high,
|
| 586 |
+
)
|
| 587 |
+
if len(binary) != len(motion):
|
| 588 |
+
raise RuntimeError("motion and gripper produced different target horizons")
|
| 589 |
+
return ActiveTrajectory(
|
| 590 |
+
np.concatenate((motion, binary), axis=1), component.active_dims
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
def _joint_motion(
|
| 594 |
+
self,
|
| 595 |
+
episode: np.ndarray,
|
| 596 |
+
component: ComponentSpec,
|
| 597 |
+
*,
|
| 598 |
+
source_hz: float,
|
| 599 |
+
current_state: np.ndarray | None,
|
| 600 |
+
) -> np.ndarray:
|
| 601 |
+
assert component.joint_dims is not None
|
| 602 |
+
source = np.take(episode, component.joint_dims, axis=1)
|
| 603 |
+
if self.input == "delta":
|
| 604 |
+
source = np.cumsum(source, axis=0, dtype=np.float32)
|
| 605 |
+
anchor = np.zeros((1, source.shape[1]), dtype=np.float32)
|
| 606 |
+
else:
|
| 607 |
+
raw_anchor = (
|
| 608 |
+
source[:1]
|
| 609 |
+
if current_state is None
|
| 610 |
+
else np.take(current_state, component.joint_dims)[None]
|
| 611 |
+
)
|
| 612 |
+
unwrapped = np.unwrap(np.concatenate((raw_anchor, source), axis=0), axis=0)
|
| 613 |
+
anchor = np.asarray(unwrapped[:1], dtype=np.float32)
|
| 614 |
+
source = np.asarray(unwrapped[1:], dtype=np.float32)
|
| 615 |
+
sampled = resample_uniform(
|
| 616 |
+
source,
|
| 617 |
+
source_hz=source_hz,
|
| 618 |
+
target_hz=self.target_hz,
|
| 619 |
+
kind="linear",
|
| 620 |
+
method=self.interpolation,
|
| 621 |
+
)
|
| 622 |
+
return np.asarray(sampled - anchor, dtype=np.float32)
|
| 623 |
+
|
| 624 |
+
def _eef_motion(
|
| 625 |
+
self,
|
| 626 |
+
episode: np.ndarray,
|
| 627 |
+
component: ComponentSpec,
|
| 628 |
+
*,
|
| 629 |
+
source_hz: float,
|
| 630 |
+
current_state: np.ndarray | None,
|
| 631 |
+
) -> np.ndarray:
|
| 632 |
+
assert (
|
| 633 |
+
component.position_dims is not None and component.rotation_dims is not None
|
| 634 |
+
)
|
| 635 |
+
position = np.take(episode, component.position_dims, axis=1)
|
| 636 |
+
rotation = np.take(episode, component.rotation_dims, axis=1)
|
| 637 |
+
if self.input == "delta":
|
| 638 |
+
pose = integrate_eef_deltas(np.concatenate((position, rotation), axis=1))
|
| 639 |
+
pose = resample_uniform(
|
| 640 |
+
pose,
|
| 641 |
+
source_hz=source_hz,
|
| 642 |
+
target_hz=self.target_hz,
|
| 643 |
+
kind="pose_xyzw",
|
| 644 |
+
method=self.interpolation,
|
| 645 |
+
)
|
| 646 |
+
return eef_pose_absolute(pose)
|
| 647 |
+
pose = eef_pose_from_components(position, rotation, component.rotation_format)
|
| 648 |
+
if current_state is None:
|
| 649 |
+
initial_pose = pose[0]
|
| 650 |
+
else:
|
| 651 |
+
initial_pose = eef_pose_from_components(
|
| 652 |
+
np.take(current_state, component.position_dims)[None],
|
| 653 |
+
np.take(current_state, component.rotation_dims)[None],
|
| 654 |
+
component.rotation_format,
|
| 655 |
+
)[0]
|
| 656 |
+
pose = resample_uniform(
|
| 657 |
+
pose,
|
| 658 |
+
source_hz=source_hz,
|
| 659 |
+
target_hz=self.target_hz,
|
| 660 |
+
kind="pose_xyzw",
|
| 661 |
+
method=self.interpolation,
|
| 662 |
+
)
|
| 663 |
+
return eef_pose_local_absolute(pose, initial_pose=initial_pose)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
def format_action_spaces(
|
| 667 |
+
presets: Mapping[str, PresetConfig],
|
| 668 |
+
*,
|
| 669 |
+
selected: str | None = None,
|
| 670 |
+
action_dim: int | None = None,
|
| 671 |
+
second_order_dims: Mapping[str, Sequence[int]] | None = None,
|
| 672 |
+
) -> str:
|
| 673 |
+
"""Explain registered tensor layouts without inferring semantics from shape.
|
| 674 |
+
|
| 675 |
+
Args:
|
| 676 |
+
presets: Registered names mapped to their tensor-layout definitions.
|
| 677 |
+
selected: Currently bound action-space name, if any.
|
| 678 |
+
action_dim: Optional source-width filter; matching layouts are candidates.
|
| 679 |
+
second_order_dims: Physical profile names mapped to dimensions requiring
|
| 680 |
+
an explicit first-order boundary in the loaded artifact. ``None``
|
| 681 |
+
means the artifact's boundary requirements are unknown.
|
| 682 |
+
"""
|
| 683 |
+
if action_dim is not None:
|
| 684 |
+
action_dim = _integer(action_dim, "action_dim", positive=True)
|
| 685 |
+
if not presets:
|
| 686 |
+
return "No action spaces are registered."
|
| 687 |
+
candidates = sorted(
|
| 688 |
+
(
|
| 689 |
+
preset
|
| 690 |
+
for preset in presets.values()
|
| 691 |
+
if action_dim is None or preset.input_dim == action_dim
|
| 692 |
+
),
|
| 693 |
+
key=lambda preset: (preset.input_dim, preset.name),
|
| 694 |
+
)
|
| 695 |
+
lines = [
|
| 696 |
+
"ActionCodec2 — action layout guide",
|
| 697 |
+
f"Selected action_space: {selected or 'none (choose a layout before encoding)'}",
|
| 698 |
+
"",
|
| 699 |
+
"Input: NumPy arrays or Torch tensors, (T,D) or (B,T,D).",
|
| 700 |
+
" T = timesteps, D = columns per action, B = batch size; column indices start at 0.",
|
| 701 |
+
" Any positive T is accepted. A batch needs equal T; encode unequal-length episodes separately.",
|
| 702 |
+
" decode returns (B,T,D), including B=1 for a single episode; values are quantized, not lossless.",
|
| 703 |
+
"",
|
| 704 |
+
"How to choose:",
|
| 705 |
+
" joint = robot joint coordinates; EEF = end-effector (tool) position and orientation.",
|
| 706 |
+
" absolute = target coordinates; delta = per-step motion increments, not velocities.",
|
| 707 |
+
" Match column order, units, rotation convention, gripper commands and controller fps.",
|
| 708 |
+
" Shape alone cannot identify physical meaning: layouts sharing D are only candidates.",
|
| 709 |
+
" Consult your dataset/controller documentation if these meanings are unknown.",
|
| 710 |
+
" Supply physical units, not normalized [-1,1] features; convert degrees to radians.",
|
| 711 |
+
" Non-radian joint coordinates need an explicit mapping to the registered canonical units.",
|
| 712 |
+
" Pass the actual sampling rate as fps=...; defaults and codec rates appear below.",
|
| 713 |
+
" Resampling can change decoded T; token coverage determines the returned length.",
|
| 714 |
+
"",
|
| 715 |
+
f"Registered configurations: {len(candidates)} shown / {len(presets)} total"
|
| 716 |
+
+ (
|
| 717 |
+
""
|
| 718 |
+
if action_dim is None
|
| 719 |
+
else f" (D={action_dim} candidates, not automatic selection)"
|
| 720 |
+
),
|
| 721 |
+
]
|
| 722 |
+
if not candidates:
|
| 723 |
+
widths = ", ".join(
|
| 724 |
+
map(str, sorted({preset.input_dim for preset in presets.values()}))
|
| 725 |
+
)
|
| 726 |
+
lines.extend(
|
| 727 |
+
[
|
| 728 |
+
f" No registered action space matches D={action_dim}. Available D: {widths}.",
|
| 729 |
+
" Confirm your column meanings and register a custom layout; do not pad/truncate to force a match.",
|
| 730 |
+
]
|
| 731 |
+
)
|
| 732 |
+
for preset in candidates:
|
| 733 |
+
name = preset.name
|
| 734 |
+
marker = " [selected]" if name == selected else ""
|
| 735 |
+
description = preset.describe().splitlines()
|
| 736 |
+
lines.extend(["", f"- {description[0]}{marker}"])
|
| 737 |
+
lines.extend(f" {line.strip()}" for line in description[1:])
|
| 738 |
+
if second_order_dims is not None:
|
| 739 |
+
boundaries = [
|
| 740 |
+
f"{component.name!r}: (B,{len(component.active_dims)})"
|
| 741 |
+
for component in preset.components
|
| 742 |
+
if set(component.active_dims).intersection(
|
| 743 |
+
second_order_dims.get(component.space, ())
|
| 744 |
+
)
|
| 745 |
+
]
|
| 746 |
+
lines.append(
|
| 747 |
+
" previous_first_order: "
|
| 748 |
+
+ (
|
| 749 |
+
"required in encode/decode, {" + ", ".join(boundaries) + "}; "
|
| 750 |
+
f"reconstructed increments at {preset.target_hz:g} Hz, B=1 for a single episode"
|
| 751 |
+
if boundaries
|
| 752 |
+
else "not required by this artifact for this layout"
|
| 753 |
+
)
|
| 754 |
+
)
|
| 755 |
+
lines.extend(
|
| 756 |
+
[
|
| 757 |
+
"",
|
| 758 |
+
"Next steps:",
|
| 759 |
+
" Load: ActionCodec2.from_pretrained(path, action_space='NAME')",
|
| 760 |
+
" Switch: codec = codec.for_action_space('NAME')",
|
| 761 |
+
" Filter: codec.print_action_spaces(action_dim=D)",
|
| 762 |
+
" Config: codec.action_spaces['NAME'].to_dict() # exact settings for a listed layout",
|
| 763 |
+
" Custom: codec.register_action_space(preset) # explicit column definitions, then switch",
|
| 764 |
+
" Custom layouts can reorder columns; each joint component supports 1..7 coordinates.",
|
| 765 |
+
" EEF absolute rotations support rotation_vector, euler_xyz or quaternion_xyzw (unit x,y,z,w).",
|
| 766 |
+
" EEF delta rotations require rotation_vector with the convention shown above.",
|
| 767 |
+
" For absolute layouts, current_state is (B,D), even when actions are (T,D).",
|
| 768 |
+
]
|
| 769 |
+
)
|
| 770 |
+
return "\n".join(lines)
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def resample_canonical_trajectory(
|
| 774 |
+
trajectory: ActiveTrajectory,
|
| 775 |
+
component: ComponentSpec,
|
| 776 |
+
*,
|
| 777 |
+
source_hz: float,
|
| 778 |
+
target_hz: float,
|
| 779 |
+
target_length: int | None = None,
|
| 780 |
+
method: str = "linear",
|
| 781 |
+
) -> ActiveTrajectory:
|
| 782 |
+
"""Rate-convert one decoded canonical component without changing slots."""
|
| 783 |
+
values = np.asarray(trajectory.values, dtype=np.float32)
|
| 784 |
+
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
|
| 785 |
+
motion = values[:, :motion_width]
|
| 786 |
+
if component.space == "eef":
|
| 787 |
+
pose = eef_pose_from_components(
|
| 788 |
+
motion[:, :3], motion[:, 3:6], "rotation_vector"
|
| 789 |
+
)
|
| 790 |
+
sampled_pose = resample_uniform(
|
| 791 |
+
pose,
|
| 792 |
+
source_hz=source_hz,
|
| 793 |
+
target_hz=target_hz,
|
| 794 |
+
kind="pose_xyzw",
|
| 795 |
+
target_length=target_length,
|
| 796 |
+
method=method,
|
| 797 |
+
)
|
| 798 |
+
sampled_motion = eef_pose_absolute(sampled_pose)
|
| 799 |
+
else:
|
| 800 |
+
sampled_motion = resample_uniform(
|
| 801 |
+
motion,
|
| 802 |
+
source_hz=source_hz,
|
| 803 |
+
target_hz=target_hz,
|
| 804 |
+
kind="linear",
|
| 805 |
+
target_length=target_length,
|
| 806 |
+
method=method,
|
| 807 |
+
)
|
| 808 |
+
if component.gripper_dim is None:
|
| 809 |
+
return ActiveTrajectory(sampled_motion, trajectory.active_dims)
|
| 810 |
+
gripper = resample_uniform(
|
| 811 |
+
values[:, motion_width : motion_width + 1],
|
| 812 |
+
source_hz=source_hz,
|
| 813 |
+
target_hz=target_hz,
|
| 814 |
+
kind="hold",
|
| 815 |
+
target_length=target_length,
|
| 816 |
+
method=method,
|
| 817 |
+
)
|
| 818 |
+
return ActiveTrajectory(
|
| 819 |
+
np.concatenate((sampled_motion, gripper), axis=1), trajectory.active_dims
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def externalize_canonical_trajectory(
|
| 824 |
+
trajectory: ActiveTrajectory,
|
| 825 |
+
component: ComponentSpec,
|
| 826 |
+
*,
|
| 827 |
+
current_state: np.ndarray | None,
|
| 828 |
+
) -> np.ndarray:
|
| 829 |
+
"""Return deltas, or absolute values when an external anchor is supplied.
|
| 830 |
+
|
| 831 |
+
The input is always a decoded local position/pose path. Gripper remains an
|
| 832 |
+
absolute binary command in both output modes.
|
| 833 |
+
"""
|
| 834 |
+
values = np.asarray(trajectory.values, dtype=np.float32)
|
| 835 |
+
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
|
| 836 |
+
motion = values[:, :motion_width]
|
| 837 |
+
if component.space == "joint":
|
| 838 |
+
if current_state is None:
|
| 839 |
+
previous = np.concatenate((np.zeros_like(motion[:1]), motion[:-1]), axis=0)
|
| 840 |
+
output_motion = motion - previous
|
| 841 |
+
else:
|
| 842 |
+
assert component.joint_dims is not None
|
| 843 |
+
output_motion = motion + np.take(current_state, component.joint_dims)[None]
|
| 844 |
+
else:
|
| 845 |
+
local_pose = eef_pose_from_components(
|
| 846 |
+
motion[:, :3], motion[:, 3:6], "rotation_vector"
|
| 847 |
+
)
|
| 848 |
+
if current_state is None:
|
| 849 |
+
output_motion = eef_pose_deltas(local_pose)
|
| 850 |
+
else:
|
| 851 |
+
assert (
|
| 852 |
+
component.position_dims is not None
|
| 853 |
+
and component.rotation_dims is not None
|
| 854 |
+
)
|
| 855 |
+
initial_pose = eef_pose_from_components(
|
| 856 |
+
np.take(current_state, component.position_dims)[None],
|
| 857 |
+
np.take(current_state, component.rotation_dims)[None],
|
| 858 |
+
component.rotation_format,
|
| 859 |
+
)[0]
|
| 860 |
+
output_motion = eef_pose_components(
|
| 861 |
+
eef_pose_from_local_absolute(local_pose, initial_pose=initial_pose),
|
| 862 |
+
component.rotation_format,
|
| 863 |
+
)
|
| 864 |
+
if component.gripper_dim is None:
|
| 865 |
+
return np.asarray(output_motion, dtype=np.float32)
|
| 866 |
+
gripper = values[:, motion_width : motion_width + 1]
|
| 867 |
+
if not component.gripper_open_is_high:
|
| 868 |
+
gripper = 1.0 - gripper
|
| 869 |
+
return np.asarray(
|
| 870 |
+
np.concatenate((output_motion, gripper), axis=1),
|
| 871 |
+
dtype=np.float32,
|
| 872 |
+
)
|
runtime/routing/routing.py
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Required-preset routing over the two shared ActionCodec2 processors.
|
| 2 |
+
|
| 3 |
+
Each profile owns a disjoint token namespace and is named for a physical
|
| 4 |
+
quantity, never a robot. Presets describe tensor slots and preprocessing but
|
| 5 |
+
cannot alter already-fitted primitives, BPE rules, or token meanings.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
from collections.abc import Mapping, Sequence
|
| 12 |
+
from dataclasses import asdict, dataclass
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from types import MappingProxyType
|
| 15 |
+
from typing import TYPE_CHECKING
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import yaml
|
| 19 |
+
|
| 20 |
+
from ..common._validation import optional_finite_array
|
| 21 |
+
from .cache import BoundedCache
|
| 22 |
+
from ..physical.configuration import (
|
| 23 |
+
ConfigError,
|
| 24 |
+
_integer,
|
| 25 |
+
_mapping,
|
| 26 |
+
_reject_unknown,
|
| 27 |
+
_required,
|
| 28 |
+
_UniqueKeyLoader,
|
| 29 |
+
)
|
| 30 |
+
from ..integration.hf import (
|
| 31 |
+
ROUTER_ALLOW_PATTERNS,
|
| 32 |
+
ROUTER_CONFIG_NAME,
|
| 33 |
+
ROUTER_MODEL_TYPE,
|
| 34 |
+
resolve_pretrained_directory,
|
| 35 |
+
save_huggingface_metadata,
|
| 36 |
+
save_model_card,
|
| 37 |
+
)
|
| 38 |
+
from ..physical.physical_processor import ActionCodec2Processor
|
| 39 |
+
from .presets import (
|
| 40 |
+
ComponentSpec,
|
| 41 |
+
PresetConfig,
|
| 42 |
+
externalize_canonical_trajectory,
|
| 43 |
+
format_action_spaces,
|
| 44 |
+
resample_canonical_trajectory,
|
| 45 |
+
)
|
| 46 |
+
from ..physical.semantic import resampled_horizon
|
| 47 |
+
from ..physical.trajectory import ActiveTrajectory
|
| 48 |
+
|
| 49 |
+
if TYPE_CHECKING:
|
| 50 |
+
from .processing_actioncodec2 import ActionCodec2
|
| 51 |
+
|
| 52 |
+
# v3 freezes stateless decode semantics: no current_state -> deltas;
|
| 53 |
+
# current_state -> absolute motion. v2 returned local position paths.
|
| 54 |
+
ROUTER_CONFIG_VERSION = 3
|
| 55 |
+
_NAME = re.compile(r"[a-z][a-z0-9_]*\Z")
|
| 56 |
+
_PROFILES = {"eef", "joint"}
|
| 57 |
+
_PROFILE_WIDTHS = {"eef": 7, "joint": 8}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _current_state_rows(
|
| 61 |
+
current_state: np.ndarray | None, *, count: int, input_dim: int
|
| 62 |
+
) -> list[np.ndarray | None]:
|
| 63 |
+
if current_state is None:
|
| 64 |
+
return [None] * count
|
| 65 |
+
values = optional_finite_array(current_state, shape=(count, input_dim))
|
| 66 |
+
assert values is not None
|
| 67 |
+
return list(values)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass(frozen=True)
|
| 71 |
+
class ProfileSpec:
|
| 72 |
+
"""Token namespace metadata for one shared physical processor."""
|
| 73 |
+
|
| 74 |
+
name: str
|
| 75 |
+
token_offset: int
|
| 76 |
+
token_budget: int
|
| 77 |
+
artifact_fingerprint: str
|
| 78 |
+
|
| 79 |
+
def __post_init__(self) -> None:
|
| 80 |
+
if _NAME.fullmatch(self.name) is None:
|
| 81 |
+
raise ConfigError(f"profile.name must match {_NAME.pattern!r}")
|
| 82 |
+
_integer(self.token_offset, "profile.token_offset")
|
| 83 |
+
_integer(self.token_budget, "profile.token_budget", positive=True)
|
| 84 |
+
if re.fullmatch(r"[0-9a-f]{64}", self.artifact_fingerprint) is None:
|
| 85 |
+
raise ConfigError(
|
| 86 |
+
"profile.artifact_fingerprint must be a SHA-256 hex digest"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
@classmethod
|
| 90 |
+
def from_dict(cls, payload: object, *, path: str) -> ProfileSpec:
|
| 91 |
+
values = _mapping(payload, path)
|
| 92 |
+
allowed = {"name", "token_offset", "token_budget", "artifact_fingerprint"}
|
| 93 |
+
_reject_unknown(values, allowed, path)
|
| 94 |
+
return cls(
|
| 95 |
+
name=str(_required(values, "name", path)),
|
| 96 |
+
token_offset=_integer(
|
| 97 |
+
_required(values, "token_offset", path), f"{path}.token_offset"
|
| 98 |
+
),
|
| 99 |
+
token_budget=_integer(
|
| 100 |
+
_required(values, "token_budget", path),
|
| 101 |
+
f"{path}.token_budget",
|
| 102 |
+
positive=True,
|
| 103 |
+
),
|
| 104 |
+
artifact_fingerprint=str(_required(values, "artifact_fingerprint", path)),
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
def to_dict(self) -> dict[str, object]:
|
| 108 |
+
return asdict(self)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@dataclass(frozen=True)
|
| 112 |
+
class _ProfileRuntime:
|
| 113 |
+
spec: ProfileSpec
|
| 114 |
+
processor: ActionCodec2Processor
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@dataclass(frozen=True)
|
| 118 |
+
class _Segment:
|
| 119 |
+
component: ComponentSpec
|
| 120 |
+
profile: _ProfileRuntime
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class RoutedActionCodec2Grammar:
|
| 124 |
+
"""Constrained grammar over a preset's ordered, namespaced segments."""
|
| 125 |
+
|
| 126 |
+
def __init__(
|
| 127 |
+
self, router: ActionCodec2Router, *, preset: str, horizon: int
|
| 128 |
+
) -> None:
|
| 129 |
+
horizon = _integer(horizon, "horizon", positive=True)
|
| 130 |
+
self.segments = router._segments(router.preset(preset))
|
| 131 |
+
self.vocab_size = router.vocab_size
|
| 132 |
+
self._grammars = tuple(
|
| 133 |
+
segment.profile.processor.grammar(
|
| 134 |
+
horizon, active_dims=segment.component.active_dims
|
| 135 |
+
)
|
| 136 |
+
for segment in self.segments
|
| 137 |
+
)
|
| 138 |
+
initial = self._normalize_state(0, self._grammars[0]._initial_state())
|
| 139 |
+
self._initial_state = initial
|
| 140 |
+
self._state_cache = BoundedCache(256)
|
| 141 |
+
self._state_cache[()] = initial
|
| 142 |
+
|
| 143 |
+
def _normalize_state(
|
| 144 |
+
self, segment_index: int, state: tuple[int, ...]
|
| 145 |
+
) -> tuple[int, tuple[int, ...]]:
|
| 146 |
+
while segment_index < len(self.segments) and self._grammars[
|
| 147 |
+
segment_index
|
| 148 |
+
]._state_is_complete(state):
|
| 149 |
+
segment_index += 1
|
| 150 |
+
if segment_index < len(self.segments):
|
| 151 |
+
state = self._grammars[segment_index]._initial_state()
|
| 152 |
+
return segment_index, state
|
| 153 |
+
|
| 154 |
+
def _advance_routed_state(
|
| 155 |
+
self,
|
| 156 |
+
routed_state: tuple[int, tuple[int, ...]],
|
| 157 |
+
token: int,
|
| 158 |
+
) -> tuple[int, tuple[int, ...]] | None:
|
| 159 |
+
segment_index, state = routed_state
|
| 160 |
+
if segment_index == len(self.segments):
|
| 161 |
+
return None
|
| 162 |
+
segment = self.segments[segment_index]
|
| 163 |
+
local_token = token - segment.profile.spec.token_offset
|
| 164 |
+
if not 0 <= local_token < segment.profile.processor.vocab_size:
|
| 165 |
+
return None
|
| 166 |
+
next_state = self._grammars[segment_index]._advance(state, local_token)
|
| 167 |
+
if next_state is None:
|
| 168 |
+
return None
|
| 169 |
+
return self._normalize_state(segment_index, next_state)
|
| 170 |
+
|
| 171 |
+
def _consume(self, tokens: Sequence[int]) -> tuple[int, tuple[int, ...]] | None:
|
| 172 |
+
checked: list[int] = []
|
| 173 |
+
for raw_token in tokens:
|
| 174 |
+
if isinstance(raw_token, bool) or not isinstance(
|
| 175 |
+
raw_token, (int, np.integer)
|
| 176 |
+
):
|
| 177 |
+
return None
|
| 178 |
+
checked.append(int(raw_token))
|
| 179 |
+
key = tuple(checked)
|
| 180 |
+
if not key:
|
| 181 |
+
return self._initial_state
|
| 182 |
+
missing = object()
|
| 183 |
+
cached = self._state_cache.get(key, missing)
|
| 184 |
+
if cached is not missing:
|
| 185 |
+
return cached
|
| 186 |
+
parent = key[:-1]
|
| 187 |
+
state = self._state_cache.get(parent, missing)
|
| 188 |
+
if state is missing:
|
| 189 |
+
state = self._initial_state
|
| 190 |
+
for token in parent:
|
| 191 |
+
if state is None:
|
| 192 |
+
break
|
| 193 |
+
state = self._advance_routed_state(state, token)
|
| 194 |
+
self._state_cache[parent] = state
|
| 195 |
+
if state is not None:
|
| 196 |
+
state = self._advance_routed_state(state, key[-1])
|
| 197 |
+
self._state_cache[key] = state
|
| 198 |
+
return state
|
| 199 |
+
|
| 200 |
+
def next_token_mask(self, tokens: Sequence[int]) -> np.ndarray:
|
| 201 |
+
"""Return a global namespace mask for the next preset token."""
|
| 202 |
+
state = self._consume(tokens)
|
| 203 |
+
if state is None:
|
| 204 |
+
raise ValueError("token prefix is not legal for this routed preset")
|
| 205 |
+
segment_index, local_state = state
|
| 206 |
+
output = np.zeros(self.vocab_size, dtype=bool)
|
| 207 |
+
if segment_index == len(self.segments):
|
| 208 |
+
return output
|
| 209 |
+
segment = self.segments[segment_index]
|
| 210 |
+
mask = self._grammars[segment_index]._next_token_mask_from_state(local_state)
|
| 211 |
+
start = segment.profile.spec.token_offset
|
| 212 |
+
output[start : start + len(mask)] = mask
|
| 213 |
+
return output
|
| 214 |
+
|
| 215 |
+
def is_complete(self, tokens: Sequence[int]) -> bool:
|
| 216 |
+
"""Whether the prefix completes every segment exactly once."""
|
| 217 |
+
state = self._consume(tokens)
|
| 218 |
+
return state is not None and state[0] == len(self.segments)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class ActionCodec2Router:
|
| 222 |
+
"""Route heterogeneous dataset states into physical token namespaces.
|
| 223 |
+
|
| 224 |
+
The router is fitted only from already-built physical processors. Presets
|
| 225 |
+
cannot alter their primitives, merge rules, or token meanings.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
profiles: Ordered namespace metadata and fitted processor pairs for
|
| 229 |
+
the shared joint and end-effector physical profiles.
|
| 230 |
+
presets: Action-space definitions mapping source tensor columns into
|
| 231 |
+
the physical profiles.
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
model_type = ROUTER_MODEL_TYPE
|
| 235 |
+
|
| 236 |
+
def __init__(
|
| 237 |
+
self,
|
| 238 |
+
profiles: Sequence[tuple[ProfileSpec, ActionCodec2Processor]],
|
| 239 |
+
presets: Sequence[PresetConfig],
|
| 240 |
+
) -> None:
|
| 241 |
+
runtimes: dict[str, _ProfileRuntime] = {}
|
| 242 |
+
expected_offset = 0
|
| 243 |
+
for spec, processor in profiles:
|
| 244 |
+
if spec.name in runtimes:
|
| 245 |
+
raise ConfigError(f"duplicate physical profile {spec.name!r}")
|
| 246 |
+
if spec.token_offset != expected_offset:
|
| 247 |
+
raise ConfigError(
|
| 248 |
+
"profile token namespaces must be contiguous and ordered"
|
| 249 |
+
)
|
| 250 |
+
if processor.vocab_size > spec.token_budget:
|
| 251 |
+
raise ConfigError(
|
| 252 |
+
f"profile {spec.name!r} uses {processor.vocab_size} tokens, "
|
| 253 |
+
f"exceeding budget {spec.token_budget}"
|
| 254 |
+
)
|
| 255 |
+
if processor.artifact_fingerprint != spec.artifact_fingerprint:
|
| 256 |
+
raise ConfigError(
|
| 257 |
+
f"profile {spec.name!r} artifact fingerprint does not match its processor"
|
| 258 |
+
)
|
| 259 |
+
if processor.config.set_bpe.allow_cross_component_merge:
|
| 260 |
+
raise ConfigError(
|
| 261 |
+
"allow_cross_component_merge=true is not supported by the v1 router; "
|
| 262 |
+
"a component-aware Set-BPE serializer and grammar are required"
|
| 263 |
+
)
|
| 264 |
+
runtimes[spec.name] = _ProfileRuntime(spec, processor)
|
| 265 |
+
expected_offset += spec.token_budget
|
| 266 |
+
if not runtimes or not set(runtimes) <= _PROFILES:
|
| 267 |
+
raise ConfigError(
|
| 268 |
+
"router requires joint and/or eef physical profiles"
|
| 269 |
+
)
|
| 270 |
+
for name, runtime in runtimes.items():
|
| 271 |
+
if runtime.processor.action_dim != _PROFILE_WIDTHS[name]:
|
| 272 |
+
raise ConfigError(
|
| 273 |
+
f"profile {name!r} must have width {_PROFILE_WIDTHS[name]}, "
|
| 274 |
+
f"got {runtime.processor.action_dim}"
|
| 275 |
+
)
|
| 276 |
+
self.profiles = MappingProxyType(runtimes)
|
| 277 |
+
self._source_directory = Path(__file__).resolve().parent.parent
|
| 278 |
+
self._grammar_cache = BoundedCache(8)
|
| 279 |
+
self._action_spaces: dict[str, PresetConfig] = {}
|
| 280 |
+
for preset in presets:
|
| 281 |
+
self.register_action_space(preset)
|
| 282 |
+
self.vocab_size = expected_offset
|
| 283 |
+
|
| 284 |
+
@property
|
| 285 |
+
def action_spaces(self) -> Mapping[str, PresetConfig]:
|
| 286 |
+
"""Read-only registered built-in and user-created action spaces."""
|
| 287 |
+
return MappingProxyType(self._action_spaces)
|
| 288 |
+
|
| 289 |
+
def __str__(self) -> str:
|
| 290 |
+
return self.describe_action_spaces()
|
| 291 |
+
|
| 292 |
+
def describe_action_spaces(
|
| 293 |
+
self, *, selected: str | None = None, action_dim: int | None = None
|
| 294 |
+
) -> str:
|
| 295 |
+
"""Explain all layouts, or source-width candidates, using this artifact's state requirements."""
|
| 296 |
+
second_order_dims = {
|
| 297 |
+
name: tuple(
|
| 298 |
+
dimension
|
| 299 |
+
for component in runtime.processor.config.physical.components.values()
|
| 300 |
+
if component.primitive_order == 2
|
| 301 |
+
for dimension in component.dims
|
| 302 |
+
)
|
| 303 |
+
for name, runtime in self.profiles.items()
|
| 304 |
+
}
|
| 305 |
+
return format_action_spaces(
|
| 306 |
+
self.action_spaces,
|
| 307 |
+
selected=selected,
|
| 308 |
+
action_dim=action_dim,
|
| 309 |
+
second_order_dims=second_order_dims,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
@classmethod
|
| 313 |
+
def from_processors(
|
| 314 |
+
cls,
|
| 315 |
+
processors: Mapping[str, ActionCodec2Processor],
|
| 316 |
+
*,
|
| 317 |
+
token_budgets: Mapping[str, int],
|
| 318 |
+
presets: Sequence[PresetConfig],
|
| 319 |
+
) -> ActionCodec2Router:
|
| 320 |
+
"""Assign stable contiguous namespaces in canonical joint/eef order."""
|
| 321 |
+
if set(processors) != set(token_budgets):
|
| 322 |
+
raise ConfigError("processors and token_budgets must share keys")
|
| 323 |
+
if set(processors) != _PROFILES:
|
| 324 |
+
raise ConfigError(
|
| 325 |
+
"processors must contain exactly the joint and eef profiles"
|
| 326 |
+
)
|
| 327 |
+
offset = 0
|
| 328 |
+
entries = []
|
| 329 |
+
for name in ("joint", "eef"):
|
| 330 |
+
processor = processors[name]
|
| 331 |
+
budget = _integer(
|
| 332 |
+
token_budgets[name], f"token_budgets.{name}", positive=True
|
| 333 |
+
)
|
| 334 |
+
spec = ProfileSpec(
|
| 335 |
+
name,
|
| 336 |
+
offset,
|
| 337 |
+
budget,
|
| 338 |
+
processor.artifact_fingerprint,
|
| 339 |
+
)
|
| 340 |
+
entries.append((spec, processor))
|
| 341 |
+
offset += budget
|
| 342 |
+
return cls(entries, presets)
|
| 343 |
+
|
| 344 |
+
def preset(self, name: str) -> PresetConfig:
|
| 345 |
+
"""Resolve one preset or fail with the available names."""
|
| 346 |
+
try:
|
| 347 |
+
return self._action_spaces[name]
|
| 348 |
+
except KeyError as error:
|
| 349 |
+
raise KeyError(
|
| 350 |
+
f"unknown preset {name!r}; available: {sorted(self._action_spaces)}"
|
| 351 |
+
) from error
|
| 352 |
+
|
| 353 |
+
def register_action_space(
|
| 354 |
+
self,
|
| 355 |
+
preset: PresetConfig | Mapping[str, object] | str | Path,
|
| 356 |
+
) -> PresetConfig:
|
| 357 |
+
"""Register a validated action-space config, mapping, or YAML file.
|
| 358 |
+
|
| 359 |
+
Registration changes only tensor interpretation. It cannot alter either
|
| 360 |
+
fitted physical vocabulary or any token ID.
|
| 361 |
+
"""
|
| 362 |
+
if isinstance(preset, Mapping):
|
| 363 |
+
preset = PresetConfig.from_dict(preset)
|
| 364 |
+
elif isinstance(preset, (str, Path)):
|
| 365 |
+
preset = PresetConfig.from_yaml(preset)
|
| 366 |
+
if not isinstance(preset, PresetConfig):
|
| 367 |
+
raise TypeError(
|
| 368 |
+
"register_action_space expects PresetConfig, a mapping, or a preset YAML path"
|
| 369 |
+
)
|
| 370 |
+
existing = self._action_spaces.get(preset.name)
|
| 371 |
+
if existing is not None and existing != preset:
|
| 372 |
+
raise ConfigError(
|
| 373 |
+
f"action-space name {preset.name!r} is already registered differently"
|
| 374 |
+
)
|
| 375 |
+
self._action_spaces[preset.name] = preset
|
| 376 |
+
return preset
|
| 377 |
+
|
| 378 |
+
def _segments(self, preset: PresetConfig) -> tuple[_Segment, ...]:
|
| 379 |
+
return tuple(
|
| 380 |
+
_Segment(component, self.profiles[component.space])
|
| 381 |
+
for component in preset.components
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
def encode_batch(
|
| 385 |
+
self,
|
| 386 |
+
inputs: np.ndarray,
|
| 387 |
+
*,
|
| 388 |
+
preset: str,
|
| 389 |
+
fps: float | None = None,
|
| 390 |
+
current_state: np.ndarray | None = None,
|
| 391 |
+
previous_first_order: Mapping[str, np.ndarray] | None = None,
|
| 392 |
+
) -> list[list[int]]:
|
| 393 |
+
"""Encode one regular-rate ``[B,T,D]`` tensor at ``fps``.
|
| 394 |
+
|
| 395 |
+
Preset preprocessing and runtime encoding use the same canonical rate
|
| 396 |
+
adapter. The shared physical processors therefore only receive the
|
| 397 |
+
preset's target clock. For order-0 presets,
|
| 398 |
+
``current_state[B,D]`` is the state immediately before row zero. The
|
| 399 |
+
low-level router permits omitting it and then anchors at row zero; the
|
| 400 |
+
public frontend requires it for absolute action spaces so the first
|
| 401 |
+
transition cannot be silently discarded.
|
| 402 |
+
``previous_first_order`` maps component names to ``[B,D_active]``
|
| 403 |
+
reconstructed increments in canonical units at the preset's target
|
| 404 |
+
clock; active second-order components require an entry.
|
| 405 |
+
"""
|
| 406 |
+
preset_config = self.preset(preset)
|
| 407 |
+
self._validate_boundary_keys(previous_first_order, preset_config)
|
| 408 |
+
canonical = preset_config.canonicalize(
|
| 409 |
+
inputs, source_hz=fps, current_state=current_state
|
| 410 |
+
)
|
| 411 |
+
batch = len(next(iter(canonical.values())))
|
| 412 |
+
rows: list[list[int]] = [[] for _ in range(batch)]
|
| 413 |
+
for segment in self._segments(preset_config):
|
| 414 |
+
values = canonical[segment.component.name]
|
| 415 |
+
streams = segment.profile.processor.encode_batch(
|
| 416 |
+
np.stack([trajectory.values for trajectory in values]),
|
| 417 |
+
active_dims=segment.component.active_dims,
|
| 418 |
+
previous_first_order=None
|
| 419 |
+
if previous_first_order is None
|
| 420 |
+
else previous_first_order.get(segment.component.name),
|
| 421 |
+
)
|
| 422 |
+
offset = segment.profile.spec.token_offset
|
| 423 |
+
for row, stream in zip(rows, streams, strict=True):
|
| 424 |
+
row.extend(offset + token for token in stream)
|
| 425 |
+
return rows
|
| 426 |
+
|
| 427 |
+
def route_trajectories(
|
| 428 |
+
self,
|
| 429 |
+
inputs: np.ndarray,
|
| 430 |
+
*,
|
| 431 |
+
preset: str,
|
| 432 |
+
current_state: np.ndarray | None = None,
|
| 433 |
+
fps: float | None = None,
|
| 434 |
+
previous_first_order: Mapping[str, np.ndarray] | None = None,
|
| 435 |
+
) -> dict[str, list[ActiveTrajectory]]:
|
| 436 |
+
"""Expose canonical complete trajectories grouped by physical profile.
|
| 437 |
+
|
| 438 |
+
This is the fitting boundary: callers pool the returned trajectories
|
| 439 |
+
from many presets by profile name, balance datasets, then call
|
| 440 |
+
:meth:`ProfileTrainer.fit` once per profile.
|
| 441 |
+
``previous_first_order`` uses the same component-to-``[B,D_active]``
|
| 442 |
+
canonical boundary mapping as :meth:`encode_batch`.
|
| 443 |
+
"""
|
| 444 |
+
preset_config = self.preset(preset)
|
| 445 |
+
self._validate_boundary_keys(previous_first_order, preset_config)
|
| 446 |
+
canonical = preset_config.canonicalize(
|
| 447 |
+
inputs, current_state=current_state, source_hz=fps
|
| 448 |
+
)
|
| 449 |
+
output: dict[str, list[ActiveTrajectory]] = {}
|
| 450 |
+
for component in preset_config.components:
|
| 451 |
+
trajectories = canonical[component.name]
|
| 452 |
+
processor = self.profiles[component.space].processor
|
| 453 |
+
previous = (
|
| 454 |
+
None
|
| 455 |
+
if previous_first_order is None
|
| 456 |
+
else previous_first_order.get(component.name)
|
| 457 |
+
)
|
| 458 |
+
dense = processor._dense_previous_first_order(
|
| 459 |
+
previous, component.active_dims, len(trajectories)
|
| 460 |
+
)
|
| 461 |
+
rows = (
|
| 462 |
+
[None] * len(trajectories)
|
| 463 |
+
if dense is None
|
| 464 |
+
else np.take(dense, component.active_dims, axis=1)
|
| 465 |
+
)
|
| 466 |
+
output.setdefault(component.space, []).extend(
|
| 467 |
+
ActiveTrajectory(t.values, t.active_dims, previous_first_order=p)
|
| 468 |
+
for t, p in zip(trajectories, rows, strict=True)
|
| 469 |
+
)
|
| 470 |
+
return output
|
| 471 |
+
|
| 472 |
+
def _validate_boundary_keys(
|
| 473 |
+
self,
|
| 474 |
+
previous_first_order: Mapping[str, np.ndarray] | None,
|
| 475 |
+
preset: PresetConfig,
|
| 476 |
+
) -> None:
|
| 477 |
+
if previous_first_order is None:
|
| 478 |
+
previous_first_order = {}
|
| 479 |
+
if not isinstance(previous_first_order, Mapping):
|
| 480 |
+
raise ValueError(
|
| 481 |
+
"previous_first_order must map component names to canonical [B,D_active] boundaries"
|
| 482 |
+
)
|
| 483 |
+
unknown = set(previous_first_order) - {c.name for c in preset.components}
|
| 484 |
+
if unknown:
|
| 485 |
+
raise ValueError(
|
| 486 |
+
f"unknown previous_first_order component(s): {sorted(unknown)}"
|
| 487 |
+
)
|
| 488 |
+
required = {
|
| 489 |
+
c.name: f"(B, {len(c.active_dims)})"
|
| 490 |
+
for c in preset.components
|
| 491 |
+
if any(
|
| 492 |
+
p.primitive_order == 2 and set(c.active_dims).intersection(p.dims)
|
| 493 |
+
for p in self.profiles[
|
| 494 |
+
c.space
|
| 495 |
+
].processor.config.physical.components.values()
|
| 496 |
+
)
|
| 497 |
+
}
|
| 498 |
+
missing = set(required) - previous_first_order.keys()
|
| 499 |
+
if missing:
|
| 500 |
+
raise ValueError(
|
| 501 |
+
"second-order tokenization requires previous_first_order with arrays "
|
| 502 |
+
f"{required}; missing {sorted(missing)}. Use canonical {preset.target_hz:g} Hz "
|
| 503 |
+
"reconstructed increments. codec.first_order_template(batch_size) explicitly "
|
| 504 |
+
"creates zeros; use zeros only if the preceding increment is known to be zero."
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
def final_first_order(
|
| 508 |
+
self,
|
| 509 |
+
streams: Sequence[Sequence[int]],
|
| 510 |
+
*,
|
| 511 |
+
preset: str,
|
| 512 |
+
previous_first_order: Mapping[str, np.ndarray],
|
| 513 |
+
executed_steps: int | None = None,
|
| 514 |
+
) -> dict[str, np.ndarray]:
|
| 515 |
+
"""Return component ``[B,D_active]`` boundaries at the canonical clock.
|
| 516 |
+
|
| 517 |
+
``executed_steps`` counts codec steps, before output resampling. EEF
|
| 518 |
+
motion uses local Cartesian displacements and rotation vectors in the
|
| 519 |
+
profile's multiplication convention. Gripper slots carry no state.
|
| 520 |
+
"""
|
| 521 |
+
preset_config = self.preset(preset)
|
| 522 |
+
if previous_first_order is None:
|
| 523 |
+
raise ValueError("final_first_order requires previous_first_order")
|
| 524 |
+
self._validate_boundary_keys(previous_first_order, preset_config)
|
| 525 |
+
segments = self._segments(preset_config)
|
| 526 |
+
steps = self._batch_steps(streams, segments)
|
| 527 |
+
split = [self._split_stream(stream, segments, steps) for stream in streams]
|
| 528 |
+
output = {}
|
| 529 |
+
for index, segment in enumerate(segments):
|
| 530 |
+
processor = segment.profile.processor
|
| 531 |
+
if not processor.requires_previous_first_order:
|
| 532 |
+
continue
|
| 533 |
+
output[segment.component.name] = processor.final_first_order(
|
| 534 |
+
[row[index] for row in split],
|
| 535 |
+
previous_first_order=previous_first_order.get(segment.component.name),
|
| 536 |
+
active_dims=segment.component.active_dims,
|
| 537 |
+
executed_steps=executed_steps,
|
| 538 |
+
)
|
| 539 |
+
return output
|
| 540 |
+
|
| 541 |
+
def _split_stream(
|
| 542 |
+
self, tokens: Sequence[int], segments: Sequence[_Segment], steps: int
|
| 543 |
+
) -> list[list[int]]:
|
| 544 |
+
cursor = 0
|
| 545 |
+
output = []
|
| 546 |
+
for segment in segments:
|
| 547 |
+
grammar = segment.profile.processor.grammar(
|
| 548 |
+
steps, active_dims=segment.component.active_dims
|
| 549 |
+
)
|
| 550 |
+
state = grammar._initial_state()
|
| 551 |
+
local: list[int] = []
|
| 552 |
+
while cursor < len(tokens) and not grammar._state_is_complete(state):
|
| 553 |
+
raw_token = tokens[cursor]
|
| 554 |
+
if isinstance(raw_token, bool) or not isinstance(
|
| 555 |
+
raw_token, (int, np.integer)
|
| 556 |
+
):
|
| 557 |
+
raise ValueError(f"tokens[{cursor}] must be an integer")
|
| 558 |
+
token = int(raw_token) - segment.profile.spec.token_offset
|
| 559 |
+
if not 0 <= token < segment.profile.processor.vocab_size:
|
| 560 |
+
raise ValueError(
|
| 561 |
+
"token belongs to the wrong routed profile namespace"
|
| 562 |
+
)
|
| 563 |
+
next_state = grammar._advance(state, token)
|
| 564 |
+
if next_state is None:
|
| 565 |
+
raise ValueError(
|
| 566 |
+
f"Action token stream has invalid coverage for component {segment.component.name!r}. Constrain generation with codec.grammar(H, fps=...).next_token_mask(prefix)."
|
| 567 |
+
)
|
| 568 |
+
state = next_state
|
| 569 |
+
local.append(token)
|
| 570 |
+
cursor += 1
|
| 571 |
+
if not grammar._state_is_complete(state):
|
| 572 |
+
raise ValueError(
|
| 573 |
+
f"Incomplete action token stream for component {segment.component.name!r}. Check codec.grammar(H, fps=...).is_complete(tokens); discard or regenerate this row."
|
| 574 |
+
)
|
| 575 |
+
output.append(local)
|
| 576 |
+
if cursor != len(tokens):
|
| 577 |
+
raise ValueError(
|
| 578 |
+
"Action token stream has extra tokens after complete coverage; remove model prompt/EOS/padding before codec.decode()."
|
| 579 |
+
)
|
| 580 |
+
return output
|
| 581 |
+
|
| 582 |
+
def _token_cell_count(self, raw_token: object, *, index: int) -> int:
|
| 583 |
+
"""Return the number of primitive cells represented by one routed token."""
|
| 584 |
+
if isinstance(raw_token, bool) or not isinstance(raw_token, (int, np.integer)):
|
| 585 |
+
raise ValueError(f"tokens[{index}] must be an integer")
|
| 586 |
+
token = int(raw_token)
|
| 587 |
+
for runtime in self.profiles.values():
|
| 588 |
+
local = token - runtime.spec.token_offset
|
| 589 |
+
if 0 <= local < runtime.processor.vocab_size:
|
| 590 |
+
return int(runtime.processor.serializer.token_cell_counts[local])
|
| 591 |
+
raise ValueError(f"tokens[{index}] is outside the routed profile vocabularies")
|
| 592 |
+
|
| 593 |
+
def _decoded_steps(
|
| 594 |
+
self, tokens: Sequence[int], segments: Sequence[_Segment]
|
| 595 |
+
) -> int:
|
| 596 |
+
"""Derive sequence length from the primitive-cell coverage in a routed stream."""
|
| 597 |
+
if not tokens:
|
| 598 |
+
raise ValueError("token stream must not be empty")
|
| 599 |
+
cells_per_step = sum(len(segment.component.active_dims) for segment in segments)
|
| 600 |
+
represented_cells = sum(
|
| 601 |
+
self._token_cell_count(token, index=index)
|
| 602 |
+
for index, token in enumerate(tokens)
|
| 603 |
+
)
|
| 604 |
+
steps, remainder = divmod(represented_cells, cells_per_step)
|
| 605 |
+
if steps < 1 or remainder:
|
| 606 |
+
raise ValueError(
|
| 607 |
+
"Incomplete action token stream: "
|
| 608 |
+
f"{cells_per_step - remainder} more coordinate values are needed to finish a step. "
|
| 609 |
+
"Constrain generation with codec.grammar(H, fps=...).next_token_mask(prefix) "
|
| 610 |
+
"and check is_complete(tokens) for the intended H before decoding. "
|
| 611 |
+
"Use codec.batch_decode(..., errors='return') to isolate invalid rows; "
|
| 612 |
+
"do not pad truncated tokens with zeros."
|
| 613 |
+
)
|
| 614 |
+
return steps
|
| 615 |
+
|
| 616 |
+
def _batch_steps(
|
| 617 |
+
self,
|
| 618 |
+
streams: Sequence[Sequence[int]],
|
| 619 |
+
segments: Sequence[_Segment],
|
| 620 |
+
) -> int:
|
| 621 |
+
"""Derive the common decoded length of a dense output batch."""
|
| 622 |
+
if not streams:
|
| 623 |
+
raise ValueError("streams must not be empty")
|
| 624 |
+
lengths = [self._decoded_steps(stream, segments) for stream in streams]
|
| 625 |
+
if len(set(lengths)) != 1:
|
| 626 |
+
raise ValueError(
|
| 627 |
+
f"all token streams must decode to the same length, got {lengths}; use codec.batch_decode(...) for independent variable-length outputs"
|
| 628 |
+
)
|
| 629 |
+
return lengths[0]
|
| 630 |
+
|
| 631 |
+
def decode_batch(
|
| 632 |
+
self,
|
| 633 |
+
streams: Sequence[Sequence[int]],
|
| 634 |
+
*,
|
| 635 |
+
preset: str,
|
| 636 |
+
fps: float | None = None,
|
| 637 |
+
current_state: np.ndarray | None = None,
|
| 638 |
+
previous_first_order: Mapping[str, np.ndarray] | None = None,
|
| 639 |
+
) -> dict[str, np.ndarray]:
|
| 640 |
+
"""Decode rows to deltas, or absolute components with ``current_state``.
|
| 641 |
+
|
| 642 |
+
Args:
|
| 643 |
+
streams: One namespaced token stream per batch item.
|
| 644 |
+
preset: Registered tensor/component interpretation.
|
| 645 |
+
fps: Requested external output rate; defaults to the preset's
|
| 646 |
+
``source_hz``. The output row count is the canonical rate
|
| 647 |
+
conversion of the token-determined codec-rate path; no original
|
| 648 |
+
pre-downsampling row count is inferred.
|
| 649 |
+
current_state: Optional ``(B,D)`` preset-layout state immediately
|
| 650 |
+
before row zero. When supplied, decoded local paths are attached
|
| 651 |
+
to it and returned as order-0 values. When omitted, the return
|
| 652 |
+
value is per-step order-1 motion. Gripper is always absolute.
|
| 653 |
+
previous_first_order: Component names mapped to ``(B,D_active)``
|
| 654 |
+
canonical reconstructed increments at the target clock, using
|
| 655 |
+
the same boundary as encode. Required for second-order motion.
|
| 656 |
+
"""
|
| 657 |
+
preset_config = self.preset(preset)
|
| 658 |
+
self._validate_boundary_keys(previous_first_order, preset_config)
|
| 659 |
+
segments = self._segments(preset_config)
|
| 660 |
+
internal_steps = self._batch_steps(streams, segments)
|
| 661 |
+
states = _current_state_rows(
|
| 662 |
+
current_state, count=len(streams), input_dim=preset_config.input_dim
|
| 663 |
+
)
|
| 664 |
+
output_fps = preset_config.source_hz if fps is None else fps
|
| 665 |
+
output_steps = resampled_horizon(
|
| 666 |
+
internal_steps,
|
| 667 |
+
source_hz=preset_config.target_hz,
|
| 668 |
+
target_hz=output_fps,
|
| 669 |
+
)
|
| 670 |
+
output: dict[str, list[np.ndarray]] = {
|
| 671 |
+
component.name: [] for component in preset_config.components
|
| 672 |
+
}
|
| 673 |
+
split_streams = [
|
| 674 |
+
self._split_stream(stream, segments, internal_steps) for stream in streams
|
| 675 |
+
]
|
| 676 |
+
for segment_index, segment in enumerate(segments):
|
| 677 |
+
decoded_batch = segment.profile.processor.decode_batch(
|
| 678 |
+
[split[segment_index] for split in split_streams],
|
| 679 |
+
active_dims=segment.component.active_dims,
|
| 680 |
+
previous_first_order=None
|
| 681 |
+
if previous_first_order is None
|
| 682 |
+
else previous_first_order.get(segment.component.name),
|
| 683 |
+
)
|
| 684 |
+
for decoded, state in zip(decoded_batch, states, strict=True):
|
| 685 |
+
restored = resample_canonical_trajectory(
|
| 686 |
+
ActiveTrajectory(decoded, segment.component.active_dims),
|
| 687 |
+
segment.component,
|
| 688 |
+
source_hz=preset_config.target_hz,
|
| 689 |
+
target_hz=output_fps,
|
| 690 |
+
target_length=output_steps,
|
| 691 |
+
method=preset_config.interpolation,
|
| 692 |
+
)
|
| 693 |
+
output[segment.component.name].append(
|
| 694 |
+
externalize_canonical_trajectory(
|
| 695 |
+
restored, segment.component, current_state=state
|
| 696 |
+
)
|
| 697 |
+
)
|
| 698 |
+
return {name: np.stack(values) for name, values in output.items()}
|
| 699 |
+
|
| 700 |
+
def grammar(self, *, preset: str, horizon: int) -> RoutedActionCodec2Grammar:
|
| 701 |
+
"""Build the global constrained grammar for one preset and target horizon."""
|
| 702 |
+
horizon = _integer(horizon, "horizon", positive=True)
|
| 703 |
+
self.preset(preset)
|
| 704 |
+
key = (preset, horizon)
|
| 705 |
+
grammar = self._grammar_cache.get(key)
|
| 706 |
+
if grammar is None:
|
| 707 |
+
grammar = RoutedActionCodec2Grammar(self, preset=preset, horizon=horizon)
|
| 708 |
+
self._grammar_cache[key] = grammar
|
| 709 |
+
return grammar
|
| 710 |
+
|
| 711 |
+
@property
|
| 712 |
+
def new_action_tokens(self) -> list[str]:
|
| 713 |
+
"""Stable physical token names, including reserved profile budget slots."""
|
| 714 |
+
return [
|
| 715 |
+
f"<action_{name}_{index:04d}>"
|
| 716 |
+
for name, runtime in self.profiles.items()
|
| 717 |
+
for index in range(runtime.spec.token_budget)
|
| 718 |
+
]
|
| 719 |
+
|
| 720 |
+
def save_pretrained(
|
| 721 |
+
self, directory: str | Path, *, include_code: bool = True
|
| 722 |
+
) -> list[str]:
|
| 723 |
+
"""Save one manifest plus isolated physical processor subdirectories."""
|
| 724 |
+
root = Path(directory)
|
| 725 |
+
files = []
|
| 726 |
+
for runtime in self.profiles.values():
|
| 727 |
+
files.extend(
|
| 728 |
+
runtime.processor.save_pretrained(root / "profiles" / runtime.spec.name)
|
| 729 |
+
)
|
| 730 |
+
manifest = {
|
| 731 |
+
"model_type": self.model_type,
|
| 732 |
+
"config_version": ROUTER_CONFIG_VERSION,
|
| 733 |
+
"profiles": [runtime.spec.to_dict() for runtime in self.profiles.values()],
|
| 734 |
+
"presets": [preset.to_dict() for preset in self.action_spaces.values()],
|
| 735 |
+
}
|
| 736 |
+
path = root / ROUTER_CONFIG_NAME
|
| 737 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 738 |
+
path.write_text(yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8")
|
| 739 |
+
metadata = save_huggingface_metadata(
|
| 740 |
+
root,
|
| 741 |
+
model_type=self.model_type,
|
| 742 |
+
processor_class=type(self).__name__,
|
| 743 |
+
**(
|
| 744 |
+
{"auto_map": {"AutoProcessor": "processing_actioncodec2.ActionCodec2"}}
|
| 745 |
+
if include_code
|
| 746 |
+
else {}
|
| 747 |
+
),
|
| 748 |
+
)
|
| 749 |
+
card = save_model_card(root, str(self))
|
| 750 |
+
if include_code:
|
| 751 |
+
from ..integration.exporting import ArtifactExporter
|
| 752 |
+
|
| 753 |
+
files.extend(
|
| 754 |
+
ArtifactExporter(root, source_directory=self._source_directory).export()
|
| 755 |
+
)
|
| 756 |
+
return [str(path), str(card), *map(str, metadata), *files]
|
| 757 |
+
|
| 758 |
+
@classmethod
|
| 759 |
+
def from_pretrained(
|
| 760 |
+
cls,
|
| 761 |
+
pretrained_model_name_or_path: str | Path,
|
| 762 |
+
**kwargs: object,
|
| 763 |
+
) -> ActionCodec2Router | ActionCodec2:
|
| 764 |
+
"""Load a router, or bind its public processor when given ``action_space``.
|
| 765 |
+
|
| 766 |
+
This also lets ``AutoProcessor`` bind an existing router artifact in
|
| 767 |
+
one call. Without ``action_space``, the result remains a router.
|
| 768 |
+
"""
|
| 769 |
+
if "action_space" in kwargs:
|
| 770 |
+
from .processing_actioncodec2 import ActionCodec2
|
| 771 |
+
|
| 772 |
+
return ActionCodec2.from_pretrained(pretrained_model_name_or_path, **kwargs)
|
| 773 |
+
root = resolve_pretrained_directory(
|
| 774 |
+
pretrained_model_name_or_path,
|
| 775 |
+
allow_patterns=ROUTER_ALLOW_PATTERNS,
|
| 776 |
+
**kwargs,
|
| 777 |
+
)
|
| 778 |
+
path = root / ROUTER_CONFIG_NAME
|
| 779 |
+
payload = _mapping(
|
| 780 |
+
yaml.load(path.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader),
|
| 781 |
+
"router",
|
| 782 |
+
)
|
| 783 |
+
_reject_unknown(
|
| 784 |
+
payload, {"model_type", "config_version", "profiles", "presets"}, "router"
|
| 785 |
+
)
|
| 786 |
+
if payload.get("model_type") != ROUTER_MODEL_TYPE:
|
| 787 |
+
raise ConfigError(f"router.model_type must be {ROUTER_MODEL_TYPE!r}")
|
| 788 |
+
if payload.get("config_version") != ROUTER_CONFIG_VERSION:
|
| 789 |
+
raise ConfigError(
|
| 790 |
+
f"unsupported router config_version {payload.get('config_version')!r}"
|
| 791 |
+
)
|
| 792 |
+
raw_profiles = payload.get("profiles")
|
| 793 |
+
raw_presets = payload.get("presets")
|
| 794 |
+
if not isinstance(raw_profiles, list) or not isinstance(raw_presets, list):
|
| 795 |
+
raise ConfigError("router.profiles and router.presets must be lists")
|
| 796 |
+
profiles = []
|
| 797 |
+
for index, raw in enumerate(raw_profiles):
|
| 798 |
+
spec = ProfileSpec.from_dict(raw, path=f"router.profiles[{index}]")
|
| 799 |
+
processor = ActionCodec2Processor.from_pretrained(
|
| 800 |
+
root / "profiles" / spec.name
|
| 801 |
+
)
|
| 802 |
+
profiles.append((spec, processor))
|
| 803 |
+
presets = [
|
| 804 |
+
PresetConfig.from_dict(raw, path=f"router.presets[{index}]")
|
| 805 |
+
for index, raw in enumerate(raw_presets)
|
| 806 |
+
]
|
| 807 |
+
router = cls(profiles, presets)
|
| 808 |
+
router._source_directory = root
|
| 809 |
+
return router
|