multimodalart HF Staff commited on
Commit
ab92892
·
verified ·
1 Parent(s): d0dac5c

Initial Hi3D v1.0 demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. AUTHORS +26 -0
  3. LICENSE +223 -0
  4. NOTICE +49 -0
  5. README.md +28 -7
  6. THIRD_PARTY_LICENSES.md +474 -0
  7. app.py +240 -0
  8. examples/typical_building_castle.png +3 -0
  9. examples/typical_creature_robot_dinosour.png +3 -0
  10. examples/typical_humanoid_mech.png +3 -0
  11. examples/typical_misc_phonograph.png +3 -0
  12. examples/typical_vehicle_excavator.png +3 -0
  13. requirements.txt +24 -0
  14. sparc3d/__init__.py +31 -0
  15. sparc3d/models/__init__.py +1 -0
  16. sparc3d/models/autoencoders/__init__.py +11 -0
  17. sparc3d/models/autoencoders/blocks.py +575 -0
  18. sparc3d/models/autoencoders/config.py +151 -0
  19. sparc3d/models/autoencoders/ptv3.py +118 -0
  20. sparc3d/models/autoencoders/sparconv_vae.py +63 -0
  21. sparc3d/models/conditioners/__init__.py +5 -0
  22. sparc3d/models/conditioners/image.py +169 -0
  23. sparc3d/models/transformers/__init__.py +6 -0
  24. sparc3d/models/transformers/config.py +195 -0
  25. sparc3d/models/transformers/sparc3d_transformer.py +280 -0
  26. sparc3d/nn/__init__.py +5 -0
  27. sparc3d/nn/norm.py +17 -0
  28. sparc3d/nn/rope.py +93 -0
  29. sparc3d/nn/sparse/__init__.py +14 -0
  30. sparc3d/nn/sparse/attention/__init__.py +5 -0
  31. sparc3d/nn/sparse/attention/full_attn.py +77 -0
  32. sparc3d/nn/sparse/attention/modules.py +123 -0
  33. sparc3d/nn/sparse/basic.py +185 -0
  34. sparc3d/nn/sparse/conv.py +88 -0
  35. sparc3d/nn/sparse/linear.py +18 -0
  36. sparc3d/nn/sparse/nonlinearity.py +18 -0
  37. sparc3d/nn/sparse/spatial.py +125 -0
  38. sparc3d/nn/sparse/transformer/__init__.py +5 -0
  39. sparc3d/nn/sparse/transformer/blocks.py +34 -0
  40. sparc3d/nn/sparse/transformer/modulated.py +65 -0
  41. sparc3d/nn/utils.py +29 -0
  42. sparc3d/ops/__init__.py +8 -0
  43. sparc3d/ops/_triton.py +21 -0
  44. sparc3d/ops/flexicubes/LICENSE.txt +202 -0
  45. sparc3d/ops/flexicubes/__init__.py +12 -0
  46. sparc3d/ops/flexicubes/flexicubes.py +349 -0
  47. sparc3d/ops/flexicubes/tables.py +546 -0
  48. sparc3d/ops/sparse_backend.py +288 -0
  49. sparc3d/ops/triton/__init__.py +7 -0
  50. sparc3d/ops/triton/voxelize.py +302 -0
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/typical_building_castle.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/typical_creature_robot_dinosour.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/typical_humanoid_mech.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/typical_misc_phonograph.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/typical_vehicle_excavator.png filter=lfs diff=lfs merge=lfs -text
AUTHORS ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SPARC3D authors and contributors
2
+ ================================
3
+
4
+ Sparc3D: Sparse Representation and Construction for High-Resolution 3D Shapes
5
+ Modeling — https://arxiv.org/abs/2505.14521
6
+
7
+ Paper authors
8
+ -------------
9
+
10
+ Zhihao Li
11
+ Yufei Wang
12
+ Heliang Zheng
13
+ Yihao Luo
14
+ Bihan Wen
15
+
16
+ Component credits
17
+ -----------------
18
+
19
+ Named here because the credit belongs with the code, not only with the paper.
20
+
21
+ Zhihao Li GPU surface voxelization in sparc3d/ops, originally written as
22
+ the pytorch-voxelization extension and implemented as Triton
23
+ kernels for this release.
24
+
25
+ Third-party code redistributed with this package is credited separately in
26
+ NOTICE and THIRD_PARTY_LICENSES.md.
LICENSE ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hi3D Open License Agreement Version 1.0
2
+
3
+ | Item | Details |
4
+ | --- | --- |
5
+ | Licensor | OCEANO HK HOLDING LIMITED |
6
+ | Licensed Project | Hi3D V1.0 |
7
+ | Version | Version 1.0 |
8
+ | Date | October 22, 2026 |
9
+
10
+
11
+
12
+ IMPORTANT NOTICE: By downloading, accessing, using, reproducing, modifying, distributing, displaying or otherwise exercising any rights in the Hi3D Materials, you acknowledge that you have read, understood and agreed to this Agreement. If you do not agree to this Agreement, do not use the Hi3D Materials.
13
+
14
+ # 1 Definitions
15
+
16
+ For purposes of this Agreement, the following terms have the meanings set out below.
17
+
18
+ ## 1.1 Agreement
19
+
20
+ "Agreement" means this Hi3D Open License Agreement Version 1.0 issued with the applicable version of the Hi3D Materials, including Schedule 1, Acceptable Use Policy, and Schedule 2, Licensed Asset Schedule.
21
+
22
+ ## 1.2 Licensor
23
+
24
+ "Licensor," "we," "us" or "our" means OCEANO HK HOLDING LIMITED, solely to the extent that it owns or is authorized to license the relevant rights in the Hi3D Materials.
25
+
26
+ ## 1.3 You
27
+
28
+ "You" means the individual or legal entity exercising rights under this Agreement. A "legal entity" includes the entity acting hereunder and any entity that controls, is controlled by, or is under common control with it. "Control" means direct or indirect ownership of more than fifty percent of the voting interests, or the power to direct management.
29
+
30
+ ## 1.4 Hi3D Materials
31
+
32
+ "Hi3D Materials" means the source code, model architecture, model weights and parameters, inference code, documentation and other materials that are owned by or licensable by the Licensor and expressly identified in the specific release listed in Schedule 2 as being subject to this Agreement. An asset is not automatically included in this Agreement merely because it has a similar name or purpose or is stored in the same repository, unless it has actually been released and is listed in Schedule 2.
33
+
34
+ ## 1.5 Excluded Materials
35
+
36
+ "Excluded Materials" means third-party software, models, weights and their outputs; training, validation and test datasets; sample media, fonts and other third-party assets; first-stage systems or server-side code not listed in Schedule 2; trademarks and brand assets except as expressly permitted under Section 8; and any other items expressly subject to separate terms. Excluded Materials are not licensed under this Agreement. Sample code calling a third-party model does not alter the licensing status of that third-party model or its outputs.
37
+
38
+ ## 1.6 Model Derivative
39
+
40
+ "Model Derivative" means another machine-learning model created by modifying, fine-tuning, adapting or quantizing all or a substantial part of the Hi3D model weights or parameters, or otherwise based on or incorporating the foregoing. Standalone Outputs, including generated 3D meshes and model files and finished works created by editing, converting or combining such files, do not become Model Derivatives merely because they were generated or post-processed; however, any portion of a finished work that actually contains Hi3D code, weights or parameters remains subject to this Agreement according to its nature.
41
+
42
+ ## 1.7 Output
43
+
44
+ "Output" means content or information generated by running the Hi3D Materials or a Model Derivative, including generated 3D geometry, meshes and exported 3D files. Any third-party asset actually contained in an Output remains subject to the applicable third-party rights or separate license.
45
+
46
+ ## 1.8 Commercial Product or Service
47
+
48
+ "Commercial Product or Service" means any product, service or hosted functionality that uses or contains the Hi3D Materials or a Model Derivative and is offered for commercial advantage, monetary consideration, revenue generation or other material commercial benefit.
49
+
50
+ ## 1.9 Related Product or Service
51
+
52
+ "Related Product or Service" means a product or service that uses or contains the Hi3D Materials or a Model Derivative. Substantially similar services split among different brands, applications, interfaces, domain names or affiliated entities to evade the Scale Threshold must be aggregated.
53
+
54
+ ## 1.10 Monthly Active Users
55
+
56
+ "Monthly Active Users" or "MAU" means the number of unique natural persons who actively use a Related Product or Service during a calendar month.
57
+
58
+ ## 1.11 First Stage
59
+
60
+ "First Stage" means coarse-mesh generation that generates the basic three-dimensional shape of an object from a reference image, determines its principal structure and approximate contours, and outputs a coarse mesh for subsequent processing.
61
+
62
+ ## 1.12 Second Stage
63
+
64
+ "Second Stage" means geometry refinement that takes a reference image and the coarse mesh produced by the First Stage as inputs, progressively refines the three-dimensional shape, and generates higher-resolution three-dimensional geometry and mesh files.
65
+
66
+ # 2 License Grant
67
+
68
+ Subject to your continuing compliance with this Agreement, and solely with respect to rights in the Hi3D Materials that the Licensor owns or is authorized to license, the Licensor grants you a worldwide, non-exclusive, royalty-free license to use, reproduce, modify, fine-tune and adapt the Hi3D Materials in source-code, object-code, weight or other applicable form; create Model Derivatives and other derivative works; publicly display and publicly perform them; and distribute, sublicense and sell the Hi3D Materials. You may also deploy the Hi3D Materials on servers, cloud platforms or other computing environments that you control or lawfully use, and provide third parties with hosted services based on the Hi3D Materials through websites, applications, application programming interfaces (APIs), software as a service (SaaS) or other remote-access methods. This license may be transferred only through sublicensing or distribution that complies with this Agreement.
69
+
70
+ ## 2.1 Commercial Use
71
+
72
+ Commercial use is permitted subject to Sections 5, 6, 7 and 8. No revenue threshold applies separately from Section 5.
73
+
74
+ ## 2.2 Training Other AI Models
75
+
76
+ To the extent the Licensor has the right to grant such permission, you may use the Hi3D Materials and their Outputs to train, evaluate, improve or develop other artificial-intelligence or machine-learning models, subject to applicable law, third-party rights and Schedule 1. This permission does not extend to coarse models, outputs or other third-party materials generated through third-party models, and does not replace their separate license terms. Another model is a Model Derivative only if it satisfies the definition in Section 1.6.
77
+
78
+ # 3 Patent License
79
+
80
+ Subject to your compliance with this Agreement, and solely with respect to patent claims that the Licensor owns or is authorized to license and that are necessarily infringed by implementation of the Hi3D Materials, the Licensor grants you a perpetual, worldwide, non-exclusive, royalty-free patent license to make, have made, use, offer for sale, sell, import and otherwise transfer the Hi3D Materials. A patent license from any other contributor arises only if that contributor has the right to grant it and expressly agrees to provide it under this Agreement.
81
+
82
+ ## 3.1 Patent Litigation
83
+
84
+ If you institute patent litigation against the Licensor or any contributor alleging that the Hi3D Materials or a contribution directly or contributorily infringes a patent, all patent licenses granted to you under this Agreement for the Hi3D Materials terminate as of the date the claim is filed.
85
+
86
+ # 4 Distribution and Sublicensing Conditions
87
+
88
+ You may distribute the Hi3D Materials and Model Derivatives, including as part of a proprietary or closed-source product, only if you comply with all of the following conditions. Merely reproducing, editing, displaying, selling or distributing Outputs that do not contain Hi3D Materials or Model Derivatives is not distribution under this Section, and does not by itself require those Outputs to be accompanied by this Agreement, a NOTICE file or a modification notice.
89
+
90
+ - Provide each recipient of files containing the Hi3D Materials or a Model Derivative with the full text of this Agreement upon delivery, or a link that provides continuous, free access to the full text of the corresponding version of this Agreement.
91
+ - Retain all applicable copyright, patent and attribution notices contained in the Hi3D Materials.
92
+ - Prominently identify the files, weights or materials that you modified.
93
+ - When distributing files containing the Hi3D Materials or a Model Derivative, include a readable NOTICE file containing the notice specified in Section 8.2 and all applicable original third-party notices and license requirements.
94
+ - Ensure that your terms do not restrict recipients from exercising the rights granted under this Agreement in unmodified portions of the Hi3D Materials, and pass the Acceptable Use Policy downstream as an enforceable condition.
95
+
96
+ ## 4.1 Licensing of Model Derivatives
97
+
98
+ You may apply different terms, including proprietary terms, to your own modifications and to a Model Derivative as a whole; however, the portions that actually reproduce or embed Hi3D code, weights or parameters remain subject to this Agreement, and the applicable attribution, branding, scale and use conditions continue to apply. A finished 3D work containing only Outputs and not the foregoing Hi3D Materials is not subject to this subsection's conditions for Model Derivatives merely because it was generated using Hi3D.
99
+
100
+ # 5 Scale Threshold and Separate License
101
+
102
+ The Scale Threshold is triggered if one or more Related Products or Services exceed 200,000 Monthly Active Users in the most recent complete calendar month or generate monthly revenue of at least USD 1,000,000. Except for the anti-circumvention aggregation rule in Section 1.9, only Related Products or Services are counted.
103
+
104
+ ## 5.1 Application
105
+
106
+ Within sixty days after first triggering the Scale Threshold, you must notify OCEANO HK HOLDING LIMITED and apply for a separate written license. The application must truthfully disclose the applicant, the affected products and services, the MAU calculation methodology, use cases, deployment regions, security measures and any other information reasonably requested by us.
107
+
108
+ ## 5.2 Transition Period
109
+
110
+ If you submit a complete, truthful and timely application and remain compliant, you may continue your existing use during the sixty-day application period and while both parties are actively negotiating in good faith; however, without our written consent, you may not materially expand the affected use beyond its scale and scope when the Scale Threshold was first exceeded.
111
+
112
+ ## 5.3 Decision on Separate License
113
+
114
+ A separate license may include fees, branding, reporting, audit, security, service-level or other commercial terms. We may decide, in our sole discretion, whether to grant or deny a separate license. No separate license is effective unless signed by an authorized representative of OCEANO HK HOLDING LIMITED or otherwise expressly confirmed by it in writing.
115
+
116
+ ## 5.4 Failure to Obtain a License
117
+
118
+ If you fail to apply within the required period, make a materially false statement in an application, or continue the affected use after written denial or after good-faith negotiations end without agreement, your right to continue above-threshold use terminates. Any continuing above-threshold use is unauthorized and constitutes a material breach.
119
+
120
+ # 6 Acceptable Use Policy
121
+
122
+ Your use of the Hi3D Materials and Model Derivatives, and your conduct in generating Outputs with them, must comply with Schedule 1, applicable law and third-party rights. This Agreement does not otherwise restrict the ordinary reproduction, editing, display, sale or distribution of Outputs that have been generated and do not contain Hi3D Materials or Model Derivatives; you remain responsible for complying with applicable law and any third-party rights contained in the Outputs. You may not knowingly assist another person in circumventing restrictions on the use of the Hi3D Materials or Model Derivatives.
123
+
124
+ ## 6.1 Policy Version
125
+
126
+ The version of Schedule 1 accompanying the Hi3D version you receive applies to that version. We may issue a revised policy for later versions. Material revisions do not apply retroactively to prior versions unless you expressly accept them.
127
+
128
+ # 7 Commercial Branding Display
129
+
130
+ Any entity integrating the Hi3D Materials or a Model Derivative into a Commercial Product or Service must display "Powered by Hi3D 1.0" clearly and with reasonable prominence. A product with a user interface must display it in the interface, on an About page or in product acknowledgements. An API, SDK, backend or embedded product without a conventional user interface must display it in primary customer-accessible technical documentation, a service description or a legal notice. Where the display location supports a clickable link, the text must link to the official Hi3D website listed in Schedule 2; if a link cannot be provided, the official website address must be stated in the same location or related documentation. Merely obtaining or using a generated 3D Output file does not, by itself, create the branding-display obligation under this Section.
131
+
132
+ You may use the Licensor's commercial marks only to comply with the branding-display obligation under this Section. This permission may be revoked at any time and does not transfer any rights or constitute the Licensor's endorsement of you or any related product or service. The Licensor may require you in writing to cease using or remove the relevant marks, and you must do so within five (5) business days after receiving the notice. Once the marks have been removed, you will no longer be subject to the corresponding branding-display obligation.
133
+
134
+ ## 7.1 No Endorsement
135
+
136
+ Unless we expressly agree in writing, you may not state or imply that your product, service, Model Derivative or Output was created, certified, endorsed or supported by OCEANO HK HOLDING LIMITED, or is an official Hi3D product.
137
+
138
+ # 8 Trademarks and Notices
139
+
140
+ Except for the limited license below, this Agreement grants no right to use the names, logos, trade dress, domain names or trademarks of OCEANO HK HOLDING LIMITED, Hi3D or their affiliates. Reasonable descriptive use to identify the source of the Hi3D Materials is permitted.
141
+
142
+ ## 8.1 Limited Mark License
143
+
144
+ While you remain in compliance with this Agreement, we grant you a limited, non-exclusive, non-transferable and non-sublicensable license to use the "Hi3D" word mark only to the extent necessary to comply with Sections 4 and 7. This license terminates when your right to use the relevant Hi3D Materials terminates under Section 14. It does not include use of any logo or any use that implies endorsement, sponsorship or official status. All goodwill belongs to the applicable trademark owner.
145
+
146
+ ## 8.2 Required Notice
147
+
148
+ The NOTICE file must state: "Hi3D is licensed under the Hi3D Open License Agreement Version 1.0. Copyright © 2026 OCEANO HK HOLDING LIMITED. All rights not expressly granted are reserved. The Hi3D trademarks are owned by OCEANO HK HOLDING LIMITED or its applicable affiliate(s)."
149
+
150
+ # 9 Outputs
151
+
152
+ As between you and us, we claim no ownership of Outputs generated by you. So long as an Output itself does not contain Hi3D Materials or a Model Derivative, you and downstream users who obtain that Output may reproduce, edit, display, sell and distribute it without being required, solely because of that use, to display "Powered by Hi3D 1.0," include this Agreement or a NOTICE file, or apply for a scale-based license under Section 5. This Section does not transfer rights and does not guarantee that an Output is copyrightable, original or non-infringing. Any applicable terms governing third-party models, inputs, materials or services must still be observed.
153
+
154
+ # 10 Third-Party Materials
155
+
156
+ Third-party software, models, weights and their outputs remain subject to their respective licenses and terms of use, and you are responsible for independently obtaining and complying with the relevant rights. Sample code included with this release may call Hunyuan3D-2mini to generate a first-stage coarse model, but Hi3D does not license that model's software, weights or outputs under this Agreement; the model version, source and license used by the sample must be identified in the release documentation. Other external models loaded for second-stage inference, and their actual versions, must also be listed in the third-party dependency schedule. Unless expressly stated otherwise, this Agreement does not license training data, sample media, fonts, datasets or other Excluded Materials.
157
+
158
+ # 11 Disclaimer
159
+
160
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE HI3D MATERIALS, MODEL DERIVATIVES, OUTPUTS AND ALL RELATED RIGHTS ARE PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, SECURITY, RELIABILITY OR UNINTERRUPTED OPERATION. YOU ARE RESPONSIBLE FOR DETERMINING WHETHER YOUR USE IS APPROPRIATE AND LAWFUL.
161
+
162
+ # 12 Limitation of Liability
163
+
164
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, OCEANO HK HOLDING LIMITED, ITS AFFILIATES AND CONTRIBUTORS WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA OR GOODWILL, OR BUSINESS INTERRUPTION, ARISING FROM THIS AGREEMENT OR THE HI3D MATERIALS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS. LIABILITY THAT CANNOT LAWFULLY BE EXCLUDED IS NOT EXCLUDED BY THIS SECTION.
165
+
166
+ # 13 Indemnification
167
+
168
+ You will, to the applicable extent, defend, indemnify and hold harmless OCEANO HK HOLDING LIMITED, its affiliates, officers, employees and contributors from and against third-party claims, losses, liabilities, damages, judgments, costs and reasonable legal fees arising from your breach of this Agreement; unlawful use; inputs; Outputs; modifications; Model Derivatives; products; services; or distribution. We will promptly notify you of a claim and, at your expense, provide reasonable assistance and permit you to control the defense and settlement; however, without the indemnified party's prior written consent, you may not admit liability on its behalf, impose obligations on it or agree to non-monetary relief against it.
169
+
170
+ # 14 Term and Termination
171
+
172
+ This Agreement takes effect when you first access the Hi3D Materials or exercise the relevant rights, and continues until terminated. Upon a material breach, your rights terminate immediately following our written notice. For any other breach, your rights terminate if you fail to cure within thirty days after our written notice. Patent termination under Section 3.1 and scale-based termination under Section 5.4 operate according to their respective terms.
173
+
174
+ ## 14.1 Effect of Termination
175
+
176
+ Upon termination, you must cease any use no longer authorized under this Agreement and delete any Hi3D model weights and other Hi3D Materials in your possession or control that are required to be deleted. Records required by law and routine backups that cannot be immediately purged must not be used and must be deleted in accordance with the applicable retention period. Termination does not automatically terminate licenses lawfully granted before termination where downstream recipients remain compliant, but you may make no new distributions. Sections 1 and 7 through 16, and accrued rights, survive to the extent applicable.
177
+
178
+ # 15 Governing Law and Arbitration
179
+
180
+ This Agreement and any non-contractual obligations arising from it are governed by the laws of the Hong Kong Special Administrative Region, without regard to conflict-of-laws rules. Any dispute arising out of or relating to this Agreement will be finally resolved by arbitration administered by the Hong Kong International Arbitration Centre under the HKIAC Administered Arbitration Rules in force when the Notice of Arbitration is submitted. The seat of arbitration is Hong Kong, the tribunal will consist of one arbitrator, and the language of arbitration is English. Either party may seek urgent interim or injunctive relief from a court of competent jurisdiction.
181
+
182
+ # 16 General
183
+
184
+ This Agreement constitutes the entire agreement concerning this public license. To the extent a separate written commercial license expressly addresses a matter and conflicts with this Agreement, the commercial license controls. If any provision is unenforceable, it will be adjusted only to the minimum extent necessary and the remainder will remain in effect. Failure to enforce a right is not a waiver. Headings and translations do not affect the meaning of the terms. Except for a permitted transfer with a related product or business where the transferee accepts this Agreement, you may not assign this Agreement.
185
+
186
+ ## 16.1 Non-Retroactive Updates
187
+
188
+ The license text accompanying the version of the Hi3D Materials you receive continues to apply to that version. We may license later versions under revised terms, but revisions do not retroactively alter rights already granted for earlier versions.
189
+
190
+ ## 16.2 Contact
191
+
192
+ Applications for scale-based licenses and legal notices must be submitted using the contact details published on the official Hi3D release page, or sent to another address formally designated by OCEANO HK HOLDING LIMITED.
193
+
194
+ # Schedule 1 Acceptable Use Policy
195
+
196
+ You may not use the Hi3D Materials or Model Derivatives for any of the following activities, or generate or disseminate the prohibited content listed below through such use. This Schedule does not impose model-use obligations on a person merely because that person obtains or ordinarily uses a generated 3D finished work that does not contain Hi3D Materials or a Model Derivative; subsequent use of such a finished work remains subject to applicable law and third-party rights.
197
+
198
+ - Violate applicable law, sanctions, export controls, court orders or binding regulatory requirements;
199
+ - Infringe or misappropriate copyright, patent rights, trademark rights, trade secrets, privacy, data protection, publicity rights, image rights or other rights;
200
+ - Exploit, sexualize, groom or traffic minors, or otherwise harm minors;
201
+ - Create, distribute or facilitate malware, ransomware, unauthorized access, credential theft, destructive code or material network abuse;
202
+ - Deceptively impersonate another person, commit fraud, manipulate identity, or generate deceptive synthetic media without disclosure where disclosure is required by law;
203
+ - Intentionally facilitate violence, terrorism, self-harm, unlawful discrimination, stalking, harassment or targeted abuse;
204
+ - Unlawfully collect, infer, disclose or use personal or sensitive information, or undertake any of those activities for the purpose of harming another person;
205
+ - Circumvent technical safeguards or access controls in the Hi3D Materials, except for authorized good-faith security research conducted with appropriate safeguards;
206
+ - Make fully automated decisions in healthcare, employment, credit, housing, education, insurance, law enforcement or essential services that have legal or similarly significant effects, without legally required human oversight, testing, notice and appeal mechanisms; or
207
+ - Falsely represent a Model Derivative or product as an official Hi3D version or as endorsed by OCEANO HK HOLDING LIMITED.
208
+
209
+ This Policy does not prohibit lawful research, testing, auditing, red-teaming or development conducted with reasonable safeguards and not deployed for a prohibited purpose.
210
+
211
+ # Schedule 2 Licensed Asset Schedule
212
+
213
+ This Schedule identifies the assets in this release that are subject to the Hi3D Open License Agreement Version 1.0. The second-stage inference code, documentation and configuration listed below as "Included," together with the eight second-stage model files identified below for distribution under the name Hi3D v1.0 in the Hugging Face model repository at https://huggingface.co/hi3d-official/Hi3Dv1.0, are included under this Agreement to the extent the Licensor owns or is authorized to license them. Inclusion is not affected by the code and weights being stored separately on GitHub and Hugging Face. No third-party model, external dependency, unreleased first-stage material or file listed as "Excluded" is licensed under this Agreement.
214
+
215
+ | Item | Details |
216
+ | --- | --- |
217
+ | Code Release (Basis of Included Scope) | The Hi3D v1.0 source release at https://github.com/hi3d-official/Hi3Dv1.0 containing this Agreement. The included code, documentation and configuration are identified by the paths below. This Schedule applies to the files accompanying this version of the Agreement, not automatically to later repository versions. |
218
+ | Second-Stage Inference Code and Architecture (Included) | Under sparc3d/: models/, nn/, ops/, pipelines/, schedulers/, sparcubes/, structures/, utils/ and package initialization files; demo/refine_mesh.py, demo/image_to_mesh.py and demo/coarse_stage.py. Coverage is limited to implementations and modifications that the Licensor is authorized to license. coarse_stage.py is sample adapter code that calls an external Hunyuan model and does not include the Hunyuan code or weights. |
219
+ | Second-Stage Model Weights and Configuration (Included) | The following eight Hi3D v1.0 files are included under this Agreement at paths relative to the root of the Hugging Face model repository: model_index.json; scheduler/scheduler_config.json; transformer_512/config.json; transformer_512/model.safetensors (3,675,679,280 bytes); transformer_1024/config.json; transformer_1024/model.safetensors (3,675,679,280 bytes); vae/config.json; and vae/model.safetensors (376,629,712 bytes). transformer_1024/model.safetensors is used for both the 1024³ and 1536³ settings; there is no separate transformer_1536 weight in this release. The designated release location for these files is https://huggingface.co/hi3d-official/Hi3Dv1.0. This item excludes all other files in that repository and also excludes external models and weights such as Hunyuan3D-2mini, DINOv2 and CLIP. |
220
+ | Documentation and Configuration (Included to the Extent Licensable) | README.md, README.zh-CN.md, docs/installation.md, docs/pipeline.md and docs/reproduction.md, together with content in pyproject.toml, MANIFEST.in, docker/Dockerfile and docker/constraints.txt that the Licensor is authorized to license. Original notices in AUTHORS, NOTICE, THIRD_PARTY_LICENSES.md and third-party license files must be preserved unchanged. |
221
+ | Sample Media (Excluded) | assets/example.png, assets/framework.png, assets/global-comparison-1.jpg, assets/global-comparison-2.jpg and assets/local-detail-comparison.jpg are excluded. Their presence in this release does not grant any license to use them under this Agreement. Any use is subject to the applicable rights and separate permissions. |
222
+ | Unreleased and Third-Party Materials (Excluded) | Unreleased proprietary Hi3D first-stage materials, training code and datasets; original third-party rights in TRELLIS, Pointcept, O-CNN and FlexiCubes; Hunyuan3D-2mini code, weights and outputs called externally by the sample; and the facebook/dinov2-large and openai/clip-vit-large-patch14 weights separately obtained at runtime are not sublicensed under this Hi3D Agreement. Original third-party notices and their respective terms continue to apply. |
223
+ | Official Hi3D Websites | The link target for "Powered by Hi3D 1.0" under Section 7 is http://hi3d.ai/. The China website is https://www.hi3d.cn/. |
NOTICE ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SPARC3D
2
+ Copyright 2026 The SPARC3D Authors
3
+
4
+ This product includes software developed by third parties. The following
5
+ components are redistributed under their own licenses, reproduced in full in
6
+ THIRD_PARTY_LICENSES.md:
7
+
8
+ * Microsoft TRELLIS (MIT License)
9
+ Copyright (c) Microsoft Corporation.
10
+ https://github.com/microsoft/TRELLIS
11
+
12
+ * Pointcept / Point Transformer V3 (MIT License)
13
+ Copyright (c) 2023 Pointcept
14
+ https://github.com/Pointcept/Pointcept
15
+
16
+ * O-CNN, ocnn-pytorch (MIT License)
17
+ Copyright (c) 2022 Peng-Shuai Wang <wangps@hotmail.com>
18
+ https://github.com/octree-nn/ocnn-pytorch
19
+
20
+ * NVIDIA FlexiCubes (Apache License 2.0)
21
+ Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
22
+ https://github.com/nv-tlabs/FlexiCubes
23
+ License also included at sparc3d/ops/flexicubes/LICENSE.txt
24
+
25
+ GPU surface voxelization in sparc3d/ops descends from the PyTorch extension
26
+ written by Zhihao Li; see AUTHORS.
27
+
28
+
29
+ Hi3D v1.0
30
+ ---------
31
+ Hi3D v1.0 includes the SPARC3D inference implementation. Original copyright,
32
+ attribution, license texts, and per-file modification notices are retained.
33
+ The Python package and API names are sparc3d / Sparc3DPipeline.
34
+
35
+ Hunyuan3D is referenced only by an optional coarse-mesh example adapter. Tencent's
36
+ Hunyuan3D implementation and weights are not bundled with or relicensed by this
37
+ distribution. External image encoders and any separately obtained model weights
38
+ retain their own terms. See THIRD_PARTY_LICENSES.md for the source and model
39
+ license index.
40
+
41
+
42
+ Hi3D Open License Agreement Version 1.0 notice
43
+ --------------------------------------------
44
+ The following notice applies to the Hi3D Materials identified in Schedule 2 of
45
+ LICENSE, only to the extent OCEANO HK HOLDING LIMITED owns or is authorized to
46
+ license them. It does not replace the original third-party notices above or
47
+ extend the Agreement to Excluded Materials. See THIRD_PARTY_LICENSES.md.
48
+
49
+ Hi3D is licensed under the Hi3D Open License Agreement Version 1.0. Copyright © 2026 OCEANO HK HOLDING LIMITED. All rights not expressly granted are reserved. The Hi3D trademarks are owned by OCEANO HK HOLDING LIMITED or its applicable affiliate(s).
README.md CHANGED
@@ -1,13 +1,34 @@
1
  ---
2
- title: Hi3d Official Hi3dv1 0
3
- emoji: 💻
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.30.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Hi3D v1.0
3
+ emoji: 🗿
4
+ colorFrom: gray
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.30.0
 
8
  app_file: app.py
9
+ python_version: "3.10"
10
+ short_description: Image to high-res 3D mesh with Hi3D v1.0 refinement
11
+ startup_duration_timeout: 1h
12
+ license: other
13
+ license_name: hi3d-open-license-agreement-1.0
14
+ license_link: LICENSE
15
+ models:
16
+ - hi3d-official/Hi3Dv1.0
17
+ - tencent/Hunyuan3D-2mini
18
+ - facebook/dinov2-large
19
+ - openai/clip-vit-large-patch14
20
  ---
21
 
22
+ # Hi3D v1.0 demo
23
+
24
+ Image → coarse mesh ([Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini), or upload your own) →
25
+ high-resolution geometry refinement with [hi3d-official/Hi3Dv1.0](https://huggingface.co/hi3d-official/Hi3Dv1.0).
26
+
27
+ *Powered by Hi3D 1.0* — <http://hi3d.ai/>
28
+
29
+ - The `sparc3d/` package is the official inference code from <https://github.com/hi3d-official/Hi3Dv1.0>
30
+ (Hi3D Open License Agreement 1.0, plus retained Apache-2.0/MIT terms — see `LICENSE`, `NOTICE`,
31
+ `THIRD_PARTY_LICENSES.md`, `AUTHORS`). One patch: sparse convolutions use spconv's Native algorithm
32
+ (`SPCONV_ALGO=native`) so they run on ZeroGPU's Blackwell GPUs.
33
+ - Hunyuan3D-2mini is a third-party model under the Tencent Hunyuan Community License.
34
+ - Example images come from [microsoft/TRELLIS](https://github.com/microsoft/TRELLIS) (MIT License, © Microsoft Corporation).
THIRD_PARTY_LICENSES.md ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party licenses
2
+
3
+ This license index adapts the SPARC3D third-party notices for Hi3D v1.0. The
4
+ original license texts and attributions are retained, with additional references
5
+ for separately obtained models and optional examples.
6
+
7
+ The original SPARC3D license is retained in [Original Apache license](#original-apache-license).
8
+ Hi3D-owned or otherwise licensable materials identified in Schedule 2 are governed
9
+ by the Hi3D Agreement in [LICENSE](LICENSE). See [License scope](#license-scope)
10
+ for the component boundaries; the new Agreement does not replace original
11
+ third-party licenses or previously granted rights.
12
+
13
+ Parts of this package are derived from other open-source projects. Each is listed
14
+ below with the files it covers and the full text of its license, as those licenses
15
+ require. Every file named here also carries a provenance header pointing back to
16
+ this document.
17
+
18
+ | Upstream | License | Covers |
19
+ | --- | --- | --- |
20
+ | [Microsoft TRELLIS](https://github.com/microsoft/TRELLIS) | MIT | the sparse transformer and its building blocks |
21
+ | [Pointcept / Point Transformer V3](https://github.com/Pointcept/Pointcept) | MIT | the `Point` structure and the sparse PTv3 decoder |
22
+ | [O-CNN (ocnn-pytorch)](https://github.com/octree-nn/ocnn-pytorch) | MIT | Z-order (Morton) encoding |
23
+ | [NVIDIA FlexiCubes](https://github.com/nv-tlabs/FlexiCubes) | Apache-2.0 | dual-marching-cubes surface extraction |
24
+
25
+ ---
26
+
27
+ ## License scope
28
+
29
+ This index explains the component boundaries in Schedule 2 of the
30
+ [Hi3D Open License Agreement Version 1.0](LICENSE). It does not amend the
31
+ Agreement or grant additional rights. The Agreement and each applicable
32
+ third-party license govern their respective materials.
33
+
34
+ | Component | Applicable terms |
35
+ | --- | --- |
36
+ | Second-stage implementation and modifications in `sparc3d/`, including package initialization | The Hi3D Agreement applies only to content OCEANO HK HOLDING LIMITED owns or is authorized to license; original third-party rights in the same files remain under their original terms |
37
+ | `demo/refine_mesh.py`, `demo/image_to_mesh.py`, `demo/coarse_stage.py` | The Hi3D Agreement applies to licensable example code; Hunyuan's separate implementation, weights and outputs are excluded |
38
+ | The README, documentation and configuration paths listed in Schedule 2 | The Hi3D Agreement applies to licensable content; original notices and third-party terms remain in effect |
39
+ | The eight refinement artifacts listed in Schedule 2 | Distributed separately from the source code and Python packages under the Hi3D Agreement; other files or versions are not included merely by sharing a repository |
40
+ | Original SPARC3D, TRELLIS, Pointcept, O-CNN and FlexiCubes rights | Retained Apache-2.0 or MIT terms, as detailed in this file and the original source headers |
41
+ | Hunyuan3D-2mini, DINOv2, CLIP and other external dependencies | Their own upstream licenses; not relicensed by Hi3D |
42
+ | The five files listed as Sample Media in Schedule 2 | Excluded from the Hi3D Agreement; their presence or display does not grant reuse rights |
43
+ | Unreleased first-stage materials, training code, datasets, and brand assets | Excluded, subject to the limited trademark permissions expressly granted in the Agreement |
44
+
45
+ `LicenseRef-Hi3D-1.0` denotes the custom Agreement in [LICENSE](LICENSE), not a
46
+ blanket license for every file. Previously granted rights under applicable
47
+ Apache-2.0 or MIT licenses are not withdrawn or replaced by the Hi3D Agreement.
48
+
49
+ The source archive contains media expressly excluded by Schedule 2. Therefore
50
+ its package metadata does not assert a single SPDX license expression for the
51
+ whole archive. The complete applicable agreements and notices are included with
52
+ the Python distributions.
53
+
54
+ ---
55
+
56
+ ## Microsoft TRELLIS
57
+
58
+ Covers:
59
+
60
+ - `sparc3d/models/transformers/sparc3d_transformer.py`
61
+ - `sparc3d/nn/norm.py`
62
+ - `sparc3d/nn/rope.py`
63
+ - `sparc3d/nn/utils.py`
64
+ - `sparc3d/nn/sparse/basic.py`
65
+ - `sparc3d/nn/sparse/conv.py`
66
+ - `sparc3d/nn/sparse/linear.py`
67
+ - `sparc3d/nn/sparse/nonlinearity.py`
68
+ - `sparc3d/nn/sparse/spatial.py`
69
+ - `sparc3d/nn/sparse/attention/full_attn.py`
70
+ - `sparc3d/nn/sparse/attention/modules.py`
71
+ - `sparc3d/nn/sparse/transformer/blocks.py`
72
+ - `sparc3d/nn/sparse/transformer/modulated.py`
73
+
74
+ Each of these files states the specific SPARC3D modification in its header. In
75
+ general the training paths, the alternative attention backends, and the dense
76
+ variants were removed, leaving the released inference architecture.
77
+
78
+ ```
79
+ MIT License
80
+
81
+ Copyright (c) Microsoft Corporation.
82
+
83
+ Permission is hereby granted, free of charge, to any person obtaining a copy
84
+ of this software and associated documentation files (the "Software"), to deal
85
+ in the Software without restriction, including without limitation the rights
86
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87
+ copies of the Software, and to permit persons to whom the Software is
88
+ furnished to do so, subject to the following conditions:
89
+
90
+ The above copyright notice and this permission notice shall be included in all
91
+ copies or substantial portions of the Software.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
99
+ SOFTWARE
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Pointcept / Point Transformer V3
105
+
106
+ Point Transformer V3 (Xiaoyang Wu et al.) is released as part of Pointcept.
107
+
108
+ Covers:
109
+
110
+ - `sparc3d/models/autoencoders/blocks.py`
111
+ - `sparc3d/models/autoencoders/ptv3.py`
112
+ - `sparc3d/structures/point.py`
113
+ - `sparc3d/structures/point_modules.py`
114
+ - `sparc3d/utils/misc.py`
115
+ - `sparc3d/utils/precision.py`
116
+ - `sparc3d/utils/serialization/default.py`
117
+
118
+ The decoder half was specialized for the sparse mesh autoencoder; the encoder,
119
+ the training paths, and the Hilbert serialization order were removed.
120
+
121
+ ```
122
+ MIT License
123
+
124
+ Copyright (c) 2023 Pointcept
125
+
126
+ Permission is hereby granted, free of charge, to any person obtaining a copy
127
+ of this software and associated documentation files (the "Software"), to deal
128
+ in the Software without restriction, including without limitation the rights
129
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
130
+ copies of the Software, and to permit persons to whom the Software is
131
+ furnished to do so, subject to the following conditions:
132
+
133
+ The above copyright notice and this permission notice shall be included in all
134
+ copies or substantial portions of the Software.
135
+
136
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
137
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
138
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
139
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
140
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
141
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
142
+ SOFTWARE.
143
+ ```
144
+
145
+ ---
146
+
147
+ ## O-CNN (ocnn-pytorch)
148
+
149
+ Covers:
150
+
151
+ - `sparc3d/utils/serialization/z_order.py`
152
+
153
+ The Morton decode tables and the batch-index field of the key were removed; only
154
+ `xyz2key` encoding is retained.
155
+
156
+ ```
157
+ MIT License
158
+
159
+ Copyright (c) 2022 Peng-Shuai Wang <wangps@hotmail.com>
160
+
161
+ Permission is hereby granted, free of charge, to any person obtaining a copy
162
+ of this software and associated documentation files (the "Software"), to deal
163
+ in the Software without restriction, including without limitation the rights
164
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
165
+ copies of the Software, and to permit persons to whom the Software is
166
+ furnished to do so, subject to the following conditions:
167
+
168
+ The above copyright notice and this permission notice shall be included in
169
+ all copies or substantial portions of the Software.
170
+
171
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
172
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
173
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
174
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
175
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
176
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
177
+ THE SOFTWARE.
178
+ ```
179
+
180
+ ---
181
+
182
+ ## NVIDIA FlexiCubes
183
+
184
+ Covers:
185
+
186
+ - `sparc3d/ops/flexicubes/`
187
+
188
+ FlexiCubes retains its Apache-2.0 license. Its copy of the
189
+ license ships in the package at `sparc3d/ops/flexicubes/LICENSE.txt`, satisfying
190
+ Apache-2.0 section 4(a); the NVIDIA copyright notices are retained in the source
191
+ files, satisfying 4(c); and `sparc3d/ops/flexicubes/flexicubes.py` carries a
192
+ header enumerating every SPARC3D modification, satisfying 4(b). Upstream ships no
193
+ `NOTICE` file, so 4(d) does not apply.
194
+
195
+ The vendored copy is inference-only: the tetrahedral output, the learned
196
+ interpolation weights, the developability regularizer, and the gradient hooks
197
+ were removed.
198
+
199
+
200
+ ---
201
+
202
+ ## External models and optional examples
203
+
204
+ This section indexes components obtained separately at runtime or by the user.
205
+ It does not add their code or weights to this distribution, grant a license to
206
+ them, or replace their upstream terms. Neither the Hi3D Agreement nor a retained
207
+ source-code license automatically applies to external model weights or
208
+ third-party outputs.
209
+
210
+ ### Required external image encoders
211
+
212
+ The default image conditioner loads the following models separately. Neither is
213
+ included in the Hi3D refinement checkpoint or in this source tree.
214
+
215
+ | Component | Role | Official model source | Upstream license reference |
216
+ | --- | --- | --- | --- |
217
+ | DINOv2-large | Spatial image features | [facebook/dinov2-large](https://huggingface.co/facebook/dinov2-large) | [Apache-2.0, DINOv2 LICENSE](https://github.com/facebookresearch/dinov2/blob/main/LICENSE) |
218
+ | CLIP ViT-L/14 | Global image conditioning | [openai/clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) | [MIT, CLIP LICENSE](https://github.com/openai/CLIP/blob/main/LICENSE) |
219
+
220
+ Consult each model's own model card and the terms supplied with the version you
221
+ obtain. Background-removal models used by the optional `rembg` integration are
222
+ also obtained separately and retain their own terms.
223
+
224
+ ### Optional Hunyuan3D coarse-mesh example
225
+
226
+ `demo/coarse_stage.py` is a Hi3D / SPARC3D example adapter. It imports
227
+ `hy3dgen.shapegen.Hunyuan3DDiTFlowMatchingPipeline` only when that example is used.
228
+ Its default model is `tencent/Hunyuan3D-2mini`, subfolder
229
+ `hunyuan3d-dit-v2-mini-turbo`.
230
+
231
+ **Hunyuan3D is only an optional example of coarse-mesh generation.** It is not a
232
+ mandatory dependency of the Hi3D refinement library or a required product
233
+ integration. `demo/refine_mesh.py` and the core `sparc3d` package accept a supplied
234
+ coarse mesh without using Hunyuan3D. Other suitable coarse-mesh generators can be
235
+ used instead. Installing the `[inference]` or `[demo]` extras does not install
236
+ Hunyuan3D.
237
+
238
+ **Tencent's Hunyuan3D implementation and pretrained weights are third-party
239
+ releases, not open-source content released by Hi3D.** They are not bundled with
240
+ this repository and are not relicensed under the Hi3D Agreement or any
241
+ retained source-code license. Users who
242
+ choose this example obtain them separately and comply with their upstream terms.
243
+
244
+ | Component used by the example | Official source | Applicable upstream agreement |
245
+ | --- | --- | --- |
246
+ | Hunyuan3D-2 implementation (`hy3dgen`) | [Tencent-Hunyuan/Hunyuan3D-2](https://github.com/Tencent-Hunyuan/Hunyuan3D-2) | [Tencent Hunyuan 3D 2.0 Community License Agreement — source LICENSE](https://github.com/Tencent-Hunyuan/Hunyuan3D-2/blob/main/LICENSE) |
247
+ | Hunyuan3D-2mini weights, mini-turbo variant | [tencent/Hunyuan3D-2mini](https://huggingface.co/tencent/Hunyuan3D-2mini) | [Tencent Hunyuan 3D 2.0 Community License Agreement — model LICENSE](https://huggingface.co/tencent/Hunyuan3D-2mini/blob/main/LICENSE) |
248
+
249
+ Upstream license terms are version-specific. If you select a different model,
250
+ subfolder, or source version, consult its matching license and model card.
251
+ Example-only use does not waive applicable restrictions, attribution,
252
+ redistribution, or hosted-service obligations in those agreements.
253
+
254
+ ### Hi3D refinement weights
255
+
256
+ The eight Hi3D refinement artifacts identified and released under Schedule 2
257
+ are governed by the Hi3D Agreement in [LICENSE](LICENSE). They are distributed
258
+ separately from this source repository; see [License scope](#license-scope).
259
+ Other model files are not covered merely because they share the repository or
260
+ use a similar name.
261
+
262
+
263
+ ---
264
+
265
+ ## Original Apache license
266
+
267
+ The following is the original SPARC3D Apache-2.0 text, retained without changes.
268
+ It continues to govern the materials and rights to which it originally applied;
269
+ the Hi3D Agreement does not revoke those grants.
270
+
271
+ ```text
272
+
273
+ Apache License
274
+ Version 2.0, January 2004
275
+ http://www.apache.org/licenses/
276
+
277
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
278
+
279
+ 1. Definitions.
280
+
281
+ "License" shall mean the terms and conditions for use, reproduction,
282
+ and distribution as defined by Sections 1 through 9 of this document.
283
+
284
+ "Licensor" shall mean the copyright owner or entity authorized by
285
+ the copyright owner that is granting the License.
286
+
287
+ "Legal Entity" shall mean the union of the acting entity and all
288
+ other entities that control, are controlled by, or are under common
289
+ control with that entity. For the purposes of this definition,
290
+ "control" means (i) the power, direct or indirect, to cause the
291
+ direction or management of such entity, whether by contract or
292
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
293
+ outstanding shares, or (iii) beneficial ownership of such entity.
294
+
295
+ "You" (or "Your") shall mean an individual or Legal Entity
296
+ exercising permissions granted by this License.
297
+
298
+ "Source" form shall mean the preferred form for making modifications,
299
+ including but not limited to software source code, documentation
300
+ source, and configuration files.
301
+
302
+ "Object" form shall mean any form resulting from mechanical
303
+ transformation or translation of a Source form, including but
304
+ not limited to compiled object code, generated documentation,
305
+ and conversions to other media types.
306
+
307
+ "Work" shall mean the work of authorship, whether in Source or
308
+ Object form, made available under the License, as indicated by a
309
+ copyright notice that is included in or attached to the work
310
+ (an example is provided in the Appendix below).
311
+
312
+ "Derivative Works" shall mean any work, whether in Source or Object
313
+ form, that is based on (or derived from) the Work and for which the
314
+ editorial revisions, annotations, elaborations, or other modifications
315
+ represent, as a whole, an original work of authorship. For the purposes
316
+ of this License, Derivative Works shall not include works that remain
317
+ separable from, or merely link (or bind by name) to the interfaces of,
318
+ the Work and Derivative Works thereof.
319
+
320
+ "Contribution" shall mean any work of authorship, including
321
+ the original version of the Work and any modifications or additions
322
+ to that Work or Derivative Works thereof, that is intentionally
323
+ submitted to Licensor for inclusion in the Work by the copyright owner
324
+ or by an individual or Legal Entity authorized to submit on behalf of
325
+ the copyright owner. For the purposes of this definition, "submitted"
326
+ means any form of electronic, verbal, or written communication sent
327
+ to the Licensor or its representatives, including but not limited to
328
+ communication on electronic mailing lists, source code control systems,
329
+ and issue tracking systems that are managed by, or on behalf of, the
330
+ Licensor for the purpose of discussing and improving the Work, but
331
+ excluding communication that is conspicuously marked or otherwise
332
+ designated in writing by the copyright owner as "Not a Contribution."
333
+
334
+ "Contributor" shall mean Licensor and any individual or Legal Entity
335
+ on behalf of whom a Contribution has been received by Licensor and
336
+ subsequently incorporated within the Work.
337
+
338
+ 2. Grant of Copyright License. Subject to the terms and conditions of
339
+ this License, each Contributor hereby grants to You a perpetual,
340
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
341
+ copyright license to reproduce, prepare Derivative Works of,
342
+ publicly display, publicly perform, sublicense, and distribute the
343
+ Work and such Derivative Works in Source or Object form.
344
+
345
+ 3. Grant of Patent License. Subject to the terms and conditions of
346
+ this License, each Contributor hereby grants to You a perpetual,
347
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
348
+ (except as stated in this section) patent license to make, have made,
349
+ use, offer to sell, sell, import, and otherwise transfer the Work,
350
+ where such license applies only to those patent claims licensable
351
+ by such Contributor that are necessarily infringed by their
352
+ Contribution(s) alone or by combination of their Contribution(s)
353
+ with the Work to which such Contribution(s) was submitted. If You
354
+ institute patent litigation against any entity (including a
355
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
356
+ or a Contribution incorporated within the Work constitutes direct
357
+ or contributory patent infringement, then any patent licenses
358
+ granted to You under this License for that Work shall terminate
359
+ as of the date such litigation is filed.
360
+
361
+ 4. Redistribution. You may reproduce and distribute copies of the
362
+ Work or Derivative Works thereof in any medium, with or without
363
+ modifications, and in Source or Object form, provided that You
364
+ meet the following conditions:
365
+
366
+ (a) You must give any other recipients of the Work or
367
+ Derivative Works a copy of this License; and
368
+
369
+ (b) You must cause any modified files to carry prominent notices
370
+ stating that You changed the files; and
371
+
372
+ (c) You must retain, in the Source form of any Derivative Works
373
+ that You distribute, all copyright, patent, trademark, and
374
+ attribution notices from the Source form of the Work,
375
+ excluding those notices that do not pertain to any part of
376
+ the Derivative Works; and
377
+
378
+ (d) If the Work includes a "NOTICE" text file as part of its
379
+ distribution, then any Derivative Works that You distribute must
380
+ include a readable copy of the attribution notices contained
381
+ within such NOTICE file, excluding those notices that do not
382
+ pertain to any part of the Derivative Works, in at least one
383
+ of the following places: within a NOTICE text file distributed
384
+ as part of the Derivative Works; within the Source form or
385
+ documentation, if provided along with the Derivative Works; or,
386
+ within a display generated by the Derivative Works, if and
387
+ wherever such third-party notices normally appear. The contents
388
+ of the NOTICE file are for informational purposes only and
389
+ do not modify the License. You may add Your own attribution
390
+ notices within Derivative Works that You distribute, alongside
391
+ or as an addendum to the NOTICE text from the Work, provided
392
+ that such additional attribution notices cannot be construed
393
+ as modifying the License.
394
+
395
+ You may add Your own copyright statement to Your modifications and
396
+ may provide additional or different license terms and conditions
397
+ for use, reproduction, or distribution of Your modifications, or
398
+ for any such Derivative Works as a whole, provided Your use,
399
+ reproduction, and distribution of the Work otherwise complies with
400
+ the conditions stated in this License.
401
+
402
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
403
+ any Contribution intentionally submitted for inclusion in the Work
404
+ by You to the Licensor shall be under the terms and conditions of
405
+ this License, without any additional terms or conditions.
406
+ Notwithstanding the above, nothing herein shall supersede or modify
407
+ the terms of any separate license agreement you may have executed
408
+ with Licensor regarding such Contributions.
409
+
410
+ 6. Trademarks. This License does not grant permission to use the trade
411
+ names, trademarks, service marks, or product names of the Licensor,
412
+ except as required for reasonable and customary use in describing the
413
+ origin of the Work and reproducing the content of the NOTICE file.
414
+
415
+ 7. Disclaimer of Warranty. Unless required by applicable law or
416
+ agreed to in writing, Licensor provides the Work (and each
417
+ Contributor provides its Contributions) on an "AS IS" BASIS,
418
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
419
+ implied, including, without limitation, any warranties or conditions
420
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
421
+ PARTICULAR PURPOSE. You are solely responsible for determining the
422
+ appropriateness of using or redistributing the Work and assume any
423
+ risks associated with Your exercise of permissions under this License.
424
+
425
+ 8. Limitation of Liability. In no event and under no legal theory,
426
+ whether in tort (including negligence), contract, or otherwise,
427
+ unless required by applicable law (such as deliberate and grossly
428
+ negligent acts) or agreed to in writing, shall any Contributor be
429
+ liable to You for damages, including any direct, indirect, special,
430
+ incidental, or consequential damages of any character arising as a
431
+ result of this License or out of the use or inability to use the
432
+ Work (including but not limited to damages for loss of goodwill,
433
+ work stoppage, computer failure or malfunction, or any and all
434
+ other commercial damages or losses), even if such Contributor
435
+ has been advised of the possibility of such damages.
436
+
437
+ 9. Accepting Warranty or Additional Liability. While redistributing
438
+ the Work or Derivative Works thereof, You may choose to offer,
439
+ and charge a fee for, acceptance of support, warranty, indemnity,
440
+ or other liability obligations and/or rights consistent with this
441
+ License. However, in accepting such obligations, You may act only
442
+ on Your own behalf and on Your sole responsibility, not on behalf
443
+ of any other Contributor, and only if You agree to indemnify,
444
+ defend, and hold each Contributor harmless for any liability
445
+ incurred by, or claims asserted against, such Contributor by reason
446
+ of your accepting any such warranty or additional liability.
447
+
448
+ END OF TERMS AND CONDITIONS
449
+
450
+ APPENDIX: How to apply the Apache License to your work.
451
+
452
+ To apply the Apache License to your work, attach the following
453
+ boilerplate notice, with the fields enclosed by brackets "[]"
454
+ replaced with your own identifying information. (Don't include
455
+ the brackets!) The text should be enclosed in the appropriate
456
+ comment syntax for the file format. We also recommend that a
457
+ file or class name and description of purpose be included on the
458
+ same "printed page" as the copyright notice for easier
459
+ identification within third-party archives.
460
+
461
+ Copyright 2026 The SPARC3D Authors
462
+
463
+ Licensed under the Apache License, Version 2.0 (the "License");
464
+ you may not use this file except in compliance with the License.
465
+ You may obtain a copy of the License at
466
+
467
+ http://www.apache.org/licenses/LICENSE-2.0
468
+
469
+ Unless required by applicable law or agreed to in writing, software
470
+ distributed under the License is distributed on an "AS IS" BASIS,
471
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
472
+ See the License for the specific language governing permissions and
473
+ limitations under the License.
474
+ ```
app.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ.setdefault("SPCONV_ALGO", "native")
4
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
5
+
6
+ import spaces # noqa: E402 (must precede torch)
7
+
8
+ import random # noqa: E402
9
+ import tempfile # noqa: E402
10
+ import time # noqa: E402
11
+
12
+ import gradio as gr # noqa: E402
13
+ import numpy as np # noqa: E402
14
+ import torch # noqa: E402
15
+ import trimesh # noqa: E402
16
+ from PIL import Image # noqa: E402
17
+
18
+ from sparc3d import Sparc3DPipeline # noqa: E402
19
+
20
+ MODEL_ID = "hi3d-official/Hi3Dv1.0"
21
+ COARSE_REPO = "tencent/Hunyuan3D-2mini"
22
+ COARSE_SUBFOLDER = "hunyuan3d-dit-v2-mini-turbo"
23
+ COARSE_STEPS = 30 # matches demo/coarse_stage.py
24
+ COARSE_OCTREE_RESOLUTION = 256 # matches demo/coarse_stage.py
25
+ MAX_SEED = 2**31 - 1
26
+
27
+ # ---------------------------------------------------------------- models
28
+ print("Loading Hi3D v1.0 refinement pipeline...")
29
+ pipe = Sparc3DPipeline.from_pretrained(MODEL_ID)
30
+ _ = pipe.transformer_1024 # materialize the deferred high-res transformer now
31
+ pipe.to("cuda")
32
+
33
+ print("Loading Hunyuan3D-2mini (turbo) coarse generator...")
34
+ from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline # noqa: E402
35
+
36
+ coarse_pipe = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
37
+ COARSE_REPO,
38
+ subfolder=COARSE_SUBFOLDER,
39
+ use_safetensors=True,
40
+ device="cuda",
41
+ )
42
+
43
+ print("Loading background remover...")
44
+ from rembg import new_session, remove # noqa: E402
45
+
46
+ rembg_session = new_session()
47
+ print("All models loaded.")
48
+
49
+
50
+ # ---------------------------------------------------------------- helpers
51
+ def _has_transparency(image: Image.Image) -> bool:
52
+ if image.mode not in ("RGBA", "LA") and "transparency" not in image.info:
53
+ return False
54
+ alpha = np.asarray(image.convert("RGBA").getchannel("A"))
55
+ return bool((alpha < 250).any())
56
+
57
+
58
+ def _prepare_input(image: Image.Image, remove_background: bool) -> Image.Image:
59
+ if remove_background and not _has_transparency(image):
60
+ image = remove(image.convert("RGB"), session=rembg_session, bgcolor=[255, 255, 255, 0])
61
+ return image.convert("RGBA")
62
+
63
+
64
+ def _export(mesh: trimesh.Trimesh, suffix: str = ".glb") -> str:
65
+ path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False).name
66
+ mesh.export(path)
67
+ return path
68
+
69
+
70
+ def _duration(image, coarse_mesh=None, resolution=1024, num_inference_steps=25, *args, **kwargs):
71
+ passes = {512: 1, 1024: 2, 1536: 3}.get(int(resolution), 2)
72
+ return int(40 + passes * (30 + 2.4 * int(num_inference_steps)))
73
+
74
+
75
+ # ---------------------------------------------------------------- inference
76
+ @spaces.GPU(duration=_duration)
77
+ def refine(
78
+ image: Image.Image,
79
+ coarse_mesh: str | None = None,
80
+ resolution: int = 1024,
81
+ num_inference_steps: int = 25,
82
+ seed: int = 0,
83
+ randomize_seed: bool = False,
84
+ remove_background: bool = True,
85
+ progress=gr.Progress(),
86
+ ):
87
+ """Generate a high-resolution 3D mesh from a single image with Hi3D v1.0.
88
+
89
+ A coarse mesh is generated with Hunyuan3D-2mini-Turbo (or taken from the
90
+ optional upload), then refined by the Hi3D v1.0 sparse flow model
91
+ conditioned on the reference image.
92
+
93
+ Args:
94
+ image: Reference image of a single object (a cut-out with alpha works best).
95
+ coarse_mesh: Optional path to your own coarse mesh (.glb/.obj/.ply). When
96
+ omitted, Hunyuan3D-2mini-Turbo produces it from the image.
97
+ resolution: Target sparse extraction grid: 512 (1 pass), 1024 (2 passes)
98
+ or 1536 (3 passes).
99
+ num_inference_steps: Scheduler updates per refinement pass.
100
+ seed: Random seed.
101
+ randomize_seed: Pick a random seed instead of `seed`.
102
+ remove_background: Remove the background with rembg when the image has
103
+ no transparency.
104
+
105
+ Returns:
106
+ Paths to the coarse mesh GLB, the refined mesh GLB, the refined GLB
107
+ as a downloadable file, the seed used and a status string.
108
+ """
109
+ if image is None:
110
+ raise gr.Error("Please upload an image.")
111
+ if randomize_seed:
112
+ seed = random.randint(0, MAX_SEED)
113
+ seed = int(seed)
114
+ resolution = int(resolution)
115
+ num_inference_steps = int(num_inference_steps)
116
+ t0 = time.perf_counter()
117
+
118
+ progress(0.0, desc="Preparing image")
119
+ image = _prepare_input(image, remove_background)
120
+
121
+ if coarse_mesh:
122
+ progress(0.02, desc="Loading coarse mesh")
123
+ coarse = trimesh.load(coarse_mesh, force="mesh", process=False)
124
+ else:
125
+ progress(0.02, desc="Generating coarse mesh (Hunyuan3D-2mini-Turbo)")
126
+ coarse = coarse_pipe(
127
+ image=image,
128
+ num_inference_steps=COARSE_STEPS,
129
+ octree_resolution=COARSE_OCTREE_RESOLUTION,
130
+ generator=torch.manual_seed(seed),
131
+ output_type="trimesh",
132
+ )[0]
133
+ t_coarse = time.perf_counter() - t0
134
+ coarse_path = _export(coarse)
135
+
136
+ def _report(event):
137
+ frac = (event.pass_index + event.step / event.num_inference_steps) / event.num_passes
138
+ progress(
139
+ 0.1 + 0.8 * frac,
140
+ desc=f"Hi3D pass {event.pass_index + 1}/{event.num_passes} "
141
+ f"(grid {event.structure_resolution}³) step {event.step}/{event.num_inference_steps}",
142
+ )
143
+
144
+ pipe.progress_callback = _report
145
+ try:
146
+ result = pipe(
147
+ image,
148
+ coarse,
149
+ resolution=resolution,
150
+ num_inference_steps=num_inference_steps,
151
+ seed=seed,
152
+ )
153
+ finally:
154
+ pipe.progress_callback = None
155
+ progress(0.95, desc="Exporting mesh")
156
+ refined_path = _export(result.mesh)
157
+ total = time.perf_counter() - t0
158
+ status = (
159
+ f"Done in {total:.1f}s (coarse {t_coarse:.1f}s) — resolution {result.resolution}, "
160
+ f"{len(result.mesh.faces):,} faces, seed {seed}"
161
+ )
162
+ print(status)
163
+ return coarse_path, refined_path, refined_path, seed, status
164
+
165
+
166
+ # ---------------------------------------------------------------- UI
167
+ CSS = """
168
+ #col-container { max-width: 1200px; margin: 0 auto; }
169
+ .dark .gradio-container { color: var(--body-text-color); }
170
+ """
171
+
172
+ EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples")
173
+ EXAMPLES = [
174
+ [os.path.join(EXAMPLES_DIR, name)]
175
+ for name in (
176
+ "typical_creature_robot_dinosour.png",
177
+ "typical_humanoid_mech.png",
178
+ "typical_building_castle.png",
179
+ "typical_misc_phonograph.png",
180
+ "typical_vehicle_excavator.png",
181
+ )
182
+ ]
183
+
184
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="Hi3D v1.0") as demo:
185
+ with gr.Column(elem_id="col-container"):
186
+ gr.Markdown(
187
+ """
188
+ # Hi3D v1.0 — high-resolution 3D geometry refinement
189
+ Upload an image of an object. A coarse mesh is generated with
190
+ [Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini) (or upload your own),
191
+ then [**Hi3D v1.0**](https://huggingface.co/hi3d-official/Hi3Dv1.0) — built on Sparc3D — refines
192
+ it into high-resolution geometry conditioned on the image. Geometry only (no textures).
193
+ [Model](https://huggingface.co/hi3d-official/Hi3Dv1.0) · [Code](https://github.com/hi3d-official/Hi3Dv1.0) · [Website](http://hi3d.ai/) — *Powered by Hi3D 1.0*
194
+ """
195
+ )
196
+ with gr.Row():
197
+ with gr.Column(scale=1):
198
+ image_in = gr.Image(label="Reference image", type="pil", image_mode="RGBA", height=360)
199
+ coarse_in = gr.File(
200
+ label="Optional: your own coarse mesh (.glb / .obj / .ply) — leave empty to auto-generate",
201
+ file_types=[".glb", ".gltf", ".obj", ".ply", ".stl", ".off"],
202
+ type="filepath",
203
+ )
204
+ resolution = gr.Radio(
205
+ [512, 1024, 1536],
206
+ value=1024,
207
+ label="Resolution (refinement passes: 1 / 2 / 3)",
208
+ )
209
+ run_btn = gr.Button("Generate", variant="primary")
210
+ with gr.Accordion("Advanced settings", open=False):
211
+ steps = gr.Slider(5, 50, value=25, step=1, label="Inference steps per pass")
212
+ seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
213
+ randomize_seed = gr.Checkbox(value=False, label="Randomize seed")
214
+ remove_bg = gr.Checkbox(
215
+ value=True,
216
+ label="Remove background (only applied if the image has no transparency)",
217
+ )
218
+ with gr.Column(scale=2):
219
+ with gr.Row():
220
+ coarse_out = gr.Model3D(label="Coarse mesh", height=420, clear_color=[1, 1, 1, 1])
221
+ refined_out = gr.Model3D(label="Hi3D refined mesh", height=420, clear_color=[1, 1, 1, 1])
222
+ refined_file = gr.File(label="Download refined GLB")
223
+ status = gr.Markdown()
224
+
225
+ inputs = [image_in, coarse_in, resolution, steps, seed, randomize_seed, remove_bg]
226
+ outputs = [coarse_out, refined_out, refined_file, seed, status]
227
+ run_btn.click(refine, inputs=inputs, outputs=outputs, api_name="refine")
228
+
229
+ gr.Examples(
230
+ examples=EXAMPLES,
231
+ inputs=[image_in],
232
+ outputs=outputs,
233
+ fn=refine,
234
+ cache_examples=True,
235
+ cache_mode="lazy",
236
+ label="Examples (images from the MIT-licensed TRELLIS repo)",
237
+ )
238
+
239
+ if __name__ == "__main__":
240
+ demo.launch(mcp_server=True)
examples/typical_building_castle.png ADDED

Git LFS Details

  • SHA256: 076f0554b087b921863643d2b1ab3e0572a13a347fd66bc29cd9d194034affae
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
examples/typical_creature_robot_dinosour.png ADDED

Git LFS Details

  • SHA256: d0986f29557a6fddf9b52b5251a6b6103728c61e201b1cfad1e709b090b72f56
  • Pointer size: 131 Bytes
  • Size of remote file: 632 kB
examples/typical_humanoid_mech.png ADDED

Git LFS Details

  • SHA256: a244ec54b7984e646e54d433de6897657081dd5b9cd5ccd3d865328d813beb49
  • Pointer size: 131 Bytes
  • Size of remote file: 850 kB
examples/typical_misc_phonograph.png ADDED

Git LFS Details

  • SHA256: 14fff9a27ea769d3ca711e9ff55ab3d9385486a5e8b99117f506df326a0a357e
  • Pointer size: 131 Bytes
  • Size of remote file: 517 kB
examples/typical_vehicle_excavator.png ADDED

Git LFS Details

  • SHA256: 27a418853eefa197f1e10ed944a7bb071413fd2bc1681804ee773a6ce3799c52
  • Pointer size: 131 Bytes
  • Size of remote file: 712 kB
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ torchvision==0.23.0
3
+ numpy<2
4
+ transformers>=5
5
+ diffusers
6
+ accelerate
7
+ safetensors
8
+ einops
9
+ omegaconf
10
+ pyyaml
11
+ tqdm
12
+ trimesh>=4.4.0,<5
13
+ pyfqmr
14
+ scikit-image
15
+ pymeshlab
16
+ pygltflib
17
+ xatlas
18
+ opencv-python==4.11.0.86
19
+ opencv-python-headless==4.11.0.86
20
+ rembg
21
+ onnxruntime
22
+ spconv-cu120==2.3.6
23
+ https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt28-cu128-cp310/flash_attn-2.7.4.post1-cp310-cp310-linux_x86_64.whl
24
+ hy3dgen @ git+https://github.com/Tencent-Hunyuan/Hunyuan3D-2@f8db63096c8282cb27354314d896feba5ba6ff8a
sparc3d/__init__.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lazy public package for the SPARC3D pipeline, output, and progress types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = ["Sparc3DPipeline", "Sparc3DPipelineOutput", "Sparc3DProgressEvent"]
10
+
11
+
12
+ def __getattr__(name: str) -> Any:
13
+ """Load one of the public symbols on first access."""
14
+ if name == "Sparc3DPipeline":
15
+ from sparc3d.pipelines.sparc3d_pipeline import Sparc3DPipeline
16
+
17
+ return Sparc3DPipeline
18
+ if name == "Sparc3DPipelineOutput":
19
+ from sparc3d.pipelines.output import Sparc3DPipelineOutput
20
+
21
+ return Sparc3DPipelineOutput
22
+ if name == "Sparc3DProgressEvent":
23
+ from sparc3d.pipelines.progress import Sparc3DProgressEvent
24
+
25
+ return Sparc3DProgressEvent
26
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
27
+
28
+
29
+ def __dir__() -> list[str]:
30
+ """Return module names including the lazy public symbols."""
31
+ return sorted({*globals(), *__all__})
sparc3d/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Neural network models (sparse mesh autoencoder)."""
sparc3d/models/autoencoders/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse mesh autoencoder: the PTv3 decoder and its JSON loader."""
2
+
3
+ from sparc3d.models.autoencoders.config import load_sparconv_vae
4
+ from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
5
+ from sparc3d.models.autoencoders.sparconv_vae import SparconvVAE
6
+
7
+ __all__ = [
8
+ "SparconvVAE",
9
+ "PTv3MeshDecoder",
10
+ "load_sparconv_vae",
11
+ ]
sparc3d/models/autoencoders/blocks.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse PTv3 building blocks: norms, residual blocks, serialized attention, resampling.
2
+
3
+ Module and parameter names are load-bearing: they define the checkpoint key layout.
4
+ """
5
+
6
+ # Adapted from Pointcept / Point Transformer V3 (Xiaoyang Wu et al.) under the
7
+ # MIT License. Modified for SPARC3D: specialized for the sparse mesh
8
+ # autoencoder, with the training paths and drop-path removed.
9
+
10
+ from __future__ import annotations
11
+
12
+ import warnings
13
+
14
+ import torch
15
+ from torch import nn
16
+
17
+ from sparc3d.ops import sparse_backend
18
+ from sparc3d.structures.point import Point
19
+ from sparc3d.structures.point_modules import PointModule, PointSequential
20
+ from sparc3d.utils.misc import batch2offset, offset2bincount
21
+ from sparc3d.utils.precision import force_fp32
22
+
23
+
24
+ def _flash_attn():
25
+ """Import flash-attn when serialized attention is first constructed."""
26
+ try:
27
+ import flash_attn
28
+ except ImportError as exc:
29
+ raise ImportError(
30
+ "SparconvVAE serialized attention requires flash-attn; "
31
+ "install sparc3d[inference]"
32
+ ) from exc
33
+ return flash_attn
34
+
35
+
36
+ _RESNET_SKIP_POINTS = 12_000_000
37
+ """Default ``SparseResnetBlock.skip_points``: above this token count the residual
38
+ blocks pass the point through untouched and warn. The 1024 path can exceed what a
39
+ full residual pass fits in memory, and the released weights were measured with this
40
+ guard active, so it is reproduced behavior rather than a tuning knob.
41
+
42
+ Deliberately the same number as ``pipelines.sparc3d_pipeline.DECODE_FEATURE_CEILING``,
43
+ but a different policy: this one degrades a single block, while the pipeline raises
44
+ when a *completed* decode is too large to feed another pass."""
45
+
46
+ _GROUP_NORM_SPLIT = 8
47
+ """Groups normalized per iteration. Group statistics are independent, so this only
48
+ bounds the temporaries, not the result."""
49
+
50
+
51
+ class Swish(nn.Module):
52
+ """``x * sigmoid(x)``."""
53
+
54
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
55
+ """Apply the Swish activation elementwise."""
56
+ return x * torch.sigmoid(x)
57
+
58
+
59
+ class PointGroupNorm(PointModule):
60
+ """Group normalization over the points of each batch element of a sparse point cloud."""
61
+
62
+ def __init__(
63
+ self,
64
+ channels: int,
65
+ group: int = 32,
66
+ min_group_channels: int = 4,
67
+ eps: float = 1e-6,
68
+ ):
69
+ """
70
+ Args:
71
+ channels: Feature channels.
72
+ group: Requested number of groups; reduced when ``channels`` is too small
73
+ to give every group ``min_group_channels`` channels.
74
+ min_group_channels: Minimum channels per group.
75
+ eps: Variance floor.
76
+
77
+ Raises:
78
+ ValueError: ``channels`` is not divisible by the resolved group count.
79
+ """
80
+ super().__init__()
81
+ self.eps = eps
82
+ self.group = group
83
+ self.channels = channels
84
+ self.min_group_channels = min_group_channels
85
+
86
+ if self.min_group_channels * self.group > channels:
87
+ self.group = channels // self.min_group_channels
88
+ if self.group < 1 or channels % self.group != 0:
89
+ raise ValueError(
90
+ f"channels {channels} must be divisible by group {self.group}"
91
+ )
92
+ self.channels_per_group = channels // self.group
93
+
94
+ self.weights = nn.Parameter(torch.ones(1, channels))
95
+ self.bias = nn.Parameter(torch.zeros(1, channels))
96
+
97
+ @force_fp32(mode="convert_back")
98
+ def forward(self, point: Point) -> Point:
99
+ """Normalize ``point.feat`` per batch element and group, in float32."""
100
+ feat = point.feat
101
+ batch = point.batch.detach().clone()
102
+ N, C = feat.shape
103
+ B = int(batch[-1] + 1)
104
+
105
+ G = self.group
106
+ CP = self.channels_per_group
107
+ feat_group = feat.view(N, G, CP)
108
+ feat_norm_all = feat.new_zeros((N, G, CP))
109
+
110
+ for start in range(0, G, _GROUP_NORM_SPLIT):
111
+ end = min(start + _GROUP_NORM_SPLIT, G)
112
+
113
+ feat_slice = feat_group[:, start:end, :]
114
+ sum_feat = feat_slice.sum(dim=2)
115
+ sum_feat_sq = feat_slice.square().sum(dim=2)
116
+
117
+ # Per-(batch, group) moments, accumulated over the points of each element.
118
+ batch_idx = batch.view(-1, 1).expand(-1, end - start).long()
119
+ sums = feat.new_zeros((B, end - start)).scatter_add(0, batch_idx, sum_feat)
120
+ sums_sq = feat.new_zeros((B, end - start)).scatter_add(
121
+ 0, batch_idx, sum_feat_sq
122
+ )
123
+
124
+ counts = torch.bincount(batch, minlength=B).float().view(B, 1) * CP
125
+ mean = sums.div_(counts + self.eps)
126
+ var = sums_sq.div_(counts + self.eps).sub_(mean.square())
127
+ inv_std = torch.rsqrt(var + self.eps)
128
+
129
+ mean_exp = mean.index_select(0, batch).unsqueeze(2)
130
+ inv_std_exp = inv_std.index_select(0, batch).unsqueeze(2)
131
+ feat_norm_all[:, start:end, :] = feat_slice.sub(mean_exp).mul_(inv_std_exp)
132
+
133
+ del feat_slice, sum_feat, sum_feat_sq, sums, sums_sq
134
+ del mean, var, inv_std, mean_exp, inv_std_exp
135
+
136
+ feat_norm_all = feat_norm_all.view(N, C)
137
+ feat_norm_all.mul_(self.weights).add_(self.bias)
138
+
139
+ point.update_feat(feat_norm_all)
140
+ return point
141
+
142
+
143
+ class LayerNormFP32(nn.LayerNorm):
144
+ """Layer norm that always computes in float32, then restores the input dtype."""
145
+
146
+ @force_fp32(mode="convert_back")
147
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
148
+ """Normalize the last dimension of ``x`` in float32."""
149
+ return super().forward(x)
150
+
151
+
152
+ class MLP(nn.Module):
153
+ """Two-layer feed-forward block."""
154
+
155
+ def __init__(
156
+ self,
157
+ in_channels: int,
158
+ hidden_channels: int | None = None,
159
+ out_channels: int | None = None,
160
+ act_layer: type[nn.Module] = nn.GELU,
161
+ ):
162
+ """
163
+ Args:
164
+ in_channels: Input channels.
165
+ hidden_channels: Width of the hidden layer; defaults to ``in_channels``.
166
+ out_channels: Output channels; defaults to ``in_channels``.
167
+ act_layer: Activation constructed between the two linear layers.
168
+ """
169
+ super().__init__()
170
+ out_channels = out_channels or in_channels
171
+ hidden_channels = hidden_channels or in_channels
172
+ self.fc1 = nn.Linear(in_channels, hidden_channels)
173
+ self.act = act_layer()
174
+ self.fc2 = nn.Linear(hidden_channels, out_channels)
175
+
176
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
177
+ """Project ``x`` through the hidden layer and back."""
178
+ return self.fc2(self.act(self.fc1(x)))
179
+
180
+
181
+ class SparseClsHead(PointModule):
182
+ """Per-voxel classification head, used to predict octree occupancy on upsampling."""
183
+
184
+ def __init__(self, in_channels: int, out_channels: int):
185
+ """
186
+ Args:
187
+ in_channels: Input feature channels.
188
+ out_channels: Logits emitted per voxel; eight for octree occupancy.
189
+ """
190
+ super().__init__()
191
+ self.fc = PointSequential(
192
+ PointGroupNorm(in_channels),
193
+ Swish(),
194
+ sparse_backend.Conv3d(
195
+ in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
196
+ ),
197
+ )
198
+
199
+ @force_fp32(mode="convert")
200
+ def forward(self, point: Point) -> Point:
201
+ """Return a point whose ``feat`` holds the per-voxel occupancy logits."""
202
+ # Wrap only the tensors the head reads; cloning the whole point here would
203
+ # keep every intermediate of the calling stage alive.
204
+ head_point = Point(
205
+ dict(
206
+ feat=point.feat,
207
+ sparse_conv_feat=point.sparse_conv_feat,
208
+ batch=point.batch,
209
+ )
210
+ )
211
+ head_point.is_sparsified = True
212
+ return self.fc(head_point)
213
+
214
+
215
+ class ConvOut(PointModule):
216
+ """Output projection that splits off the eight per-corner SDF sign logits."""
217
+
218
+ def __init__(self, in_channels: int, out_channels: int):
219
+ """
220
+ Args:
221
+ in_channels: Input feature channels.
222
+ out_channels: Output channels, the first eight being the SDF sign logits.
223
+ """
224
+ super().__init__()
225
+ self.conv = PointSequential(
226
+ PointGroupNorm(in_channels),
227
+ Swish(),
228
+ sparse_backend.Conv3d(
229
+ in_channels,
230
+ out_channels,
231
+ kernel_size=3,
232
+ stride=1,
233
+ padding=1,
234
+ bias=True,
235
+ ),
236
+ )
237
+
238
+ @force_fp32(mode="convert")
239
+ def forward(self, point: Point) -> Point:
240
+ """Split the projection into ``point["sdf_sign"]`` and the remaining features."""
241
+ point = self.conv(point)
242
+ point["sdf_sign"] = point.feat[:, :8]
243
+ point.update_feat(point.feat[:, 8:])
244
+ return point
245
+
246
+
247
+ class SparseZToF(PointModule):
248
+ """Project latent channels back to decoder features."""
249
+
250
+ def __init__(self, in_channels: int, out_channels: int):
251
+ """
252
+ Args:
253
+ in_channels: Latent channels arriving from the transformer.
254
+ out_channels: Decoder feature channels at the coarsest stage.
255
+ """
256
+ super().__init__()
257
+ self.conv = PointSequential(
258
+ sparse_backend.Conv3d(
259
+ in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
260
+ )
261
+ )
262
+
263
+ @force_fp32(mode="enforce")
264
+ def forward(self, point: Point) -> Point:
265
+ """Project latent channels up to the decoder's feature width."""
266
+ return self.conv(point)
267
+
268
+
269
+ class SparseResnetBlock(PointModule):
270
+ """Pre-norm residual block of two sparse 3x3x3 convolutions."""
271
+
272
+ def __init__(
273
+ self,
274
+ in_channels: int,
275
+ out_channels: int,
276
+ skip_points: int = _RESNET_SKIP_POINTS,
277
+ ):
278
+ """
279
+ Args:
280
+ in_channels: Input channels.
281
+ out_channels: Output channels.
282
+ skip_points: Token count above which the block passes its input through
283
+ unchanged. Defaults to the product's own guard.
284
+ """
285
+ super().__init__()
286
+ self.skip_points = skip_points
287
+ self.block = PointSequential(
288
+ PointGroupNorm(in_channels),
289
+ Swish(),
290
+ sparse_backend.Conv3d(
291
+ in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
292
+ ),
293
+ PointGroupNorm(out_channels),
294
+ Swish(),
295
+ sparse_backend.Conv3d(
296
+ out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
297
+ ),
298
+ )
299
+
300
+ # A PointSequential shortcut would alias and then overwrite point.feat.
301
+ if in_channels != out_channels:
302
+ self.res_conv = nn.Linear(in_channels, out_channels)
303
+ else:
304
+ self.res_conv = None
305
+
306
+ def forward(self, point: Point) -> Point:
307
+ """Add the residual branch to ``point``, or pass it through when oversized."""
308
+ if point.feat.shape[0] > self.skip_points:
309
+ warnings.warn(
310
+ f"SparseResnetBlock skipped: {point.feat.shape[0]} points exceeds the "
311
+ f"skip_points budget of {self.skip_points}. The block is a no-op, so "
312
+ "the output is the input; raise skip_points to run it.",
313
+ stacklevel=2,
314
+ )
315
+ return point
316
+ if self.res_conv is not None:
317
+ shortcut = self.res_conv(point.feat)
318
+ else:
319
+ shortcut = point.feat
320
+ point.update_feat(shortcut + self.block(point).feat)
321
+ return point
322
+
323
+
324
+ class SerializedAttention(PointModule):
325
+ """Windowed self-attention over patches of a space-filling-curve ordering."""
326
+
327
+ def __init__(
328
+ self,
329
+ channels: int,
330
+ num_heads: int,
331
+ patch_size: int,
332
+ order_index: int = 0,
333
+ ):
334
+ """
335
+ Args:
336
+ channels: Feature channels; must be divisible by ``num_heads``.
337
+ num_heads: Attention heads.
338
+ patch_size: Points per attention window.
339
+ order_index: Which of the point's serialized orders to attend along.
340
+
341
+ Raises:
342
+ ValueError: ``channels`` is not divisible by ``num_heads``.
343
+ ImportError: flash-attn is not installed.
344
+ """
345
+ super().__init__()
346
+ if channels % num_heads != 0:
347
+ raise ValueError(
348
+ f"channels {channels} must be divisible by num_heads {num_heads}"
349
+ )
350
+ self._flash_attn = _flash_attn()
351
+ self.channels = channels
352
+ self.num_heads = num_heads
353
+ self.scale = (channels // num_heads)**-0.5
354
+ self.order_index = order_index
355
+ self.patch_size = patch_size
356
+
357
+ self.qkv = torch.nn.Linear(channels, channels * 3, bias=True)
358
+ self.proj = torch.nn.Linear(channels, channels)
359
+
360
+ @torch.no_grad()
361
+ def get_padding_and_inverse(self, point: Point):
362
+ """Return, and cache on ``point``, the patch padding maps and sequence bounds.
363
+
364
+ Each batch element is padded up to a whole number of patches by repeating its
365
+ last full patch, so attention never needs a mask.
366
+
367
+ Returns:
368
+ ``(pad, unpad, cu_seqlens)``: the padded gather index, its inverse, and
369
+ the cumulative patch boundaries flash attention expects.
370
+ """
371
+ pad_key = "pad"
372
+ unpad_key = "unpad"
373
+ cu_seqlens_key = "cu_seqlens_key"
374
+ if (
375
+ pad_key not in point.keys()
376
+ or unpad_key not in point.keys()
377
+ or cu_seqlens_key not in point.keys()
378
+ ):
379
+ offset = point.offset
380
+ bincount = offset2bincount(offset)
381
+ bincount_pad = (
382
+ torch.div(
383
+ bincount + self.patch_size - 1,
384
+ self.patch_size,
385
+ rounding_mode="trunc",
386
+ )
387
+ * self.patch_size
388
+ )
389
+ # Only pad an element when it has more points than one patch.
390
+ mask_pad = bincount > self.patch_size
391
+ bincount_pad = ~mask_pad * bincount + mask_pad * bincount_pad
392
+ _offset = nn.functional.pad(offset, (1, 0))
393
+ _offset_pad = nn.functional.pad(torch.cumsum(bincount_pad, dim=0), (1, 0))
394
+ pad = torch.arange(_offset_pad[-1], device=offset.device)
395
+ unpad = torch.arange(_offset[-1], device=offset.device)
396
+ cu_seqlens = []
397
+ for i in range(len(offset)):
398
+ # yapf: disable
399
+ unpad[_offset[i] : _offset[i + 1]] += _offset_pad[i] - _offset[i]
400
+ if bincount[i] != bincount_pad[i]:
401
+ pad[_offset_pad[i + 1] - self.patch_size + (bincount[i] % self.patch_size) : _offset_pad[i + 1]] = \
402
+ pad[_offset_pad[i + 1] - 2 * self.patch_size + (bincount[i] % self.patch_size) : _offset_pad[i + 1] - self.patch_size]
403
+ pad[_offset_pad[i] : _offset_pad[i + 1]] -= _offset_pad[i] - _offset[i]
404
+ # yapf: enable
405
+ cu_seqlens.append(
406
+ torch.arange(
407
+ _offset_pad[i],
408
+ _offset_pad[i + 1],
409
+ step=self.patch_size,
410
+ dtype=torch.int32,
411
+ device=offset.device,
412
+ )
413
+ )
414
+ point[pad_key] = pad
415
+ point[unpad_key] = unpad
416
+ point[cu_seqlens_key] = nn.functional.pad(
417
+ torch.concat(cu_seqlens), (0, 1), value=_offset_pad[-1]
418
+ )
419
+ return point[pad_key], point[unpad_key], point[cu_seqlens_key]
420
+
421
+ def forward(self, point: Point) -> Point:
422
+ """Attend within each patch of the serialized ordering and project the result."""
423
+ H = self.num_heads
424
+ C = self.channels
425
+
426
+ pad, unpad, cu_seqlens = self.get_padding_and_inverse(point)
427
+
428
+ order = point.serialized_order[self.order_index][pad]
429
+ inverse = unpad[point.serialized_inverse[self.order_index]]
430
+
431
+ qkv = self.qkv(point.feat)[order]
432
+
433
+ feat = self._flash_attn.flash_attn_varlen_qkvpacked_func(
434
+ qkv.half().reshape(-1, 3, H, C // H),
435
+ cu_seqlens,
436
+ max_seqlen=self.patch_size,
437
+ dropout_p=0,
438
+ softmax_scale=self.scale,
439
+ ).reshape(-1, C)
440
+ feat = feat.to(qkv.dtype)[inverse]
441
+
442
+ point.feat = self.proj(feat)
443
+ return point
444
+
445
+
446
+ class SparseAttentionBlock(PointModule):
447
+ """Serialized attention block with a sparse conditional position encoding and MLP."""
448
+
449
+ def __init__(
450
+ self,
451
+ channels: int,
452
+ num_heads: int,
453
+ patch_size: int = 48,
454
+ mlp_ratio: float = 4.0,
455
+ order_index: int = 0,
456
+ ):
457
+ """
458
+ Args:
459
+ channels: Feature channels, preserved across the block.
460
+ num_heads: Attention heads.
461
+ patch_size: Points per attention window.
462
+ mlp_ratio: Hidden width of the MLP as a multiple of ``channels``.
463
+ order_index: Which of the point's serialized orders to attend along.
464
+ """
465
+ super().__init__()
466
+ self.channels = channels
467
+
468
+ norm_layer = LayerNormFP32
469
+ act_layer = nn.GELU
470
+
471
+ self.cpe = PointSequential(
472
+ sparse_backend.Conv3d(channels, channels, kernel_size=3, bias=True),
473
+ nn.Linear(channels, channels),
474
+ norm_layer(channels),
475
+ )
476
+
477
+ self.norm1 = PointSequential(norm_layer(channels))
478
+ self.attn = SerializedAttention(
479
+ channels=channels,
480
+ patch_size=patch_size,
481
+ num_heads=num_heads,
482
+ order_index=order_index,
483
+ )
484
+ self.norm2 = PointSequential(norm_layer(channels))
485
+ self.mlp = PointSequential(
486
+ MLP(
487
+ in_channels=channels,
488
+ hidden_channels=int(channels * mlp_ratio),
489
+ out_channels=channels,
490
+ act_layer=act_layer,
491
+ )
492
+ )
493
+
494
+ def forward(self, point: Point) -> Point:
495
+ """Apply the position encoding, attention and MLP residual branches in turn."""
496
+ shortcut = point.feat
497
+ point = self.cpe(point)
498
+ point.feat = shortcut + point.feat
499
+ shortcut = point.feat
500
+ point = self.norm1(point)
501
+ point = self.attn(point)
502
+ point.feat = shortcut + point.feat
503
+
504
+ shortcut = point.feat
505
+ point = self.norm2(point)
506
+ point = self.mlp(point)
507
+ point.feat = shortcut + point.feat
508
+ # Everything above touched only feat; resync the sparse view once.
509
+ point.sparse_conv_feat = sparse_backend.replace_feats(
510
+ point.sparse_conv_feat, point.feat
511
+ )
512
+ return point
513
+
514
+
515
+ class SerializedUnpooling(PointModule):
516
+ """Double the grid resolution, keeping the children the occupancy head predicts."""
517
+
518
+ def __init__(self, channels: int):
519
+ """
520
+ Args:
521
+ channels: Feature channels, preserved across the resolution doubling.
522
+ """
523
+ super().__init__()
524
+ self.cls_head = SparseClsHead(channels, 8)
525
+ self.proj = nn.Linear(channels, channels * 8, bias=False)
526
+ self.inv_grid_coord = nn.Parameter(
527
+ torch.tensor(
528
+ [
529
+ [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1],
530
+ [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1],
531
+ ]
532
+ ),
533
+ requires_grad=False,
534
+ )
535
+
536
+ def forward(self, point: Point) -> Point:
537
+ """Split each voxel into eight children and keep the ones predicted occupied."""
538
+ feat = self.proj(point.feat)
539
+ pred_octree = self.cls_head(point).feat
540
+
541
+ m, c = feat.shape[0], feat.shape[1] // 8
542
+ mask = pred_octree.flatten() > 0
543
+
544
+ feat = feat.view(m * 8, c)[mask]
545
+ grid_coord = point.grid_coord.unsqueeze(1) * 2 + self.inv_grid_coord.unsqueeze(0)
546
+ grid_coord = grid_coord.view(-1, 3)[mask]
547
+ batch = point.batch.unsqueeze(1).expand(-1, 8).flatten()[mask]
548
+
549
+ point = point.clone(
550
+ feat=feat,
551
+ grid_coord=grid_coord,
552
+ resolution=point.resolution * 2,
553
+ batch=batch,
554
+ offset=batch2offset(batch),
555
+ )
556
+ point.sparsify()
557
+ return point
558
+
559
+
560
+ class Upsample(PointModule):
561
+ """Stage-boundary upsampling by serialized unpooling."""
562
+
563
+ def __init__(self, channels: int):
564
+ """
565
+ Args:
566
+ channels: Feature channels, preserved across the upsampling.
567
+ """
568
+ super().__init__()
569
+ self.upsample = SerializedUnpooling(channels)
570
+
571
+ def forward(self, point: Point) -> Point:
572
+ """Double the grid resolution of ``point``."""
573
+ # The point count grows up to eightfold here; release the freed stage first.
574
+ torch.cuda.empty_cache()
575
+ return self.upsample(point)
sparc3d/models/autoencoders/config.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit JSON-to-module construction for the sparse mesh autoencoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Mapping
9
+
10
+ import torch
11
+
12
+ from sparc3d.models.autoencoders.sparconv_vae import SparconvVAE
13
+ from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
14
+ from sparc3d.structures.point import Point
15
+
16
+ _SUPPORTED_TYPES: dict[str, type] = {
17
+ "SparconvVAE": SparconvVAE,
18
+ "PTv3MeshDecoder": PTv3MeshDecoder,
19
+ }
20
+ """The only ``type`` discriminators a runtime config may name."""
21
+
22
+ _SUB_CONFIG_TYPES: dict[str, str] = {"decoder_cfg": "PTv3MeshDecoder"}
23
+ """The ``type`` each ``SparconvVAE`` sub-config must name."""
24
+
25
+
26
+ def load_config(config: str | Path | Mapping[str, Any]) -> dict[str, Any]:
27
+ """Read a runtime config and return its model block.
28
+
29
+ Args:
30
+ config: Path to a JSON file, or an already-parsed mapping. Either the whole
31
+ document (with a ``model`` key) or the model block itself is accepted.
32
+
33
+ Returns:
34
+ The model block as a plain dict.
35
+
36
+ Raises:
37
+ ValueError: The document has no model block carrying a ``type``.
38
+ """
39
+ if isinstance(config, (str, Path)):
40
+ with Path(config).open("r", encoding="utf-8") as f:
41
+ document = json.load(f)
42
+ else:
43
+ document = dict(config)
44
+
45
+ model = document.get("model", document)
46
+ if not isinstance(model, Mapping) or "type" not in model:
47
+ raise ValueError("config has no 'model' block with a 'type' discriminator")
48
+ return dict(model)
49
+
50
+
51
+ def build_sparconv_vae(config: str | Path | Mapping[str, Any]) -> SparconvVAE:
52
+ """Instantiate a :class:`SparconvVAE` from a runtime config, without loading weights.
53
+
54
+ Args:
55
+ config: Runtime config, as accepted by :func:`load_config`.
56
+
57
+ Returns:
58
+ Model on the CPU in eval mode.
59
+
60
+ Raises:
61
+ ValueError: An unsupported ``type``, a missing sub-config, or an unknown
62
+ constructor key.
63
+ """
64
+ model_cfg = load_config(config)
65
+ _resolve_type(model_cfg, "model", "SparconvVAE")
66
+ # Training sections (losses, optimizer, scheduler) are rejected rather than
67
+ # ignored, so a training config can never be mistaken for a runtime artifact.
68
+ extra = sorted(set(model_cfg) - set(_SUB_CONFIG_TYPES))
69
+ if extra:
70
+ raise ValueError(f"model config has non-architecture keys: {extra}")
71
+
72
+ expected_attributes = sum(
73
+ Point.ALL_LAYOUTS[key]["size"] for key in ("sdf", "deform")
74
+ )
75
+ if "decoder_cfg" not in model_cfg:
76
+ raise ValueError("model config is missing 'decoder_cfg'")
77
+ sub_cfg = dict(model_cfg["decoder_cfg"])
78
+ cls = _resolve_type(sub_cfg, "decoder_cfg", "PTv3MeshDecoder")
79
+ kwargs = _validated_kwargs(cls, sub_cfg, "decoder_cfg")
80
+ output_channels = kwargs.get(
81
+ "out_channels", inspect.signature(cls).parameters["out_channels"].default
82
+ )
83
+ if output_channels != expected_attributes:
84
+ raise ValueError(
85
+ f"decoder out_channels must be {expected_attributes}, "
86
+ f"got {output_channels}"
87
+ )
88
+
89
+ model = SparconvVAE(decoder=cls(**kwargs))
90
+ model.eval()
91
+ return model
92
+
93
+
94
+ def load_sparconv_vae(
95
+ weights: str | Path,
96
+ config: str | Path | Mapping[str, Any] | None = None,
97
+ *,
98
+ device: str | torch.device = "cuda",
99
+ ) -> SparconvVAE:
100
+ """Build a :class:`SparconvVAE` and load its weights, requiring an exact key match.
101
+
102
+ Args:
103
+ weights: Path to a ``.safetensors`` file, or to a directory holding
104
+ ``model.safetensors`` and ``config.json``.
105
+ config: Runtime config; defaults to ``config.json`` beside the weights.
106
+ device: Device to move the model to.
107
+
108
+ Returns:
109
+ Model in eval mode on ``device``.
110
+
111
+ Raises:
112
+ FileNotFoundError: The weights, or the implied ``config.json``, are missing.
113
+ RuntimeError: The checkpoint keys do not match the model's parameters exactly.
114
+ """
115
+ from safetensors.torch import load_file
116
+
117
+ weights_path = Path(weights)
118
+ if weights_path.is_dir():
119
+ weights_path = weights_path / "model.safetensors"
120
+ if not weights_path.is_file():
121
+ raise FileNotFoundError(f"no weights at {weights_path}")
122
+
123
+ if config is None:
124
+ config = weights_path.parent / "config.json"
125
+ if not Path(config).is_file():
126
+ raise FileNotFoundError(
127
+ f"no config given and no config.json beside {weights_path}"
128
+ )
129
+
130
+ model = build_sparconv_vae(config)
131
+ model.load_state_dict(load_file(str(weights_path)), strict=True)
132
+ return model.to(device).eval()
133
+
134
+
135
+ def _resolve_type(cfg: dict[str, Any], where: str, expected: str) -> type:
136
+ """Pop the ``type`` discriminator and resolve it to its class."""
137
+ type_name = cfg.pop("type", None)
138
+ if type_name != expected:
139
+ raise ValueError(
140
+ f"{where}: unsupported type {type_name!r}; expected {expected!r}"
141
+ )
142
+ return _SUPPORTED_TYPES[type_name]
143
+
144
+
145
+ def _validated_kwargs(cls: type, cfg: dict[str, Any], where: str) -> dict[str, Any]:
146
+ """Reject config keys that are not constructor parameters of ``cls``."""
147
+ accepted = set(inspect.signature(cls).parameters)
148
+ unknown = sorted(set(cfg) - accepted)
149
+ if unknown:
150
+ raise ValueError(f"{where}: unknown keys for {cls.__name__}: {unknown}")
151
+ return cfg
sparc3d/models/autoencoders/ptv3.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse PTv3 mesh decoder."""
2
+
3
+ # Adapted from Pointcept / Point Transformer V3 under the MIT License.
4
+ # Modified for SPARC3D: the decoder half only, reading a latent grid instead of
5
+ # an encoded point cloud.
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Sequence
10
+
11
+ from sparc3d.models.autoencoders.blocks import (
12
+ ConvOut,
13
+ SparseAttentionBlock,
14
+ SparseResnetBlock,
15
+ SparseZToF,
16
+ Upsample,
17
+ )
18
+ from sparc3d.structures.point import Point
19
+ from sparc3d.structures.point_modules import PointModule, PointSequential
20
+
21
+
22
+ class PTv3MeshDecoder(PointModule):
23
+ """Decode a coarse latent point cloud back into per-corner SDF and deformation.
24
+
25
+ A serialized attention stack mixes globally at the coarsest level, then residual
26
+ stages double the resolution and prune children by predicted occupancy.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ out_channels: int = 32,
32
+ order: str | Sequence[str] = ("z", "z-trans"),
33
+ depths: Sequence[int] = (2, 2, 2, 2),
34
+ channels: Sequence[int] = (128, 256, 512, 512),
35
+ mid_depth: int = 2,
36
+ mid_num_head: int = 32,
37
+ z_channels: int = 32,
38
+ patch_size: int = 48,
39
+ mlp_ratio: float = 4,
40
+ ):
41
+ """
42
+ Args:
43
+ out_channels: Attribute channels emitted, on top of the eight sign logits.
44
+ order: Space-filling curve orders; attention blocks cycle through them.
45
+ depths: Residual blocks per stage.
46
+ channels: Feature width per stage.
47
+ mid_depth: Attention blocks at the coarsest level.
48
+ mid_num_head: Attention heads at the coarsest level.
49
+ z_channels: Latent channels consumed.
50
+ patch_size: Points per attention window.
51
+ mlp_ratio: Attention-block MLP expansion ratio.
52
+
53
+ Raises:
54
+ ValueError: ``depths`` and ``channels`` describe different stage counts.
55
+ """
56
+ super().__init__()
57
+
58
+ self.num_stages = len(depths)
59
+ self.order = [order] if isinstance(order, str) else list(order)
60
+
61
+ if len(channels) != self.num_stages:
62
+ raise ValueError(
63
+ f"channels has {len(channels)} entries but depths describes "
64
+ f"{self.num_stages} stages"
65
+ )
66
+
67
+ mid_ch = channels[-1]
68
+ self.z_to_f = SparseZToF(z_channels, mid_ch)
69
+
70
+ self.mid = PointSequential()
71
+ self.mid.add(SparseResnetBlock(mid_ch, mid_ch), name="block_in")
72
+ for i in range(mid_depth):
73
+ self.mid.add(
74
+ SparseAttentionBlock(
75
+ channels=mid_ch,
76
+ num_heads=mid_num_head,
77
+ patch_size=patch_size,
78
+ mlp_ratio=mlp_ratio,
79
+ order_index=i % len(self.order),
80
+ ),
81
+ name=f"block{i}",
82
+ )
83
+ self.mid.add(SparseResnetBlock(mid_ch, mid_ch), name="block_out")
84
+
85
+ self.dec = PointSequential()
86
+ for s in reversed(range(self.num_stages)):
87
+ dec = PointSequential()
88
+ block_in = channels[s]
89
+ block_out = channels[s - 1] if s > 0 else channels[0]
90
+ for i in range(depths[s]):
91
+ dec.add(SparseResnetBlock(block_in, block_out), name=f"block{i}")
92
+ block_in = block_out
93
+ if s != 0:
94
+ dec.add(Upsample(block_out), name="up")
95
+ self.dec.add(module=dec, name=f"dec{s}")
96
+
97
+ # Eight extra output channels carry the per-corner SDF sign logits.
98
+ self.conv_out = ConvOut(block_out, 8 + out_channels)
99
+
100
+ def forward(self, point: Point) -> Point:
101
+ """Decode latents into per-corner SDF signs, distances and deformations.
102
+
103
+ Args:
104
+ point: Sparse point carrying latent ``feat`` on its grid. Sparsified and
105
+ serialized in place if the caller has not done so already.
106
+
107
+ Returns:
108
+ The same point, with ``sdf_sign`` set and ``feat`` holding the remaining
109
+ per-corner attributes at the final resolution.
110
+ """
111
+ if not point.is_sparsified:
112
+ point.sparsify()
113
+ point = self.z_to_f(point)
114
+ if not point.is_serialized:
115
+ point.serialization(order=self.order)
116
+ point = self.mid(point)
117
+ point = self.dec(point)
118
+ return self.conv_out(point)
sparc3d/models/autoencoders/sparconv_vae.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse decoder over voxel-corner mesh attributes (``SparconvVAE``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Mapping
6
+
7
+ from torch import nn
8
+
9
+ from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
10
+ from sparc3d.structures.point import Point
11
+
12
+
13
+ class SparconvVAE(nn.Module):
14
+ """Sparse latent decoder over per-corner mesh attributes.
15
+
16
+ Named for the autoencoder the weights were trained as, but this is the decoder
17
+ alone: latents arrive from the diffusion transformer, so no encoder or Gaussian
18
+ bottleneck is reachable at inference and neither is included here.
19
+ """
20
+
21
+ def __init__(self, decoder: PTv3MeshDecoder):
22
+ """
23
+ Args:
24
+ decoder: Latent-to-attribute decoder.
25
+ """
26
+ super().__init__()
27
+ self.decoder = decoder
28
+
29
+ def decode(self, z_q: Point | Mapping) -> Point:
30
+ """Decode a latent point cloud into activated per-corner attributes.
31
+
32
+ Args:
33
+ z_q: Latent point cloud, or a diffusion output mapping with ``grid_coord``, ``feat``, ``resolution``, and a
34
+ batch descriptor.
35
+
36
+ Returns:
37
+ Decoded point cloud whose ``feat`` holds activated attributes and whose
38
+ ``sdf_sign`` holds the per-corner sign logits.
39
+ """
40
+ if not isinstance(z_q, Point):
41
+ data = dict(z_q)
42
+ data.setdefault("sdf", None)
43
+ data.setdefault("deform", None)
44
+ z_q = Point(data)
45
+ z_q.initialization()
46
+ elif "layouts" not in z_q or "matrix" not in z_q:
47
+ z_q.initialization()
48
+
49
+ if not {"sdf", "deform"}.issubset(z_q.layouts):
50
+ start = 0
51
+ layouts = {}
52
+ for key in ("sdf", "deform"):
53
+ entry = Point.ALL_LAYOUTS[key]
54
+ layouts[key] = {
55
+ **entry,
56
+ "range": (start, start + entry["size"]),
57
+ }
58
+ start += entry["size"]
59
+ z_q["layouts"] = layouts
60
+
61
+ x_hat = self.decoder(z_q)
62
+ x_hat.activate()
63
+ return x_hat
sparc3d/models/conditioners/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Image conditioning models."""
2
+
3
+ from sparc3d.models.conditioners.image import ImageConditioner
4
+
5
+ __all__ = ["ImageConditioner"]
sparc3d/models/conditioners/image.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared DINOv2 and CLIP image conditioner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from PIL import Image
12
+ from torch import Tensor, nn
13
+ from transformers import AutoModel, CLIPVisionModel
14
+
15
+ DINO_MODEL_ID = "facebook/dinov2-large"
16
+ """Moving Hugging Face repository used for DINOv2 conditioning."""
17
+
18
+ CLIP_MODEL_ID = "openai/clip-vit-large-patch14"
19
+ """Moving Hugging Face repository used for CLIP conditioning."""
20
+
21
+ _DINO_SIZE = 756
22
+ _CLIP_SIZE = 224
23
+
24
+
25
+ class ImageConditioner(nn.Module):
26
+ """Frozen fp32 image conditioner combining DINOv2 tokens and CLIP context."""
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ cache_dir: str | Path | None = None,
32
+ local_files_only: bool = False,
33
+ dino_model_id: str | Path = DINO_MODEL_ID,
34
+ clip_model_id: str | Path = CLIP_MODEL_ID,
35
+ ) -> None:
36
+ """Load and freeze the shared Hugging Face vision encoders.
37
+
38
+ Args:
39
+ cache_dir: Optional Hugging Face cache directory. This is the directory
40
+ that holds the ``models--*`` trees, i.e. ``$HF_HOME/hub`` rather
41
+ than ``$HF_HOME``.
42
+ local_files_only: Whether encoder loading must use cached files only.
43
+ dino_model_id: DINOv2 repository ID, or a local directory holding the
44
+ encoder. A directory needs no Hub cache layout, which is the
45
+ simplest way to run without network access.
46
+ clip_model_id: CLIP repository ID, or a local directory.
47
+ """
48
+ super().__init__()
49
+ load_kwargs: dict[str, Any] = {}
50
+ if cache_dir is not None:
51
+ load_kwargs["cache_dir"] = str(cache_dir)
52
+ if local_files_only:
53
+ load_kwargs["local_files_only"] = True
54
+ self.dino = AutoModel.from_pretrained(str(dino_model_id), **load_kwargs).float()
55
+ self.clip = CLIPVisionModel.from_pretrained(
56
+ str(clip_model_id), **load_kwargs
57
+ ).float()
58
+
59
+ for parameter in self.parameters():
60
+ parameter.requires_grad_(False)
61
+
62
+ self.register_buffer(
63
+ "_dino_mean",
64
+ torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32).view(1, 3, 1, 1),
65
+ persistent=False,
66
+ )
67
+ self.register_buffer(
68
+ "_dino_std",
69
+ torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32).view(1, 3, 1, 1),
70
+ persistent=False,
71
+ )
72
+ self.register_buffer(
73
+ "_clip_mean",
74
+ torch.tensor(
75
+ [0.48145466, 0.4578275, 0.40821073], dtype=torch.float32
76
+ ).view(1, 3, 1, 1),
77
+ persistent=False,
78
+ )
79
+ self.register_buffer(
80
+ "_clip_std",
81
+ torch.tensor(
82
+ [0.26862954, 0.26130258, 0.27577711], dtype=torch.float32
83
+ ).view(1, 3, 1, 1),
84
+ persistent=False,
85
+ )
86
+ self.eval()
87
+
88
+ def train(self, mode: bool = True) -> "ImageConditioner":
89
+ """Keep the conditioner and both frozen encoders in evaluation mode."""
90
+ del mode
91
+ return super().train(False)
92
+
93
+ @torch.no_grad()
94
+ def forward(self, image: Image.Image) -> Tensor:
95
+ """Encode one prepared PIL image into batch-1 conditioning tokens.
96
+
97
+ Args:
98
+ image: Prepared RGB PIL image. Only a single image is accepted.
99
+
100
+ Returns:
101
+ L2-normalized DINOv2 tokens concatenated with a broadcast CLIP
102
+ embedding, shaped ``[1, 2917, 2048]``.
103
+
104
+ Raises:
105
+ TypeError: ``image`` is not a PIL image.
106
+ ValueError: The encoders return an unexpected token or channel shape.
107
+ """
108
+ if not isinstance(image, Image.Image):
109
+ raise TypeError(
110
+ f"image must be one prepared PIL image, got {type(image).__name__}"
111
+ )
112
+
113
+ device = self._dino_mean.device
114
+ base_input = self._pil_tensor(image, device)
115
+ dino_input = F.interpolate(
116
+ base_input,
117
+ size=(_DINO_SIZE, _DINO_SIZE),
118
+ mode="bilinear",
119
+ align_corners=False,
120
+ )
121
+ clip_input = self._image_tensor(
122
+ image, _CLIP_SIZE, Image.Resampling.LANCZOS, device
123
+ )
124
+ dino_input = (dino_input - self._dino_mean) / self._dino_std
125
+ clip_input = (clip_input - self._clip_mean) / self._clip_std
126
+
127
+ # Disable an enclosing mixed-precision context because conditioner numerics
128
+ # are fixed to fp32 independently of transformer inference precision.
129
+ with torch.autocast(device_type=device.type, enabled=False):
130
+ dino_output: Any = self.dino(pixel_values=dino_input)
131
+ clip_output: Any = self.clip(pixel_values=clip_input)
132
+
133
+ dino_tokens = F.normalize(
134
+ dino_output.last_hidden_state.float(), p=2, dim=-1
135
+ )
136
+ clip_embedding = F.normalize(clip_output.pooler_output.float(), p=2, dim=-1)
137
+
138
+ if dino_tokens.shape != (1, 2917, 1024):
139
+ raise ValueError(
140
+ "DINOv2 output must have shape [1, 2917, 1024], "
141
+ f"got {list(dino_tokens.shape)}"
142
+ )
143
+ if clip_embedding.shape != (1, 1024):
144
+ raise ValueError(
145
+ "CLIP output must have shape [1, 1024], "
146
+ f"got {list(clip_embedding.shape)}"
147
+ )
148
+
149
+ clip_tokens = clip_embedding[:, None, :].expand(
150
+ -1, dino_tokens.shape[1], -1
151
+ )
152
+ return torch.cat((dino_tokens, clip_tokens), dim=-1)
153
+
154
+ @staticmethod
155
+ def _image_tensor(
156
+ image: Image.Image,
157
+ size: int,
158
+ resample: Image.Resampling,
159
+ device: torch.device,
160
+ ) -> Tensor:
161
+ rgb = image.convert("RGB").resize((size, size), resample=resample)
162
+ return ImageConditioner._pil_tensor(rgb, device)
163
+
164
+ @staticmethod
165
+ def _pil_tensor(image: Image.Image, device: torch.device) -> Tensor:
166
+ rgb = image.convert("RGB")
167
+ array = np.asarray(rgb, dtype=np.float32)
168
+ tensor = torch.from_numpy(array.copy()).permute(2, 0, 1).unsqueeze(0)
169
+ return tensor.to(device=device, dtype=torch.float32).div_(255.0)
sparc3d/models/transformers/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Sparse latent transformer and its validated runtime configuration."""
2
+
3
+ from .config import Sparc3DTransformerConfig
4
+ from .sparc3d_transformer import Sparc3DTransformer
5
+
6
+ __all__ = ["Sparc3DTransformer", "Sparc3DTransformerConfig"]
sparc3d/models/transformers/config.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validated configuration for the released sparse latent transformer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, fields
6
+ from typing import Any, Mapping
7
+
8
+
9
+ _LATENT_MEAN = (
10
+ -0.00155994,
11
+ 0.03314649,
12
+ -0.00307796,
13
+ 0.00630751,
14
+ -0.00835457,
15
+ -0.03860179,
16
+ 0.02528263,
17
+ 0.03740620,
18
+ -0.06209833,
19
+ -0.00539356,
20
+ -0.02110271,
21
+ 0.05126001,
22
+ 0.00753371,
23
+ 0.02866821,
24
+ 0.02772475,
25
+ -0.02131735,
26
+ -0.02799912,
27
+ 0.01084617,
28
+ 0.03433723,
29
+ -0.00903100,
30
+ -0.00121899,
31
+ -0.01944244,
32
+ 0.01641873,
33
+ -0.01063657,
34
+ -0.01990618,
35
+ 0.04856062,
36
+ 0.01747232,
37
+ -0.02387775,
38
+ 0.03216169,
39
+ -0.03732105,
40
+ -0.03730354,
41
+ -0.01948937,
42
+ )
43
+ """Per-channel mean of the released structured latent."""
44
+
45
+ _LATENT_STD = (
46
+ 0.61686146,
47
+ 0.80115320,
48
+ 0.85489200,
49
+ 0.96384716,
50
+ 0.77005910,
51
+ 0.62859850,
52
+ 0.61541015,
53
+ 0.74368310,
54
+ 0.62762610,
55
+ 1.01842280,
56
+ 0.77113867,
57
+ 0.62627990,
58
+ 0.68749195,
59
+ 0.62698966,
60
+ 0.75491240,
61
+ 0.61577400,
62
+ 0.63780660,
63
+ 0.80406000,
64
+ 0.83846325,
65
+ 0.69120390,
66
+ 0.69227580,
67
+ 0.79321617,
68
+ 0.63475930,
69
+ 0.63848300,
70
+ 0.67194235,
71
+ 0.73519380,
72
+ 0.84781533,
73
+ 0.93012910,
74
+ 0.62691440,
75
+ 0.67906080,
76
+ 0.65198770,
77
+ 0.77692040,
78
+ )
79
+ """Per-channel standard deviation of the released structured latent."""
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class Sparc3DTransformerConfig:
84
+ """Narrow architecture configuration for :class:`Sparc3DTransformer`.
85
+
86
+ Only the dimensions that actually vary between the two released checkpoints are
87
+ configurable. The architecture choices the weights were trained under -- rotary
88
+ position encoding, full flash attention, self-attention QK RMS normalization,
89
+ shared timestep modulation, and I/O skip connections -- are fixed in the model.
90
+ """
91
+
92
+ resolution: int = 64
93
+ """Structure-grid resolution represented by input coordinates."""
94
+
95
+ in_channels: int = 32
96
+ """Input latent feature channels."""
97
+
98
+ out_channels: int = 32
99
+ """Predicted latent feature channels."""
100
+
101
+ model_channels: int = 2112
102
+ """Transformer hidden width."""
103
+
104
+ cond_channels: int = 2048
105
+ """Image-conditioning width."""
106
+
107
+ num_blocks: int = 24
108
+ """Number of shared-modulation transformer blocks."""
109
+
110
+ num_heads: int = 22
111
+ """Number of attention heads."""
112
+
113
+ mlp_ratio: float = 4.0
114
+ """Expansion ratio of each feed-forward layer."""
115
+
116
+ patch_size: int = 2
117
+ """Sparse I/O downsampling factor."""
118
+
119
+ num_io_res_blocks: int = 2
120
+ """Residual blocks in each I/O stage."""
121
+
122
+ io_block_channels: tuple[int, ...] = (256,)
123
+ """Channels in each sparse I/O stage."""
124
+
125
+ use_fp16: bool = True
126
+ """Whether the transformer torso uses float16 parameters."""
127
+
128
+ latent_mean: tuple[float, ...] = _LATENT_MEAN
129
+ """Per-channel latent mean used by pipeline normalization."""
130
+
131
+ latent_std: tuple[float, ...] = _LATENT_STD
132
+ """Per-channel latent standard deviation used by pipeline normalization."""
133
+
134
+ def __post_init__(self) -> None:
135
+ positive = {
136
+ "resolution": self.resolution,
137
+ "in_channels": self.in_channels,
138
+ "out_channels": self.out_channels,
139
+ "model_channels": self.model_channels,
140
+ "cond_channels": self.cond_channels,
141
+ "num_blocks": self.num_blocks,
142
+ "num_heads": self.num_heads,
143
+ "patch_size": self.patch_size,
144
+ "num_io_res_blocks": self.num_io_res_blocks,
145
+ }
146
+ invalid = [name for name, value in positive.items() if value <= 0]
147
+ if invalid:
148
+ raise ValueError(f"positive dimensions required for {invalid}")
149
+ if self.model_channels % self.num_heads:
150
+ raise ValueError("model_channels must be divisible by num_heads")
151
+ head_dim = self.model_channels // self.num_heads
152
+ if head_dim % 2 or head_dim < 6:
153
+ raise ValueError("attention head dimension must be even and at least 6")
154
+ if self.patch_size & (self.patch_size - 1):
155
+ raise ValueError("patch_size must be a power of two")
156
+ if self.patch_size != 2 ** len(self.io_block_channels):
157
+ raise ValueError("I/O stage count must equal log2(patch_size)")
158
+ if any(channels <= 1 for channels in self.io_block_channels):
159
+ raise ValueError("I/O stage channels must be greater than one")
160
+ if len(self.latent_mean) != self.in_channels:
161
+ raise ValueError("latent_mean length must match in_channels")
162
+ if len(self.latent_std) != self.in_channels:
163
+ raise ValueError("latent_std length must match in_channels")
164
+ if any(value <= 0 for value in self.latent_std):
165
+ raise ValueError("latent_std values must be positive")
166
+
167
+ @classmethod
168
+ def from_dict(cls, values: Mapping[str, Any]) -> "Sparc3DTransformerConfig":
169
+ """Build a config from an allowlisted mapping.
170
+
171
+ Args:
172
+ values: Parsed transformer configuration.
173
+
174
+ Returns:
175
+ Validated config.
176
+
177
+ Raises:
178
+ ValueError: Unknown keys are present.
179
+ """
180
+ values = dict(values)
181
+ allowed = {item.name for item in fields(cls)}
182
+ unknown = sorted(set(values) - allowed)
183
+ if unknown:
184
+ raise ValueError(f"unknown transformer config keys: {unknown}")
185
+ for key in ("io_block_channels", "latent_mean", "latent_std"):
186
+ if key in values:
187
+ values[key] = tuple(values[key])
188
+ return cls(**values)
189
+
190
+ def to_dict(self) -> dict[str, Any]:
191
+ """Return a JSON-compatible configuration mapping."""
192
+ values = asdict(self)
193
+ for key in ("io_block_channels", "latent_mean", "latent_std"):
194
+ values[key] = list(values[key])
195
+ return values
sparc3d/models/transformers/sparc3d_transformer.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse latent flow transformer for image-conditioned mesh refinement."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D: the released sparse inference architecture.
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from typing import Any
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from torch import nn
14
+
15
+ from sparc3d.nn import LayerNorm32
16
+ from sparc3d.nn.sparse import (
17
+ SparseConv3d,
18
+ SparseDownsample,
19
+ SparseLinear,
20
+ SparseTensor,
21
+ SparseUpsample,
22
+ )
23
+ from sparc3d.nn.sparse.transformer import ModulatedSparseTransformerCrossBlock
24
+ from sparc3d.nn.utils import convert_module_to_f16, zero_module
25
+
26
+ from .config import Sparc3DTransformerConfig
27
+
28
+
29
+ class TimestepEmbedder(nn.Module):
30
+ """Sinusoidal scalar timestep embedding followed by a two-layer MLP."""
31
+
32
+ def __init__(self, hidden_size: int, frequency_embedding_size: int = 256):
33
+ """
34
+ Args:
35
+ hidden_size: Width of the embedding the MLP produces.
36
+ frequency_embedding_size: Sinusoidal features formed before the MLP.
37
+ """
38
+ super().__init__()
39
+ self.mlp = nn.Sequential(
40
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
41
+ nn.SiLU(),
42
+ nn.Linear(hidden_size, hidden_size, bias=True),
43
+ )
44
+ self.frequency_embedding_size = frequency_embedding_size
45
+
46
+ @staticmethod
47
+ def timestep_embedding(
48
+ timestep: torch.Tensor, dim: int, max_period: int = 10000
49
+ ) -> torch.Tensor:
50
+ """Create sinusoidal embeddings for scalar timesteps.
51
+
52
+ Args:
53
+ timestep: Fractional or integral timesteps ``[B]``.
54
+ dim: Embedding width.
55
+ max_period: Minimum-frequency denominator.
56
+
57
+ Returns:
58
+ Embeddings ``[B, dim]``.
59
+ """
60
+ half = dim // 2
61
+ frequencies = torch.exp(
62
+ -math.log(max_period) * torch.arange(half, dtype=torch.float32) / half
63
+ ).to(timestep.device)
64
+ arguments = timestep[:, None].float() * frequencies[None]
65
+ embedding = torch.cat((torch.cos(arguments), torch.sin(arguments)), dim=-1)
66
+ if dim % 2:
67
+ embedding = torch.cat(
68
+ (embedding, torch.zeros_like(embedding[:, :1])), dim=-1
69
+ )
70
+ return embedding
71
+
72
+ def forward(self, timestep: torch.Tensor) -> torch.Tensor:
73
+ """Embed ``timestep`` into the transformer hidden width."""
74
+ frequencies = self.timestep_embedding(timestep, self.frequency_embedding_size)
75
+ return self.mlp(frequencies)
76
+
77
+
78
+ class SparseResBlock3d(nn.Module):
79
+ """Timestep-modulated sparse residual block with optional paired resampling."""
80
+
81
+ def __init__(
82
+ self,
83
+ channels: int,
84
+ emb_channels: int,
85
+ out_channels: int | None = None,
86
+ *,
87
+ downsample: bool = False,
88
+ upsample: bool = False,
89
+ ):
90
+ """
91
+ Args:
92
+ channels: Input feature channels.
93
+ emb_channels: Timestep embedding channels consumed by the modulation.
94
+ out_channels: Output channels; defaults to ``channels``.
95
+ downsample: Halve the spatial resolution across the block.
96
+ upsample: Double the spatial resolution across the block.
97
+
98
+ Raises:
99
+ ValueError: Both ``downsample`` and ``upsample`` are set.
100
+ """
101
+ super().__init__()
102
+ if downsample and upsample:
103
+ raise ValueError("cannot downsample and upsample in one block")
104
+ self.channels = channels
105
+ self.emb_channels = emb_channels
106
+ self.out_channels = out_channels or channels
107
+ self.hidden_channels = self.out_channels // 2
108
+ self.downsample = downsample
109
+ self.upsample = upsample
110
+ self.norm1 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
111
+ self.norm2 = LayerNorm32(
112
+ self.hidden_channels, elementwise_affine=False, eps=1e-6
113
+ )
114
+ self.conv1 = SparseConv3d(channels, self.hidden_channels, 3)
115
+ self.conv2 = SparseConv3d(self.hidden_channels, self.hidden_channels, 3)
116
+ self.linear = zero_module(SparseLinear(self.hidden_channels, self.out_channels))
117
+ self.emb_layers = nn.Sequential(
118
+ nn.SiLU(),
119
+ nn.Linear(emb_channels, 2 * self.hidden_channels, bias=True),
120
+ )
121
+ self.skip_connection = (
122
+ SparseLinear(channels, self.out_channels)
123
+ if channels != self.out_channels
124
+ else nn.Identity()
125
+ )
126
+ self.updown: nn.Module | None = None
127
+ if downsample:
128
+ self.updown = SparseDownsample(2)
129
+ elif upsample:
130
+ self.updown = SparseUpsample(2)
131
+
132
+ def forward(self, x: SparseTensor, embedding: torch.Tensor) -> SparseTensor:
133
+ """Apply residual convolution and optional paired resampling."""
134
+ scale, shift = self.emb_layers(embedding).to(x.dtype).chunk(2, dim=1)
135
+ if self.updown is not None:
136
+ x = self.updown(x)
137
+ h = x.replace(self.norm1(x.feats))
138
+ h = h.replace(F.silu(h.feats))
139
+ h = self.conv1(h)
140
+ h = h.replace(self.norm2(h.feats)) * (1 + scale) + shift
141
+ h = h.replace(F.silu(h.feats))
142
+ h = self.conv2(h)
143
+ h = h.replace(F.silu(h.feats))
144
+ return self.linear(h) + self.skip_connection(x)
145
+
146
+
147
+ class Sparc3DTransformer(nn.Module):
148
+ """Released sparse latent flow transformer with a validated narrow config."""
149
+
150
+ def __init__(
151
+ self,
152
+ config: Sparc3DTransformerConfig | None = None,
153
+ **config_values: Any,
154
+ ):
155
+ """
156
+ Args:
157
+ config: Validated configuration. Mutually exclusive with ``config_values``.
158
+ **config_values: Configuration fields to validate into a config instead.
159
+
160
+ Raises:
161
+ ValueError: Both a config and configuration values were given, or a value
162
+ fails validation.
163
+ """
164
+ super().__init__()
165
+ if config is not None and config_values:
166
+ raise ValueError("pass either config or configuration keyword values")
167
+ self.config = config or Sparc3DTransformerConfig.from_dict(config_values)
168
+ config = self.config
169
+ self.dtype = torch.float16 if config.use_fp16 else torch.float32
170
+
171
+ channels = list(config.io_block_channels)
172
+ self.t_embedder = TimestepEmbedder(config.model_channels)
173
+ self.adaLN_modulation = nn.Sequential(
174
+ nn.SiLU(),
175
+ nn.Linear(config.model_channels, 6 * config.model_channels, bias=True),
176
+ )
177
+ self.input_layer = SparseLinear(config.in_channels, channels[0])
178
+ self.input_blocks = nn.ModuleList()
179
+ for current, following in zip(channels, channels[1:] + [config.model_channels]):
180
+ self.input_blocks.extend(
181
+ SparseResBlock3d(current, config.model_channels)
182
+ for _ in range(config.num_io_res_blocks - 1)
183
+ )
184
+ self.input_blocks.append(
185
+ SparseResBlock3d(
186
+ current,
187
+ config.model_channels,
188
+ out_channels=following,
189
+ downsample=True,
190
+ )
191
+ )
192
+
193
+ self.blocks = nn.ModuleList(
194
+ ModulatedSparseTransformerCrossBlock(
195
+ config.model_channels,
196
+ config.cond_channels,
197
+ config.num_heads,
198
+ mlp_ratio=config.mlp_ratio,
199
+ )
200
+ for _ in range(config.num_blocks)
201
+ )
202
+
203
+ self.out_blocks = nn.ModuleList()
204
+ previous_channels = [config.model_channels] + list(reversed(channels[1:]))
205
+ for current, previous in zip(reversed(channels), previous_channels):
206
+ self.out_blocks.append(
207
+ SparseResBlock3d(
208
+ previous * 2,
209
+ config.model_channels,
210
+ out_channels=current,
211
+ upsample=True,
212
+ )
213
+ )
214
+ self.out_blocks.extend(
215
+ SparseResBlock3d(
216
+ current * 2,
217
+ config.model_channels,
218
+ out_channels=current,
219
+ )
220
+ for _ in range(config.num_io_res_blocks - 1)
221
+ )
222
+ self.out_layer = SparseLinear(channels[0], config.out_channels)
223
+
224
+ self.initialize_weights()
225
+ if config.use_fp16:
226
+ self.convert_to_fp16()
227
+
228
+ def initialize_weights(self) -> None:
229
+ """Initialize linear, timestep, modulation, and output projections."""
230
+
231
+ def initialize_linear(module: nn.Module) -> None:
232
+ if isinstance(module, nn.Linear):
233
+ nn.init.xavier_uniform_(module.weight)
234
+ if module.bias is not None:
235
+ nn.init.constant_(module.bias, 0)
236
+
237
+ self.apply(initialize_linear)
238
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
239
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
240
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
241
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
242
+ nn.init.constant_(self.out_layer.weight, 0)
243
+ nn.init.constant_(self.out_layer.bias, 0)
244
+
245
+ def convert_to_fp16(self) -> None:
246
+ """Convert the I/O and transformer torso to float16."""
247
+ self.input_blocks.apply(convert_module_to_f16)
248
+ self.blocks.apply(convert_module_to_f16)
249
+ self.out_blocks.apply(convert_module_to_f16)
250
+ self.dtype = torch.float16
251
+
252
+ def forward(
253
+ self, x: SparseTensor, timestep: torch.Tensor, cond: torch.Tensor
254
+ ) -> SparseTensor:
255
+ """Predict latent flow for sparse ``x`` under image ``cond``."""
256
+ if x.shape[0] != timestep.shape[0] or x.shape[0] != cond.shape[0]:
257
+ raise ValueError("sparse input, timestep, and condition batches must match")
258
+ if x.feats.shape[1] != self.config.in_channels:
259
+ raise ValueError(
260
+ f"expected {self.config.in_channels} input channels, "
261
+ f"got {x.feats.shape[1]}"
262
+ )
263
+ h = self.input_layer(x).type(self.dtype)
264
+ timestep_embedding = self.t_embedder(timestep)
265
+ convolution_embedding = timestep_embedding.to(self.dtype)
266
+ skips = []
267
+ for block in self.input_blocks:
268
+ h = block(h, convolution_embedding)
269
+ skips.append(h.feats)
270
+
271
+ modulation = self.adaLN_modulation(timestep_embedding).to(self.dtype)
272
+ context = cond.to(self.dtype)
273
+ for block in self.blocks:
274
+ h = block(h, modulation, context)
275
+
276
+ for block, skip in zip(self.out_blocks, reversed(skips)):
277
+ h = h.replace(torch.cat((h.feats, skip), dim=1))
278
+ h = block(h, convolution_embedding)
279
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
280
+ return self.out_layer(h.type(x.dtype))
sparc3d/nn/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Private neural network primitives for the sparse latent transformer."""
2
+
3
+ from .norm import LayerNorm32
4
+
5
+ __all__ = ["LayerNorm32"]
sparc3d/nn/norm.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Numerically stable normalization layers for the sparse transformer."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain float32 layer normalization only.
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+
12
+ class LayerNorm32(nn.LayerNorm):
13
+ """Layer normalization with float32 statistics and input-dtype output."""
14
+
15
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
16
+ """Normalize ``x`` in float32 and restore its dtype."""
17
+ return super().forward(x.float()).to(x.dtype)
sparc3d/nn/rope.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Spatial rotary position embedding shared by the sparse attention blocks."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain long-grid frequency scaling only.
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+ import torch
11
+ from torch import nn
12
+
13
+
14
+ class RotaryPositionEmbedder(nn.Module):
15
+ """Spatial rotary embedding with frequency scaling for long coordinate grids."""
16
+
17
+ def __init__(self, hidden_size: int, in_channels: int = 3):
18
+ """
19
+ Args:
20
+ hidden_size: Head dimension the rotation is applied over; must be even.
21
+ in_channels: Spatial coordinate axes sharing that dimension.
22
+
23
+ Raises:
24
+ ValueError: ``hidden_size`` is odd or too small to split across axes.
25
+ """
26
+ super().__init__()
27
+ if hidden_size % 2:
28
+ raise ValueError("hidden_size must be divisible by 2")
29
+ if hidden_size // in_channels // 2 == 0:
30
+ raise ValueError("hidden_size is too small for spatial RoPE")
31
+ self.hidden_size = hidden_size
32
+ self.in_channels = in_channels
33
+ self.freq_dim = hidden_size // in_channels // 2
34
+ freqs = torch.arange(self.freq_dim, dtype=torch.float32) / self.freq_dim
35
+ self.register_buffer("freqs", 1.0 / (10000**freqs), persistent=False)
36
+
37
+ @staticmethod
38
+ def _update_freqs(
39
+ freqs: torch.Tensor, train_length: int, infer_length: int
40
+ ) -> torch.Tensor:
41
+ wavelengths = freqs * train_length / (2 * math.pi)
42
+ threshold = 24
43
+ ratio = (wavelengths - 1) / (threshold - 1)
44
+ ratio = ratio + (1 - ratio) * train_length / infer_length
45
+ ratio = torch.where(wavelengths > threshold, 1.0, ratio)
46
+ ratio = torch.where(wavelengths < 1, train_length / infer_length, ratio)
47
+ return freqs * ratio
48
+
49
+ def _get_phases(self, indices: torch.Tensor) -> torch.Tensor:
50
+ max_length = int(indices.max().item())
51
+ if max_length < 64:
52
+ freqs = self.freqs
53
+ else:
54
+ freqs = self._update_freqs(
55
+ self.freqs, train_length=64, infer_length=(64 + max_length) // 2
56
+ )
57
+ phases = torch.outer(indices, freqs.to(indices.device))
58
+ return torch.polar(torch.ones_like(phases), phases)
59
+
60
+ @staticmethod
61
+ def _rotary_embedding(x: torch.Tensor, phases: torch.Tensor) -> torch.Tensor:
62
+ x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
63
+ rotated = x_complex * phases.unsqueeze(1)
64
+ return torch.view_as_real(rotated).reshape(*rotated.shape[:-1], -1).to(x.dtype)
65
+
66
+ def forward(
67
+ self,
68
+ q: torch.Tensor,
69
+ k: torch.Tensor,
70
+ indices: torch.Tensor,
71
+ ) -> tuple[torch.Tensor, torch.Tensor]:
72
+ """Rotate queries and keys at ``indices``.
73
+
74
+ Args:
75
+ q: Queries ending in ``[H, D]``.
76
+ k: Keys with the same shape as ``q``.
77
+ indices: Spatial positions ending in three coordinates.
78
+
79
+ Returns:
80
+ Rotated queries and keys.
81
+ """
82
+ phases = self._get_phases(indices.reshape(-1)).reshape(*indices.shape[:-1], -1)
83
+ missing = self.hidden_size // 2 - phases.shape[-1]
84
+ if missing > 0:
85
+ padding = torch.polar(
86
+ torch.ones(*phases.shape[:-1], missing, device=phases.device),
87
+ torch.zeros(*phases.shape[:-1], missing, device=phases.device),
88
+ )
89
+ phases = torch.cat((phases, padding), dim=-1)
90
+ return (
91
+ self._rotary_embedding(q, phases),
92
+ self._rotary_embedding(k, phases),
93
+ )
sparc3d/nn/sparse/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Private sparse tensor and neural network operations."""
2
+
3
+ from .basic import SparseTensor
4
+ from .conv import SparseConv3d
5
+ from .linear import SparseLinear
6
+ from .spatial import SparseDownsample, SparseUpsample
7
+
8
+ __all__ = [
9
+ "SparseConv3d",
10
+ "SparseDownsample",
11
+ "SparseLinear",
12
+ "SparseTensor",
13
+ "SparseUpsample",
14
+ ]
sparc3d/nn/sparse/attention/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Full flash attention over sparse variable-length sequences."""
2
+
3
+ from .modules import SparseMultiHeadAttention
4
+
5
+ __all__ = ["SparseMultiHeadAttention"]
sparc3d/nn/sparse/attention/full_attn.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Variable-length full flash attention for sparse feature sequences."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D: variable-length flash attention only.
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+
10
+ from ..basic import SparseTensor
11
+
12
+
13
+ def _flash_attn():
14
+ """Import flash-attn when sparse attention is first executed."""
15
+ try:
16
+ import flash_attn
17
+ except ImportError as exc:
18
+ raise ImportError(
19
+ "Sparc3DTransformer attention requires flash-attn; "
20
+ "install sparc3d[inference]"
21
+ ) from exc
22
+ return flash_attn
23
+
24
+
25
+ def _lengths(tensor: SparseTensor) -> list[int]:
26
+ return [span.stop - span.start for span in tensor.layout]
27
+
28
+
29
+ def _cumulative(lengths: list[int], device: torch.device) -> torch.Tensor:
30
+ return torch.tensor(
31
+ [0, *lengths], device=device, dtype=torch.int32
32
+ ).cumsum(0, dtype=torch.int32)
33
+
34
+
35
+ def sparse_scaled_dot_product_attention(
36
+ query: SparseTensor, kv: torch.Tensor | None = None
37
+ ) -> SparseTensor:
38
+ """Apply variable-length full flash attention over sparse queries.
39
+
40
+ Args:
41
+ query: Packed sparse ``qkv`` of shape ``[N, 3, H, D]`` for self-attention,
42
+ or sparse queries ``[N, H, D]`` when ``kv`` is given.
43
+ kv: Dense packed keys and values ``[B, L, 2, H, D]`` for cross-attention;
44
+ ``None`` selects self-attention over the packed ``query``.
45
+
46
+ Returns:
47
+ Sparse output over the query's coordinates.
48
+
49
+ Raises:
50
+ TypeError: ``query`` is not sparse.
51
+ ValueError: ``kv`` does not share the query's batch size.
52
+ """
53
+ flash_attn = _flash_attn()
54
+ if not isinstance(query, SparseTensor):
55
+ raise TypeError("Sparc3D sparse attention requires sparse queries")
56
+ query_lengths = _lengths(query)
57
+ cumulative_query = _cumulative(query_lengths, query.device)
58
+
59
+ if kv is None:
60
+ output = flash_attn.flash_attn_varlen_qkvpacked_func(
61
+ query.feats, cumulative_query, max(query_lengths)
62
+ )
63
+ return query.replace(output)
64
+
65
+ batch, length = kv.shape[:2]
66
+ if batch != query.shape[0]:
67
+ raise ValueError("query and context batch sizes must match")
68
+ key_lengths = [length] * batch
69
+ output = flash_attn.flash_attn_varlen_kvpacked_func(
70
+ query.feats,
71
+ kv.reshape(batch * length, *kv.shape[2:]),
72
+ cumulative_query,
73
+ _cumulative(key_lengths, query.device),
74
+ max(query_lengths),
75
+ max(key_lengths),
76
+ )
77
+ return query.replace(output)
sparc3d/nn/sparse/attention/modules.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Full multi-head attention over sparse variable-length batches."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D: full flash attention with spatial RoPE.
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from torch import nn
11
+
12
+ from ...rope import RotaryPositionEmbedder
13
+ from ..basic import SparseTensor
14
+ from .full_attn import sparse_scaled_dot_product_attention
15
+
16
+
17
+ class SparseMultiHeadRMSNorm(nn.Module):
18
+ """Per-head RMS normalization of a ``[N, H, D]`` query or key projection."""
19
+
20
+ def __init__(self, dim: int, heads: int):
21
+ """
22
+ Args:
23
+ dim: Channels per head.
24
+ heads: Attention heads, each with its own gain.
25
+ """
26
+ super().__init__()
27
+ self.scale = dim**0.5
28
+ self.gamma = nn.Parameter(torch.ones(heads, dim))
29
+
30
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
31
+ """Normalize the head dimension in float32 and restore ``x``'s dtype."""
32
+ return (F.normalize(x.float(), dim=-1) * self.gamma * self.scale).to(x.dtype)
33
+
34
+
35
+ class SparseMultiHeadAttention(nn.Module):
36
+ """Full flash self- or cross-attention over sparse queries."""
37
+
38
+ def __init__(
39
+ self,
40
+ channels: int,
41
+ num_heads: int,
42
+ ctx_channels: int | None = None,
43
+ *,
44
+ attention_type: str = "self",
45
+ ):
46
+ """
47
+ Args:
48
+ channels: Query feature channels; must be divisible by ``num_heads``.
49
+ num_heads: Attention heads.
50
+ ctx_channels: Context channels for cross-attention; defaults to
51
+ ``channels``. Ignored for self-attention.
52
+ attention_type: ``"self"`` or ``"cross"``. Self-attention alone carries
53
+ rotary embeddings and query/key RMS normalization, matching the
54
+ parameters the released weights provide.
55
+
56
+ Raises:
57
+ ValueError: ``channels`` is not divisible by ``num_heads``, or
58
+ ``attention_type`` is not one of the two supported kinds.
59
+ """
60
+ super().__init__()
61
+ if channels % num_heads:
62
+ raise ValueError("channels must be divisible by num_heads")
63
+ if attention_type not in {"self", "cross"}:
64
+ raise ValueError("attention_type must be 'self' or 'cross'")
65
+ self.channels = channels
66
+ self.ctx_channels = ctx_channels or channels
67
+ self.num_heads = num_heads
68
+ self.head_dim = channels // num_heads
69
+ self._type = attention_type
70
+ if attention_type == "self":
71
+ self.to_qkv = nn.Linear(channels, channels * 3, bias=True)
72
+ # Self-attention alone is positional and query/key normalized; the
73
+ # released weights carry no RoPE or RMS parameters on the cross path.
74
+ self.q_rms_norm = SparseMultiHeadRMSNorm(self.head_dim, num_heads)
75
+ self.k_rms_norm = SparseMultiHeadRMSNorm(self.head_dim, num_heads)
76
+ self.rope = RotaryPositionEmbedder(self.head_dim)
77
+ else:
78
+ self.to_q = nn.Linear(channels, channels, bias=True)
79
+ self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=True)
80
+ self.to_out = nn.Linear(channels, channels)
81
+
82
+ @staticmethod
83
+ def _linear(
84
+ module: nn.Linear, x: SparseTensor | torch.Tensor
85
+ ) -> SparseTensor | torch.Tensor:
86
+ if isinstance(x, SparseTensor):
87
+ return x.replace(module(x.feats))
88
+ return module(x)
89
+
90
+ def _fused_pre(
91
+ self, x: SparseTensor | torch.Tensor, count: int
92
+ ) -> SparseTensor | torch.Tensor:
93
+ if isinstance(x, SparseTensor):
94
+ return x.reshape(count, self.num_heads, self.head_dim)
95
+ return x.reshape(*x.shape[:2], count, self.num_heads, self.head_dim)
96
+
97
+ def forward(
98
+ self,
99
+ x: SparseTensor,
100
+ context: SparseTensor | torch.Tensor | None = None,
101
+ ) -> SparseTensor:
102
+ """Attend from sparse ``x`` to itself or dense ``context``."""
103
+ if self._type == "self":
104
+ qkv = self._fused_pre(self._linear(self.to_qkv, x), 3)
105
+ assert isinstance(qkv, SparseTensor)
106
+ q, k, v = qkv.feats.unbind(dim=1)
107
+ q, k = self.rope(q, k, qkv.coords[:, 1:])
108
+ q = self.q_rms_norm(q)
109
+ k = self.k_rms_norm(k)
110
+ qkv = qkv.replace(torch.stack((q, k, v), dim=1))
111
+ output = sparse_scaled_dot_product_attention(qkv)
112
+ else:
113
+ if context is None:
114
+ raise ValueError("cross-attention requires context")
115
+ query = self._linear(self.to_q, x)
116
+ assert isinstance(query, SparseTensor)
117
+ query = query.reshape(self.num_heads, self.head_dim)
118
+ kv = self._fused_pre(self._linear(self.to_kv, context), 2)
119
+ output = sparse_scaled_dot_product_attention(query, kv)
120
+ output = output.reshape(-1)
121
+ projected = self._linear(self.to_out, output)
122
+ assert isinstance(projected, SparseTensor)
123
+ return projected
sparc3d/nn/sparse/basic.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Private sparse tensor wrapper with contiguous batches and spatial caches."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to use the local sparse backend and paired resampling cache.
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+ import torch
12
+
13
+ from sparc3d.ops import sparse_backend
14
+
15
+
16
+ class SparseTensor:
17
+ """Sparse features with batch-first coordinates and shared spatial caches."""
18
+
19
+ def __init__(
20
+ self,
21
+ feats: torch.Tensor | None = None,
22
+ coords: torch.Tensor | None = None,
23
+ shape: torch.Size | None = None,
24
+ layout: list[slice] | None = None,
25
+ *,
26
+ data: Any | None = None,
27
+ scale: tuple[int, ...] = (1, 1, 1),
28
+ spatial_cache: dict[str, dict[str, Any]] | None = None,
29
+ ):
30
+ """
31
+ Args:
32
+ feats: Features ``[N, C]``, one row per occupied coordinate.
33
+ coords: Coordinates ``[N, 4]`` as ``(batch, x, y, z)``, sorted by batch.
34
+ shape: Logical shape; derived from ``coords`` and ``feats`` when omitted.
35
+ layout: Per-batch row slices; recomputed from ``coords`` when omitted.
36
+ data: Backend sparse tensor to adopt instead of ``feats`` and ``coords``.
37
+ scale: Cumulative downsampling factor relative to the original grid, so
38
+ ``(1, 1, 1)`` is the original resolution and each downsample
39
+ multiplies it. It also keys :attr:`_spatial_cache`, so it has to
40
+ differ between nesting levels.
41
+ spatial_cache: Cache shared with tensors over the same geometry, which is
42
+ how a downsample hands its inverse mapping to the paired upsample.
43
+
44
+ Raises:
45
+ ValueError: Both ``data`` and explicit features or coordinates were given,
46
+ either is missing, the shapes disagree, or the tensor would be empty.
47
+ """
48
+ if data is not None:
49
+ if feats is not None or coords is not None:
50
+ raise ValueError("provide either data or feats and coords")
51
+ feats = sparse_backend.get_feats(data)
52
+ coords = data.indices
53
+ if feats is None or coords is None:
54
+ raise ValueError("feats and coords are required")
55
+ if coords.ndim != 2 or coords.shape[1] != 4:
56
+ raise ValueError(f"expected coords [N, 4], got {tuple(coords.shape)}")
57
+ if feats.shape[0] != coords.shape[0]:
58
+ raise ValueError("features and coordinates must have the same row count")
59
+ if coords.shape[0] == 0:
60
+ raise ValueError("a SparseTensor cannot be empty")
61
+ self._feats = feats
62
+ self._coords = coords.int().contiguous()
63
+ self.data = data
64
+ batch_size = int(self._coords[:, 0].max().item()) + 1
65
+ self._shape = shape or torch.Size((batch_size, *feats.shape[1:]))
66
+ self._layout = layout or self._calculate_layout(self._coords, batch_size)
67
+ self._scale = tuple(scale)
68
+ self._spatial_cache = spatial_cache if spatial_cache is not None else {}
69
+ self._validate_layout()
70
+
71
+ @staticmethod
72
+ def _calculate_layout(coords: torch.Tensor, batch_size: int) -> list[slice]:
73
+ counts = torch.bincount(coords[:, 0].long(), minlength=batch_size)
74
+ offsets = torch.cumsum(counts, dim=0)
75
+ return [
76
+ slice(int(offsets[i] - counts[i]), int(offsets[i]))
77
+ for i in range(batch_size)
78
+ ]
79
+
80
+ def _validate_layout(self) -> None:
81
+ for batch, span in enumerate(self._layout):
82
+ if not torch.all(self._coords[span, 0] == batch):
83
+ raise ValueError("coordinates for each batch must be contiguous")
84
+
85
+ def backend_data(self) -> Any:
86
+ """Return the sparse-backend tensor, creating it lazily."""
87
+ if self.data is None:
88
+ self.data = sparse_backend.sparse_tensor(self._feats, self._coords)
89
+ return self.data
90
+
91
+ @property
92
+ def shape(self) -> torch.Size:
93
+ """Logical shape ``[B, *feature_shape]``."""
94
+ return self._shape
95
+
96
+ @property
97
+ def layout(self) -> list[slice]:
98
+ """Contiguous feature slice for each batch element."""
99
+ return self._layout
100
+
101
+ @property
102
+ def feats(self) -> torch.Tensor:
103
+ """Feature values in coordinate-row order."""
104
+ return self._feats
105
+
106
+ @property
107
+ def coords(self) -> torch.Tensor:
108
+ """Batch-first integer coordinates in ``[batch, x, y, z]`` order."""
109
+ return self._coords
110
+
111
+ @property
112
+ def dtype(self) -> torch.dtype:
113
+ """Feature dtype."""
114
+ return self.feats.dtype
115
+
116
+ @property
117
+ def device(self) -> torch.device:
118
+ """Feature device."""
119
+ return self.feats.device
120
+
121
+ @property
122
+ def scale(self) -> tuple[int, ...]:
123
+ """Current spatial scale tracked by paired resampling."""
124
+ return self._scale
125
+
126
+ def replace(self, feats: torch.Tensor) -> "SparseTensor":
127
+ """Return a tensor sharing coordinates and caches with new features."""
128
+ if feats.shape[0] != self.feats.shape[0]:
129
+ raise ValueError("replacement features must preserve the sparse row count")
130
+ data = None
131
+ if self.data is not None and feats.ndim == 2:
132
+ data = sparse_backend.replace_feats(self.data, feats)
133
+ return SparseTensor(
134
+ feats=feats if data is None else None,
135
+ coords=self.coords if data is None else None,
136
+ data=data,
137
+ shape=torch.Size((self.shape[0], *feats.shape[1:])),
138
+ layout=self.layout,
139
+ scale=self._scale,
140
+ spatial_cache=self._spatial_cache,
141
+ )
142
+
143
+ def reshape(self, *shape: int) -> "SparseTensor":
144
+ """Reshape feature dimensions while retaining the sparse coordinates."""
145
+ return self.replace(self.feats.reshape(self.feats.shape[0], *shape))
146
+
147
+ def type(self, dtype: torch.dtype) -> "SparseTensor":
148
+ """Cast features to ``dtype``."""
149
+ return self.replace(self.feats.to(dtype=dtype))
150
+
151
+ def register_spatial_cache(self, key: str, value: Any) -> None:
152
+ """Store ``value`` under ``key`` for the current scale."""
153
+ self._spatial_cache.setdefault(str(self._scale), {})[key] = value
154
+
155
+ def get_spatial_cache(self, key: str) -> Any | None:
156
+ """Return a cached value for the current scale."""
157
+ return self._spatial_cache.get(str(self._scale), {}).get(key)
158
+
159
+ def _elementwise(
160
+ self,
161
+ other: torch.Tensor | "SparseTensor" | float,
162
+ operation: Callable[[torch.Tensor, Any], torch.Tensor],
163
+ ) -> "SparseTensor":
164
+ if isinstance(other, SparseTensor):
165
+ if not torch.equal(self.coords, other.coords):
166
+ raise ValueError("sparse operands must have identical coordinates")
167
+ other = other.feats
168
+ elif isinstance(other, torch.Tensor) and other.ndim >= 1:
169
+ if other.shape[0] == self.shape[0] and other.shape != self.feats.shape:
170
+ other = sparse_batch_broadcast(self, other)
171
+ return self.replace(operation(self.feats, other))
172
+
173
+ def __add__(self, other):
174
+ return self._elementwise(other, torch.add)
175
+
176
+ def __mul__(self, other):
177
+ return self._elementwise(other, torch.mul)
178
+
179
+
180
+ def sparse_batch_broadcast(input: SparseTensor, other: torch.Tensor) -> torch.Tensor:
181
+ """Broadcast a batch-first tensor over sparse feature rows."""
182
+ output = torch.empty_like(input.feats)
183
+ for batch, span in enumerate(input.layout):
184
+ output[span] = other[batch]
185
+ return output
sparc3d/nn/sparse/conv.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse submanifold convolution over spconv."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to route convolution through ``ops.sparse_backend``.
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ from sparc3d.ops import sparse_backend
12
+
13
+ from .basic import SparseTensor
14
+
15
+
16
+ class _DeferredConv3d(nn.Module):
17
+ """State-compatible placeholder used when spconv is not installed."""
18
+
19
+ def __init__(
20
+ self,
21
+ in_channels: int,
22
+ out_channels: int,
23
+ kernel_size: int,
24
+ bias: bool,
25
+ ):
26
+ super().__init__()
27
+ self.weight = nn.Parameter(
28
+ torch.empty(
29
+ out_channels,
30
+ kernel_size,
31
+ kernel_size,
32
+ kernel_size,
33
+ in_channels,
34
+ )
35
+ )
36
+ self.bias = nn.Parameter(torch.empty(out_channels)) if bias else None
37
+ nn.init.kaiming_uniform_(self.weight, a=5**0.5)
38
+ if self.bias is not None:
39
+ nn.init.zeros_(self.bias)
40
+
41
+ def forward(self, _input):
42
+ """Raise the deferred sparse-backend import error."""
43
+ raise ImportError("sparse convolution requires spconv")
44
+
45
+
46
+ class SparseConv3d(nn.Module):
47
+ """Coordinate-preserving three-dimensional sparse convolution."""
48
+
49
+ _sparc3d_convertible = True
50
+
51
+ def __init__(
52
+ self,
53
+ in_channels: int,
54
+ out_channels: int,
55
+ kernel_size: int,
56
+ bias: bool = True,
57
+ ):
58
+ """
59
+ Args:
60
+ in_channels: Input feature channels.
61
+ out_channels: Output feature channels.
62
+ kernel_size: Cubic kernel extent.
63
+ bias: Add a bias term.
64
+ """
65
+ super().__init__()
66
+ try:
67
+ self.conv = sparse_backend.Conv3d(
68
+ in_channels,
69
+ out_channels,
70
+ kernel_size=kernel_size,
71
+ bias=bias,
72
+ )
73
+ except ImportError:
74
+ # Construction and state-dict validation must work without spconv.
75
+ # Forward still raises; this is not an inference fallback.
76
+ self.conv = _DeferredConv3d(in_channels, out_channels, kernel_size, bias)
77
+ self.out_channels = out_channels
78
+
79
+ def forward(self, x: SparseTensor) -> SparseTensor:
80
+ """Convolve ``x`` while preserving its coordinate lineage."""
81
+ output = self.conv(x.backend_data())
82
+ return SparseTensor(
83
+ data=output,
84
+ shape=x.shape[:1] + (self.out_channels,),
85
+ layout=x.layout,
86
+ scale=x.scale,
87
+ spatial_cache=x._spatial_cache,
88
+ )
sparc3d/nn/sparse/linear.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Linear projection over sparse feature rows."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D's private sparse tensor.
5
+
6
+ from __future__ import annotations
7
+
8
+ from torch import nn
9
+
10
+ from .basic import SparseTensor
11
+
12
+
13
+ class SparseLinear(nn.Linear):
14
+ """Linear layer preserving sparse coordinates and caches."""
15
+
16
+ def forward(self, input: SparseTensor) -> SparseTensor:
17
+ """Project ``input`` features."""
18
+ return input.replace(super().forward(input.feats))
sparc3d/nn/sparse/nonlinearity.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Nonlinear activations over sparse feature rows."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain GELU only.
5
+
6
+ from __future__ import annotations
7
+
8
+ from torch import nn
9
+
10
+ from .basic import SparseTensor
11
+
12
+
13
+ class SparseGELU(nn.GELU):
14
+ """GELU activation preserving sparse coordinates and caches."""
15
+
16
+ def forward(self, input: SparseTensor) -> SparseTensor:
17
+ """Activate ``input`` features."""
18
+ return input.replace(super().forward(input.feats))
sparc3d/nn/sparse/spatial.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Paired average downsampling and cached nearest-neighbor upsampling."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain only paired downsample and upsample operations.
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Sequence
9
+
10
+ import torch
11
+ from torch import nn
12
+
13
+ from .basic import SparseTensor
14
+
15
+
16
+ def _factor_tuple(factor: int | Sequence[int], dimensions: int) -> tuple[int, ...]:
17
+ values = tuple(factor) if isinstance(factor, Sequence) else (factor,) * dimensions
18
+ if len(values) != dimensions or any(value <= 0 for value in values):
19
+ raise ValueError("factor must provide one positive value per spatial dimension")
20
+ return values
21
+
22
+
23
+ class SparseDownsample(nn.Module):
24
+ """Average features that collapse onto the same downsampled coordinate."""
25
+
26
+ def __init__(self, factor: int | Sequence[int]):
27
+ """
28
+ Args:
29
+ factor: Per-axis downsampling factor, or one factor for every axis.
30
+ """
31
+ super().__init__()
32
+ self.factor = tuple(factor) if isinstance(factor, Sequence) else factor
33
+
34
+ def forward(self, input: SparseTensor) -> SparseTensor:
35
+ """Downsample ``input`` and register the inverse mapping."""
36
+ dimensions = input.coords.shape[-1] - 1
37
+ factor = _factor_tuple(self.factor, dimensions)
38
+ coordinate_columns = list(input.coords.unbind(dim=-1))
39
+ for axis, value in enumerate(factor):
40
+ coordinate_columns[axis + 1] = coordinate_columns[axis + 1] // value
41
+
42
+ extents = [
43
+ int(coordinate_columns[axis + 1].max().item()) + 1
44
+ for axis in range(dimensions)
45
+ ]
46
+ offsets = torch.cumprod(torch.tensor(extents[::-1]), 0).tolist()[::-1] + [1]
47
+ codes = sum(
48
+ coordinate * offset
49
+ for coordinate, offset in zip(coordinate_columns, offsets)
50
+ )
51
+ unique_codes, inverse = codes.unique(return_inverse=True)
52
+ new_feats = torch.scatter_reduce(
53
+ torch.zeros(
54
+ unique_codes.shape[0],
55
+ input.feats.shape[1],
56
+ device=input.device,
57
+ dtype=input.dtype,
58
+ ),
59
+ dim=0,
60
+ index=inverse[:, None].expand(-1, input.feats.shape[1]),
61
+ src=input.feats,
62
+ reduce="mean",
63
+ )
64
+ new_coords = torch.stack(
65
+ [unique_codes // offsets[0]]
66
+ + [
67
+ (unique_codes // offsets[axis + 1]) % extents[axis]
68
+ for axis in range(dimensions)
69
+ ],
70
+ dim=-1,
71
+ )
72
+ output = SparseTensor(
73
+ new_feats,
74
+ new_coords,
75
+ shape=input.shape,
76
+ scale=tuple(scale * value for scale, value in zip(input.scale, factor)),
77
+ spatial_cache=input._spatial_cache,
78
+ )
79
+ output.register_spatial_cache(f"upsample_{factor}_coords", input.coords)
80
+ output.register_spatial_cache(f"upsample_{factor}_layout", input.layout)
81
+ output.register_spatial_cache(f"upsample_{factor}_idx", inverse)
82
+ return output
83
+
84
+
85
+ class SparseUpsample(nn.Module):
86
+ """Restore coordinates from a paired downsample cache."""
87
+
88
+ def __init__(self, factor: int | Sequence[int]):
89
+ """
90
+ Args:
91
+ factor: Per-axis factor, matching the paired :class:`SparseDownsample`.
92
+ """
93
+ super().__init__()
94
+ self.factor = tuple(factor) if isinstance(factor, Sequence) else factor
95
+
96
+ def forward(self, input: SparseTensor) -> SparseTensor:
97
+ """Upsample ``input`` through the recorded inverse mapping.
98
+
99
+ Raises:
100
+ ValueError: ``input`` is not at a scale this factor could have produced,
101
+ or no paired :class:`SparseDownsample` registered a cache for it.
102
+ """
103
+ dimensions = input.coords.shape[-1] - 1
104
+ factor = _factor_tuple(self.factor, dimensions)
105
+ if any(scale % value for scale, value in zip(input.scale, factor)):
106
+ raise ValueError(
107
+ f"cannot upsample by {factor} from scale {input.scale}; "
108
+ "SparseUpsample must mirror its paired SparseDownsample"
109
+ )
110
+ coords = input.get_spatial_cache(f"upsample_{factor}_coords")
111
+ layout = input.get_spatial_cache(f"upsample_{factor}_layout")
112
+ inverse = input.get_spatial_cache(f"upsample_{factor}_idx")
113
+ if coords is None or layout is None or inverse is None:
114
+ raise ValueError(
115
+ "upsample cache not found; SparseUpsample must follow its "
116
+ "paired SparseDownsample"
117
+ )
118
+ return SparseTensor(
119
+ input.feats[inverse],
120
+ coords,
121
+ shape=input.shape,
122
+ layout=layout,
123
+ scale=tuple(scale // value for scale, value in zip(input.scale, factor)),
124
+ spatial_cache=input._spatial_cache,
125
+ )
sparc3d/nn/sparse/transformer/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Transformer blocks over private sparse tensors."""
2
+
3
+ from .modulated import ModulatedSparseTransformerCrossBlock
4
+
5
+ __all__ = ["ModulatedSparseTransformerCrossBlock"]
sparc3d/nn/sparse/transformer/blocks.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Feed-forward building block for the sparse transformer."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain the sparse feed-forward path only.
5
+
6
+ from __future__ import annotations
7
+
8
+ from torch import nn
9
+
10
+ from ..basic import SparseTensor
11
+ from ..linear import SparseLinear
12
+ from ..nonlinearity import SparseGELU
13
+
14
+
15
+ class SparseFeedForwardNet(nn.Module):
16
+ """Two-layer sparse MLP with tanh-approximated GELU."""
17
+
18
+ def __init__(self, channels: int, mlp_ratio: float = 4.0):
19
+ """
20
+ Args:
21
+ channels: Input and output channels.
22
+ mlp_ratio: Hidden width as a multiple of ``channels``.
23
+ """
24
+ super().__init__()
25
+ hidden_channels = int(channels * mlp_ratio)
26
+ self.mlp = nn.Sequential(
27
+ SparseLinear(channels, hidden_channels),
28
+ SparseGELU(approximate="tanh"),
29
+ SparseLinear(hidden_channels, channels),
30
+ )
31
+
32
+ def forward(self, x: SparseTensor) -> SparseTensor:
33
+ """Transform sparse features with the MLP."""
34
+ return self.mlp(x)
sparc3d/nn/sparse/transformer/modulated.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared-modulation sparse transformer cross-attention block."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D: shared timestep modulation with full attention.
5
+
6
+ from __future__ import annotations
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ from ...norm import LayerNorm32
12
+ from ..attention import SparseMultiHeadAttention
13
+ from ..basic import SparseTensor
14
+ from .blocks import SparseFeedForwardNet
15
+
16
+
17
+ class ModulatedSparseTransformerCrossBlock(nn.Module):
18
+ """Sparse self/cross-attention block with shared adaptive normalization."""
19
+
20
+ def __init__(
21
+ self,
22
+ channels: int,
23
+ ctx_channels: int,
24
+ num_heads: int,
25
+ mlp_ratio: float = 4.0,
26
+ ):
27
+ super().__init__()
28
+ self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
29
+ self.norm2 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
30
+ self.norm3 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
31
+ self.self_attn = SparseMultiHeadAttention(
32
+ channels,
33
+ num_heads,
34
+ attention_type="self",
35
+ )
36
+ self.cross_attn = SparseMultiHeadAttention(
37
+ channels,
38
+ num_heads,
39
+ ctx_channels=ctx_channels,
40
+ attention_type="cross",
41
+ )
42
+ self.mlp = SparseFeedForwardNet(channels, mlp_ratio)
43
+ self.modulation = nn.Parameter(torch.randn(1, 6 * channels) / channels**0.5)
44
+
45
+ def forward(
46
+ self, x: SparseTensor, mod: torch.Tensor, context: torch.Tensor
47
+ ) -> SparseTensor:
48
+ """Transform ``x`` with timestep modulation and image context."""
49
+ (
50
+ shift_msa,
51
+ scale_msa,
52
+ gate_msa,
53
+ shift_mlp,
54
+ scale_mlp,
55
+ gate_mlp,
56
+ ) = (self.modulation + mod).chunk(6, dim=1)
57
+ h = x.replace(self.norm1(x.feats))
58
+ h = h * (1 + scale_msa) + shift_msa
59
+ h = self.self_attn(h) * gate_msa
60
+ x = x + h
61
+ h = x.replace(self.norm2(x.feats))
62
+ x = x + self.cross_attn(h, context)
63
+ h = x.replace(self.norm3(x.feats))
64
+ h = h * (1 + scale_mlp) + shift_mlp
65
+ return x + self.mlp(h) * gate_mlp
sparc3d/nn/utils.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Parameter precision and initialization helpers for neural network modules."""
2
+
3
+ # Adapted from Microsoft TRELLIS under the MIT License.
4
+ # Modified for SPARC3D to retain the sparse-transformer precision closure.
5
+
6
+ from __future__ import annotations
7
+
8
+ from torch import nn
9
+
10
+
11
+ def _is_convertible(module: nn.Module) -> bool:
12
+ return isinstance(module, (nn.Conv1d, nn.Conv2d, nn.Conv3d, nn.Linear)) or bool(
13
+ getattr(module, "_sparc3d_convertible", False)
14
+ )
15
+
16
+
17
+ def convert_module_to_f16(module: nn.Module) -> None:
18
+ """Convert primitive module parameters to float16."""
19
+ if _is_convertible(module):
20
+ recurse = bool(getattr(module, "_sparc3d_convertible", False))
21
+ for parameter in module.parameters(recurse=recurse):
22
+ parameter.data = parameter.data.half()
23
+
24
+
25
+ def zero_module(module: nn.Module) -> nn.Module:
26
+ """Zero ``module`` parameters and return the module."""
27
+ for parameter in module.parameters():
28
+ parameter.detach().zero_()
29
+ return module
sparc3d/ops/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Public GPU operations backed by Triton kernels."""
2
+
3
+ from sparc3d.ops.voxelize import MeshVoxelizer, adaptive_vox
4
+
5
+ __all__ = [
6
+ "MeshVoxelizer",
7
+ "adaptive_vox",
8
+ ]
sparc3d/ops/_triton.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lazy loader for the Triton operation backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import ModuleType
6
+
7
+
8
+ def load_triton_ops() -> ModuleType:
9
+ """Import and return ``sparc3d.ops.triton``, or raise a clear ``ImportError``.
10
+
11
+ Raises:
12
+ ImportError: Triton is unavailable in the running environment.
13
+ """
14
+ try:
15
+ from sparc3d.ops import triton as triton_ops
16
+ except ImportError as exc:
17
+ raise ImportError(
18
+ "The SPARC3D operations require Triton. Install the verified Linux "
19
+ "CUDA PyTorch stack, which supplies it."
20
+ ) from exc
21
+ return triton_ops
sparc3d/ops/flexicubes/LICENSE.txt 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.
sparc3d/ops/flexicubes/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vendored FlexiCubes isosurface extractor.
3
+
4
+ Vendored from `nv-tlabs/FlexiCubes <https://github.com/nv-tlabs/FlexiCubes>`_ and
5
+ distributed under the Apache License 2.0 in ``LICENSE.txt``. The modifications
6
+ this package makes are listed in the header of ``flexicubes.py`` and marked at
7
+ each site.
8
+ """
9
+
10
+ from sparc3d.ops.flexicubes.flexicubes import FlexiCubes
11
+
12
+ __all__ = ["FlexiCubes"]
sparc3d/ops/flexicubes/flexicubes.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
2
+ # All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # This file has been modified from nv-tlabs/FlexiCubes. Every change is marked
17
+ # below with a "SPARC3D modification" comment. In summary:
18
+ #
19
+ # * sparse cube indexing -- ``_get_case_id_sparse`` resolves the ambiguous DMC
20
+ # configurations through an explicit ``cube_index_map`` instead of scanning a
21
+ # dense ``res x res x res`` array, so the extractor never materializes the
22
+ # full grid;
23
+ # * lookup tables and index tensors are int32 rather than int64;
24
+ # * ``__call__`` takes a ``dtype`` so the extractor can run in half precision;
25
+ # * the quadrilateral winding fix is out-of-place (``torch.cat``) instead of
26
+ # masked in-place assignment;
27
+ # * removed upstream code that this inference-only package never reaches: the
28
+ # ``construct_voxel_grid`` dense grid builder, ``_tetrahedralize`` and the
29
+ # ``output_tetmesh`` output mode, the ``grad_func`` gradient path, the dense
30
+ # ``_get_case_id`` superseded by ``_get_case_id_sparse``, the unused
31
+ # ``_solve_vd_QEF`` dual-vertex solver, the ``L_dev`` developability
32
+ # regularizer, and the differentiable ``training`` quad-splitting branch;
33
+ # * removed the learned per-cube weights ``alpha``/``beta``/``gamma_f`` and the
34
+ # ``weight_scale`` that normalizes them. They are trainable features of the
35
+ # upstream extractor and no released checkpoint carries them, so the
36
+ # inference path always passed ``None`` and ``_normalize_weights`` always
37
+ # returned tensors of ones. Folding those ones through leaves plain linear
38
+ # interpolation, an unweighted mean over each dual vertex, and the fixed
39
+ # ``quad_split_2`` triangulation of every quadrilateral. The float32
40
+ # accumulation the all-ones weights used to force is kept explicitly, so
41
+ # half-precision inputs still reduce in float32;
42
+ # * ``cube_index_map`` is required rather than defaulting to ``None``, and the
43
+ # unread ``cube_corners`` table is dropped.
44
+ """FlexiCubes dual marching cubes extractor with sparse cube indexing."""
45
+
46
+ import torch
47
+
48
+ # SPARC3D modification: relative import, since the tables live in this package.
49
+ from .tables import *
50
+
51
+ __all__ = [
52
+ 'FlexiCubes'
53
+ ]
54
+
55
+
56
+ class FlexiCubes:
57
+ """Dual marching cubes extractor over a sparse set of occupied cubes."""
58
+
59
+ # SPARC3D modification: every table below is int32 rather than upstream's int64.
60
+ def __init__(self, device="cuda"):
61
+ """
62
+ Args:
63
+ device: Device the lookup tables are allocated on. Inputs to
64
+ :meth:`__call__` must live on the same device.
65
+ """
66
+ self.device = device
67
+ self.dmc_table = torch.tensor(dmc_table, dtype=torch.int32, device=device, requires_grad=False)
68
+ self.num_vd_table = torch.tensor(num_vd_table,
69
+ dtype=torch.int32, device=device, requires_grad=False)
70
+ self.check_table = torch.tensor(
71
+ check_table,
72
+ dtype=torch.int32, device=device, requires_grad=False)
73
+
74
+ self.quad_split_2 = torch.tensor([0, 1, 3, 3, 1, 2], dtype=torch.int32, device=device, requires_grad=False)
75
+
76
+ self.cube_corners_idx = torch.pow(2, torch.arange(8, dtype=torch.int32, requires_grad=False))
77
+ self.cube_edges = torch.tensor([0, 1, 1, 5, 4, 5, 0, 4, 2, 3, 3, 7, 6, 7, 2, 6,
78
+ 2, 0, 3, 1, 7, 5, 6, 4], dtype=torch.int32, device=device, requires_grad=False)
79
+
80
+ # SPARC3D modification: renamed arguments, added dtype and cube_index_map, and
81
+ # dropped upstream's output_tetmesh and grad_func. Extracts a triangle mesh from the
82
+ # sparse scalar field; returns (vertices, faces).
83
+ def __call__(self, voxelgrid_vertices, scalar_field, cube_idx, resolution,
84
+ cube_index_map, dtype=None): # dtype so that it can work with half precision.
85
+ """Extract a triangle mesh from a sparse signed scalar field.
86
+
87
+ Args:
88
+ voxelgrid_vertices: Cube-corner positions ``[V, 3]``.
89
+ scalar_field: Signed value at each corner ``[V]``; the surface is its
90
+ zero level set.
91
+ cube_idx: Corner indices of each occupied cube ``[C, 8]``.
92
+ resolution: Grid resolution the cube indices are expressed in.
93
+ cube_index_map: Integer grid coordinate of each cube ``[C, 3]``, used to
94
+ resolve ambiguous configurations against neighbours. Required: the
95
+ sparse case-id path indexes it directly, so omitting it fails only
96
+ once a mesh happens to contain an ambiguous cube.
97
+ dtype: Float dtype to unify inputs to, e.g. ``torch.float16``. Inferred
98
+ from ``voxelgrid_vertices`` when ``None``, defaulting to float32.
99
+
100
+ Returns:
101
+ ``(vertices, faces)``, both empty when no cube straddles the surface.
102
+ """
103
+ # unify floating tensors to a chosen dtype, so it can work with half, etc:
104
+ if dtype is None:
105
+ if voxelgrid_vertices.dtype in (torch.float16, torch.bfloat16, torch.float32, torch.float64):
106
+ dtype = voxelgrid_vertices.dtype
107
+ else:
108
+ dtype = torch.float32
109
+
110
+ # cast all to dtype, so that it works with half-precision:
111
+ voxelgrid_vertices = voxelgrid_vertices.to(dtype) # Cast the geometry to the chosen dtype
112
+ scalar_field = scalar_field.to(dtype)
113
+
114
+ surf_cubes, occ_fx8 = self._identify_surf_cubes(scalar_field, cube_idx)
115
+ if surf_cubes.sum() == 0:
116
+ return (
117
+ torch.zeros((0, 3), device=self.device, dtype=dtype),
118
+ torch.zeros((0, 3), dtype=torch.int32, device=self.device),
119
+ )
120
+
121
+ case_ids = self._get_case_id_sparse(occ_fx8, surf_cubes, resolution, cube_index_map)
122
+
123
+ surf_edges, idx_map, edge_counts, surf_edges_mask = self._identify_surf_edges(
124
+ scalar_field, cube_idx, surf_cubes
125
+ )
126
+
127
+ vd, vd_idx_map = self._compute_vd(
128
+ voxelgrid_vertices, surf_edges, scalar_field, case_ids, idx_map)
129
+
130
+ return self._triangulate(
131
+ scalar_field, surf_edges, vd, edge_counts, idx_map,
132
+ vd_idx_map, surf_edges_mask)
133
+
134
+ # SPARC3D modification: sparse replacement for _get_case_id. Upstream builds a
135
+ # dense res^3 x 5 array to look up the configuration of adjacent cubes; this
136
+ # version sorts the occupied cube keys and binary-searches them instead, so
137
+ # cost scales with the number of surface cubes rather than with res^3.
138
+ @torch.no_grad()
139
+ def _get_case_id_sparse(self, occ_fx8, surf_cubes, res, cube_index_map):
140
+ """
141
+ Obtains the ID of topology cases based on cell corner occupancy. This function resolves the
142
+ ambiguity in the Dual Marching Cubes (DMC) configurations as described in Section 1.3 of the
143
+ supplementary material.
144
+ """
145
+ occ_int = occ_fx8[surf_cubes].to(torch.int32) # Convert boolean to int32
146
+ corners_idx = self.cube_corners_idx.to(self.device).unsqueeze(0)
147
+ case_ids = (occ_int * corners_idx).sum(-1, dtype=torch.int32) # int32 else sum defaults to int64 by default
148
+ problem_config = self.check_table.to(self.device)[case_ids]
149
+ to_check = problem_config[..., 0] == 1
150
+
151
+ problem_config = problem_config[to_check]
152
+ problem_config_index = cube_index_map[surf_cubes][to_check]
153
+
154
+ # SPARC3D modification: an ambiguous cube is resolved against its neighbour, which
155
+ # upstream looks up in a dense res^3 array of per-cube configurations. Here the
156
+ # neighbour is found by binary-searching the sorted keys of the occupied cubes
157
+ # below, so no dense volume is ever allocated.
158
+ vol_idx = cube_index_map # N, 3
159
+ vol_idx_problem = vol_idx[surf_cubes][to_check]
160
+
161
+ vol_idx_problem_adj = vol_idx_problem + problem_config[..., 1:4]
162
+
163
+ within_range = (
164
+ vol_idx_problem_adj[..., 0] >= 0) & (
165
+ vol_idx_problem_adj[..., 0] < res) & (
166
+ vol_idx_problem_adj[..., 1] >= 0) & (
167
+ vol_idx_problem_adj[..., 1] < res) & (
168
+ vol_idx_problem_adj[..., 2] >= 0) & (
169
+ vol_idx_problem_adj[..., 2] < res)
170
+
171
+ vol_idx_problem = vol_idx_problem[within_range]
172
+ vol_idx_problem_adj = vol_idx_problem_adj[within_range]
173
+ problem_config = problem_config[within_range]
174
+ problem_config_index = problem_config_index[within_range]
175
+
176
+ if problem_config.shape[0] == 0:
177
+ return case_ids
178
+
179
+ inverse_keys = (
180
+ (vol_idx_problem_adj[..., 0].long() * res
181
+ + vol_idx_problem_adj[..., 1].long()) * res
182
+ + vol_idx_problem_adj[..., 2].long()
183
+ )
184
+ problem_keys = (
185
+ (problem_config_index[..., 0].long() * res
186
+ + problem_config_index[..., 1].long()) * res
187
+ + problem_config_index[..., 2].long()
188
+ )
189
+ sorted_keys, sorted_order = torch.sort(problem_keys)
190
+ positions = torch.searchsorted(sorted_keys, inverse_keys)
191
+ bounded_positions = positions.clamp(max=sorted_keys.shape[0] - 1)
192
+ found = (positions < sorted_keys.shape[0]) & (
193
+ sorted_keys[bounded_positions] == inverse_keys
194
+ )
195
+ inverse_indices = torch.full_like(positions, -1, dtype=torch.long)
196
+ inverse_indices[found] = sorted_order[bounded_positions[found]]
197
+ problem_config = torch.cat(
198
+ (
199
+ problem_config,
200
+ torch.zeros(
201
+ (1, 5),
202
+ dtype=problem_config.dtype,
203
+ device=problem_config.device,
204
+ ),
205
+ )
206
+ )
207
+ problem_config_adj = problem_config[inverse_indices]
208
+
209
+ # If two cubes with cases C16 and C19 share an ambiguous face, both cases are inverted.
210
+ to_invert = (problem_config_adj[..., 0] == 1)
211
+ idx = torch.arange(case_ids.shape[0], dtype=torch.int32, device=self.device)[to_check][within_range][to_invert]
212
+ case_ids.index_put_((idx,), problem_config[:-1][to_invert][..., -1])
213
+
214
+ return case_ids
215
+
216
+
217
+ @torch.no_grad()
218
+ def _identify_surf_edges(self, scalar_field, cube_idx, surf_cubes):
219
+ """
220
+ Identifies grid edges that intersect with the underlying surface by checking for opposite signs. As each edge
221
+ can be shared by multiple cubes, this function also assigns a unique index to each surface-intersecting edge
222
+ and marks the cube edges with this index.
223
+ """
224
+ occ_n = scalar_field < 0
225
+ all_edges = cube_idx[surf_cubes][:, self.cube_edges].reshape(-1, 2)
226
+ unique_edges, _idx_map, counts = torch.unique(all_edges, dim=0, return_inverse=True, return_counts=True)
227
+
228
+ unique_edges = unique_edges.int()
229
+ mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
230
+
231
+ surf_edges_mask = mask_edges[_idx_map]
232
+ counts = counts[_idx_map]
233
+
234
+ mapping = torch.ones((unique_edges.shape[0]), dtype=torch.int32, device=cube_idx.device) * -1
235
+ mapping[mask_edges] = torch.arange(mask_edges.sum(), dtype=torch.int32, device=cube_idx.device) # int32 else arrange defaults to int64
236
+ # Shaped as [number of cubes x 12 edges per cube]. This is later used to map a cube edge to the unique index
237
+ # for a surface-intersecting edge. Non-surface-intersecting edges are marked with -1.
238
+ idx_map = mapping[_idx_map]
239
+ surf_edges = unique_edges[mask_edges]
240
+ return surf_edges, idx_map, counts, surf_edges_mask
241
+
242
+ @torch.no_grad()
243
+ def _identify_surf_cubes(self, scalar_field, cube_idx):
244
+ """
245
+ Identifies grid cubes that intersect with the underlying surface by checking if the signs at
246
+ all corners are not identical.
247
+ """
248
+ occ_n = scalar_field < 0
249
+ occ_fx8 = occ_n[cube_idx.reshape(-1)].reshape(-1, 8)
250
+ _occ_sum = torch.sum(occ_fx8, -1)
251
+ surf_cubes = (_occ_sum > 0) & (_occ_sum < 8)
252
+ return surf_cubes, occ_fx8
253
+
254
+ def _linear_interp(self, edges_weight, edges_x):
255
+ """
256
+ Computes the location of zero-crossings on 'edges_x' using linear interpolation with 'edges_weight'.
257
+ """
258
+ edge_dim = edges_weight.dim() - 2
259
+ assert edges_weight.shape[edge_dim] == 2
260
+ edges_weight = torch.cat([torch.index_select(input=edges_weight, index=torch.tensor(1, device=self.device), dim=edge_dim), -
261
+ torch.index_select(input=edges_weight, index=torch.tensor(0, device=self.device), dim=edge_dim)]
262
+ , edge_dim)
263
+ denominator = edges_weight.sum(edge_dim)
264
+ ue = (edges_x * edges_weight).sum(edge_dim) / denominator
265
+ return ue
266
+
267
+ def _compute_vd(self, voxelgrid_vertices, surf_edges, scalar_field,
268
+ case_ids, idx_map):
269
+ """
270
+ Computes the location of dual vertices as described in Section 4.2
271
+ """
272
+ surf_edges_x = torch.index_select(input=voxelgrid_vertices, index=surf_edges.reshape(-1), dim=0).reshape(-1, 2, 3)
273
+ surf_edges_s = torch.index_select(input=scalar_field, index=surf_edges.reshape(-1), dim=0).reshape(-1, 2, 1)
274
+
275
+ idx_map = idx_map.reshape(-1, 12)
276
+ num_vd = torch.index_select(input=self.num_vd_table, index=case_ids, dim=0)
277
+ edge_group, edge_group_to_vd, edge_group_to_cube = [], [], []
278
+
279
+ total_num_vd = 0
280
+ vd_idx_map = torch.zeros((case_ids.shape[0], 12), dtype=torch.int64, device=self.device, requires_grad=False)
281
+
282
+ for num in torch.unique(num_vd):
283
+ cur_cubes = (num_vd == num) # consider cubes with the same numbers of vd emitted (for batching)
284
+ curr_num_vd = cur_cubes.sum() * num
285
+ curr_edge_group = self.dmc_table[case_ids[cur_cubes], :num].reshape(-1, num * 7)
286
+ curr_edge_group_to_vd = torch.arange(
287
+ curr_num_vd, device=self.device).unsqueeze(-1).repeat(1, 7) + total_num_vd
288
+ total_num_vd += curr_num_vd
289
+ curr_edge_group_to_cube = torch.arange(idx_map.shape[0], device=self.device)[
290
+ cur_cubes].unsqueeze(-1).repeat(1, num * 7).reshape_as(curr_edge_group)
291
+
292
+ curr_mask = (curr_edge_group != -1)
293
+ edge_group.append(torch.masked_select(curr_edge_group, curr_mask))
294
+ edge_group_to_vd.append(torch.masked_select(curr_edge_group_to_vd.reshape_as(curr_edge_group), curr_mask))
295
+ edge_group_to_cube.append(torch.masked_select(curr_edge_group_to_cube, curr_mask))
296
+
297
+ edge_group = torch.cat(edge_group)
298
+ edge_group_to_vd = torch.cat(edge_group_to_vd)
299
+ edge_group_to_cube = torch.cat(edge_group_to_cube)
300
+
301
+ # SPARC3D modification: dual vertices accumulate in float32 even when the inputs
302
+ # are half. Averaging up to twelve edge crossings per vertex in half precision
303
+ # loses enough mantissa to move the surface visibly.
304
+ vd = torch.zeros((total_num_vd, 3), device=self.device, dtype=torch.float)
305
+ edges_per_vd = torch.zeros((total_num_vd, 1), device=self.device, dtype=torch.float)
306
+
307
+ idx_group = torch.gather(input=idx_map.reshape(-1), dim=0, index=edge_group_to_cube * 12 + edge_group)
308
+
309
+ x_group = torch.index_select(input=surf_edges_x, index=idx_group.reshape(-1), dim=0).reshape(-1, 2, 3)
310
+ s_group = torch.index_select(input=surf_edges_s, index=idx_group.reshape(-1), dim=0).reshape(-1, 2, 1)
311
+
312
+ ue_group = self._linear_interp(s_group.float(), x_group)
313
+
314
+ edges_per_vd = edges_per_vd.index_add_(
315
+ 0, index=edge_group_to_vd, source=torch.ones_like(ue_group[:, :1]))
316
+ vd = vd.index_add_(0, index=edge_group_to_vd, source=ue_group) / edges_per_vd
317
+
318
+ v_idx = torch.arange(vd.shape[0], dtype=torch.int64, device=self.device) # + total_num_vd
319
+ index = edge_group_to_cube * 12 + edge_group
320
+ vd_idx_map = (vd_idx_map.reshape(-1)).scatter(dim=0, index=index, src=v_idx[edge_group_to_vd])
321
+
322
+ return vd, vd_idx_map
323
+
324
+ # SPARC3D modification: every quadrilateral splits along the same fixed diagonal.
325
+ # Choosing per-quad between the two diagonals needs the learned gamma weights, which
326
+ # the released inference weights do not carry.
327
+ def _triangulate(self, scalar_field, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask):
328
+ """
329
+ Connects four neighboring dual vertices to form a quadrilateral. Each quadrilateral is then split
330
+ into two triangles along a fixed diagonal, as described in Section 4.3.
331
+ """
332
+ with torch.no_grad():
333
+ group_mask = (edge_counts == 4) & surf_edges_mask # surface edges shared by 4 cubes.
334
+ group = idx_map.reshape(-1)[group_mask]
335
+ vd_idx = vd_idx_map[group_mask]
336
+ edge_indices, indices = torch.sort(group, stable=True)
337
+ indices = indices.to(torch.int32)
338
+ quad_vd_idx = vd_idx[indices].reshape(-1, 4).to(torch.int32)
339
+
340
+ # Ensure all face directions point towards the positive SDF to maintain consistent winding.
341
+ # SPARC3D modification: rebuilt out-of-place rather than by masked
342
+ # in-place assignment, which reorders the quadrilaterals.
343
+ s_edges = scalar_field[surf_edges[edge_indices.reshape(-1, 4)[:, 0]].reshape(-1)].reshape(-1, 2)
344
+ flip_mask = s_edges[:, 0] > 0
345
+ quad_vd_idx = torch.cat((quad_vd_idx[flip_mask][:, [0, 1, 3, 2]],
346
+ quad_vd_idx[~flip_mask][:, [2, 3, 1, 0]]))
347
+
348
+ faces = quad_vd_idx[:, self.quad_split_2].reshape(-1, 3)
349
+ return vd, faces
sparc3d/ops/flexicubes/tables.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
2
+ # All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Vendored from nv-tlabs/FlexiCubes. The surviving table data is unmodified.
17
+ # SPARC3D modification: upstream's ``tet_table`` is dropped along with the
18
+ # tetrahedral output mode that was its only consumer.
19
+ """Dual marching cubes lookup tables used by :class:`FlexiCubes`."""
20
+
21
+ dmc_table = [
22
+ [[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
23
+ [[0, 3, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
24
+ [[0, 1, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
25
+ [[1, 3, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
26
+ [[4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
27
+ [[0, 3, 4, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
28
+ [[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
29
+ [[1, 3, 4, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
30
+ [[4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
31
+ [[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
32
+ [[0, 1, 4, 5, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
33
+ [[1, 3, 4, 5, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
34
+ [[5, 7, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
35
+ [[0, 3, 5, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
36
+ [[0, 1, 5, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
37
+ [[1, 3, 5, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
38
+ [[2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
39
+ [[0, 2, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
40
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
41
+ [[1, 2, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
42
+ [[4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
43
+ [[0, 2, 4, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
44
+ [[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
45
+ [[1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
46
+ [[4, 5, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
47
+ [[0, 2, 8, 11, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
48
+ [[0, 1, 4, 5, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
49
+ [[1, 2, 4, 5, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
50
+ [[5, 7, 8, 9, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
51
+ [[0, 2, 5, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
52
+ [[0, 1, 5, 7, 8, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
53
+ [[1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
54
+ [[1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
55
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
56
+ [[0, 2, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
57
+ [[2, 3, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
58
+ [[4, 7, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
59
+ [[0, 3, 4, 7, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
60
+ [[0, 2, 9, 10, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
61
+ [[2, 3, 4, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
62
+ [[4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
63
+ [[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
64
+ [[0, 2, 4, 5, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
65
+ [[2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
66
+ [[5, 7, 8, 9, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
67
+ [[0, 3, 5, 7, 9, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
68
+ [[0, 2, 5, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
69
+ [[2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
70
+ [[1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
71
+ [[0, 1, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
72
+ [[0, 3, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
73
+ [[8, 9, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
74
+ [[4, 7, 8, -1, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
75
+ [[0, 1, 4, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
76
+ [[0, 3, 9, 10, 11, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
77
+ [[4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
78
+ [[4, 5, 9, -1, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
79
+ [[0, 1, 8, 10, 11, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
80
+ [[0, 3, 4, 5, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
81
+ [[4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
82
+ [[5, 7, 8, 9, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
83
+ [[0, 1, 5, 7, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
84
+ [[0, 3, 5, 7, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
85
+ [[5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
86
+ [[6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
87
+ [[0, 3, 8, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
88
+ [[0, 1, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
89
+ [[1, 3, 8, 9, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
90
+ [[4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
91
+ [[0, 3, 4, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
92
+ [[0, 1, 9, -1, -1, -1, -1], [4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
93
+ [[1, 3, 4, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
94
+ [[4, 5, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
95
+ [[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
96
+ [[0, 1, 4, 5, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
97
+ [[1, 3, 4, 5, 8, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
98
+ [[5, 6, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
99
+ [[0, 3, 5, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
100
+ [[0, 1, 5, 6, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
101
+ [[1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
102
+ [[2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
103
+ [[0, 2, 6, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
104
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
105
+ [[1, 2, 6, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
106
+ [[2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
107
+ [[0, 2, 4, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
108
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
109
+ [[1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
110
+ [[4, 5, 9, -1, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
111
+ [[0, 2, 6, 7, 8, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
112
+ [[0, 1, 4, 5, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
113
+ [[1, 2, 4, 5, 6, 7, 8], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
114
+ [[2, 3, 5, 6, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
115
+ [[0, 2, 5, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
116
+ [[0, 1, 2, 3, 5, 6, 8], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
117
+ [[1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
118
+ [[1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
119
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
120
+ [[0, 2, 9, 10, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
121
+ [[2, 3, 8, 9, 10, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
122
+ [[4, 6, 8, 11, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
123
+ [[0, 3, 4, 6, 11, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
124
+ [[0, 2, 9, 10, -1, -1, -1], [4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
125
+ [[2, 3, 4, 6, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
126
+ [[4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
127
+ [[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1]],
128
+ [[0, 2, 4, 5, 10, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
129
+ [[2, 3, 4, 5, 8, 10, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
130
+ [[5, 6, 8, 9, 11, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
131
+ [[0, 3, 5, 6, 9, 11, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
132
+ [[0, 2, 5, 6, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
133
+ [[2, 3, 5, 6, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
134
+ [[1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
135
+ [[0, 1, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
136
+ [[0, 3, 6, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
137
+ [[6, 7, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
138
+ [[1, 3, 4, 6, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
139
+ [[0, 1, 4, 6, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
140
+ [[0, 3, 4, 6, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
141
+ [[4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
142
+ [[4, 5, 9, -1, -1, -1, -1], [1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
143
+ [[0, 1, 6, 7, 8, 10, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
144
+ [[0, 3, 4, 5, 6, 7, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
145
+ [[4, 5, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
146
+ [[1, 3, 5, 6, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
147
+ [[0, 1, 5, 6, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
148
+ [[0, 3, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
149
+ [[5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
150
+ [[5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
151
+ [[0, 3, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
152
+ [[0, 1, 9, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
153
+ [[1, 3, 8, 9, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
154
+ [[4, 7, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
155
+ [[0, 3, 4, 7, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
156
+ [[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
157
+ [[1, 3, 4, 7, 9, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
158
+ [[4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
159
+ [[0, 3, 8, -1, -1, -1, -1], [4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
160
+ [[0, 1, 4, 6, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
161
+ [[1, 3, 4, 6, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
162
+ [[6, 7, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
163
+ [[0, 3, 6, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
164
+ [[0, 1, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
165
+ [[1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
166
+ [[2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
167
+ [[0, 2, 8, 11, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
168
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
169
+ [[1, 2, 8, 9, 11, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
170
+ [[4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
171
+ [[0, 2, 4, 7, 11, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
172
+ [[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1]],
173
+ [[1, 2, 4, 7, 9, 11, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
174
+ [[4, 6, 9, 10, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
175
+ [[0, 2, 8, 11, -1, -1, -1], [4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
176
+ [[0, 1, 4, 6, 10, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
177
+ [[1, 2, 4, 6, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
178
+ [[6, 7, 8, 9, 10, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
179
+ [[0, 2, 6, 7, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
180
+ [[0, 1, 6, 7, 8, 10, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
181
+ [[1, 2, 6, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
182
+ [[1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
183
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
184
+ [[0, 2, 5, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
185
+ [[2, 3, 5, 6, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
186
+ [[4, 7, 8, -1, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
187
+ [[0, 3, 4, 7, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
188
+ [[0, 2, 5, 6, 9, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
189
+ [[2, 3, 4, 5, 6, 7, 9], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
190
+ [[1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
191
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
192
+ [[0, 2, 4, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
193
+ [[2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
194
+ [[1, 2, 6, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
195
+ [[0, 1, 2, 3, 6, 7, 9], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
196
+ [[0, 2, 6, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
197
+ [[2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
198
+ [[1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
199
+ [[0, 1, 5, 6, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
200
+ [[0, 3, 5, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
201
+ [[5, 6, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
202
+ [[4, 7, 8, -1, -1, -1, -1], [1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
203
+ [[0, 1, 4, 5, 6, 7, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
204
+ [[0, 3, 5, 6, 9, 11, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
205
+ [[4, 5, 6, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
206
+ [[1, 3, 4, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
207
+ [[0, 1, 4, 6, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
208
+ [[0, 3, 4, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
209
+ [[4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
210
+ [[1, 3, 6, 7, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
211
+ [[0, 1, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
212
+ [[0, 3, 6, 7, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
213
+ [[6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
214
+ [[5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
215
+ [[0, 3, 8, -1, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
216
+ [[0, 1, 9, -1, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
217
+ [[1, 3, 8, 9, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
218
+ [[4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
219
+ [[0, 3, 4, 5, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
220
+ [[0, 1, 9, -1, -1, -1, -1], [4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
221
+ [[1, 3, 4, 5, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
222
+ [[4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
223
+ [[0, 3, 8, -1, -1, -1, -1], [4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
224
+ [[0, 1, 4, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
225
+ [[1, 3, 4, 7, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
226
+ [[8, 9, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
227
+ [[0, 3, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
228
+ [[0, 1, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
229
+ [[1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
230
+ [[2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
231
+ [[0, 2, 5, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
232
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
233
+ [[1, 2, 5, 7, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
234
+ [[2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
235
+ [[0, 2, 4, 5, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
236
+ [[0, 1, 9, -1, -1, -1, -1], [2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
237
+ [[1, 2, 4, 5, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
238
+ [[2, 3, 4, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
239
+ [[0, 2, 4, 7, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
240
+ [[0, 1, 2, 3, 4, 7, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
241
+ [[4, 7, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
242
+ [[2, 3, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
243
+ [[0, 2, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
244
+ [[0, 1, 2, 3, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
245
+ [[1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
246
+ [[1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
247
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
248
+ [[0, 2, 5, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
249
+ [[2, 3, 5, 7, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
250
+ [[1, 2, 4, 5, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
251
+ [[0, 1, 2, 3, 4, 5, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
252
+ [[0, 2, 4, 5, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
253
+ [[4, 5, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
254
+ [[1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
255
+ [[0, 3, 8, -1, -1, -1, -1], [1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
256
+ [[0, 2, 4, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
257
+ [[2, 3, 4, 7, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
258
+ [[1, 2, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
259
+ [[0, 1, 2, 3, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
260
+ [[0, 2, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
261
+ [[2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
262
+ [[1, 3, 5, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
263
+ [[0, 1, 5, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
264
+ [[0, 3, 5, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
265
+ [[5, 7, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
266
+ [[1, 3, 4, 5, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
267
+ [[0, 1, 4, 5, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
268
+ [[0, 3, 4, 5, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
269
+ [[4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
270
+ [[1, 3, 4, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
271
+ [[0, 1, 4, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
272
+ [[0, 3, 4, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
273
+ [[4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
274
+ [[1, 3, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
275
+ [[0, 1, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
276
+ [[0, 3, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
277
+ [[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]]
278
+ ]
279
+ num_vd_table = [0, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 2, 2,
280
+ 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 2, 2, 2, 1, 2, 3, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2,
281
+ 1, 2, 1, 2, 2, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 2, 3, 2, 2, 1, 1, 1, 1,
282
+ 1, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 2, 2, 2, 2, 2, 1, 3, 4, 2,
283
+ 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 2, 2, 2,
284
+ 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 3, 2, 3, 2, 4, 2, 2, 2, 2, 1, 2, 1, 2, 1, 1,
285
+ 2, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1,
286
+ 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2,
287
+ 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1,
288
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
289
+ check_table = [
290
+ [0, 0, 0, 0, 0],
291
+ [0, 0, 0, 0, 0],
292
+ [0, 0, 0, 0, 0],
293
+ [0, 0, 0, 0, 0],
294
+ [0, 0, 0, 0, 0],
295
+ [0, 0, 0, 0, 0],
296
+ [0, 0, 0, 0, 0],
297
+ [0, 0, 0, 0, 0],
298
+ [0, 0, 0, 0, 0],
299
+ [0, 0, 0, 0, 0],
300
+ [0, 0, 0, 0, 0],
301
+ [0, 0, 0, 0, 0],
302
+ [0, 0, 0, 0, 0],
303
+ [0, 0, 0, 0, 0],
304
+ [0, 0, 0, 0, 0],
305
+ [0, 0, 0, 0, 0],
306
+ [0, 0, 0, 0, 0],
307
+ [0, 0, 0, 0, 0],
308
+ [0, 0, 0, 0, 0],
309
+ [0, 0, 0, 0, 0],
310
+ [0, 0, 0, 0, 0],
311
+ [0, 0, 0, 0, 0],
312
+ [0, 0, 0, 0, 0],
313
+ [0, 0, 0, 0, 0],
314
+ [0, 0, 0, 0, 0],
315
+ [0, 0, 0, 0, 0],
316
+ [0, 0, 0, 0, 0],
317
+ [0, 0, 0, 0, 0],
318
+ [0, 0, 0, 0, 0],
319
+ [0, 0, 0, 0, 0],
320
+ [0, 0, 0, 0, 0],
321
+ [0, 0, 0, 0, 0],
322
+ [0, 0, 0, 0, 0],
323
+ [0, 0, 0, 0, 0],
324
+ [0, 0, 0, 0, 0],
325
+ [0, 0, 0, 0, 0],
326
+ [0, 0, 0, 0, 0],
327
+ [0, 0, 0, 0, 0],
328
+ [0, 0, 0, 0, 0],
329
+ [0, 0, 0, 0, 0],
330
+ [0, 0, 0, 0, 0],
331
+ [0, 0, 0, 0, 0],
332
+ [0, 0, 0, 0, 0],
333
+ [0, 0, 0, 0, 0],
334
+ [0, 0, 0, 0, 0],
335
+ [0, 0, 0, 0, 0],
336
+ [0, 0, 0, 0, 0],
337
+ [0, 0, 0, 0, 0],
338
+ [0, 0, 0, 0, 0],
339
+ [0, 0, 0, 0, 0],
340
+ [0, 0, 0, 0, 0],
341
+ [0, 0, 0, 0, 0],
342
+ [0, 0, 0, 0, 0],
343
+ [0, 0, 0, 0, 0],
344
+ [0, 0, 0, 0, 0],
345
+ [0, 0, 0, 0, 0],
346
+ [0, 0, 0, 0, 0],
347
+ [0, 0, 0, 0, 0],
348
+ [0, 0, 0, 0, 0],
349
+ [0, 0, 0, 0, 0],
350
+ [0, 0, 0, 0, 0],
351
+ [1, 1, 0, 0, 194],
352
+ [1, -1, 0, 0, 193],
353
+ [0, 0, 0, 0, 0],
354
+ [0, 0, 0, 0, 0],
355
+ [0, 0, 0, 0, 0],
356
+ [0, 0, 0, 0, 0],
357
+ [0, 0, 0, 0, 0],
358
+ [0, 0, 0, 0, 0],
359
+ [0, 0, 0, 0, 0],
360
+ [0, 0, 0, 0, 0],
361
+ [0, 0, 0, 0, 0],
362
+ [0, 0, 0, 0, 0],
363
+ [0, 0, 0, 0, 0],
364
+ [0, 0, 0, 0, 0],
365
+ [0, 0, 0, 0, 0],
366
+ [0, 0, 0, 0, 0],
367
+ [0, 0, 0, 0, 0],
368
+ [0, 0, 0, 0, 0],
369
+ [0, 0, 0, 0, 0],
370
+ [0, 0, 0, 0, 0],
371
+ [0, 0, 0, 0, 0],
372
+ [0, 0, 0, 0, 0],
373
+ [0, 0, 0, 0, 0],
374
+ [0, 0, 0, 0, 0],
375
+ [0, 0, 0, 0, 0],
376
+ [0, 0, 0, 0, 0],
377
+ [0, 0, 0, 0, 0],
378
+ [0, 0, 0, 0, 0],
379
+ [0, 0, 0, 0, 0],
380
+ [0, 0, 0, 0, 0],
381
+ [1, 0, 1, 0, 164],
382
+ [0, 0, 0, 0, 0],
383
+ [0, 0, 0, 0, 0],
384
+ [1, 0, -1, 0, 161],
385
+ [0, 0, 0, 0, 0],
386
+ [0, 0, 0, 0, 0],
387
+ [0, 0, 0, 0, 0],
388
+ [0, 0, 0, 0, 0],
389
+ [0, 0, 0, 0, 0],
390
+ [0, 0, 0, 0, 0],
391
+ [0, 0, 0, 0, 0],
392
+ [0, 0, 0, 0, 0],
393
+ [1, 0, 0, 1, 152],
394
+ [0, 0, 0, 0, 0],
395
+ [0, 0, 0, 0, 0],
396
+ [0, 0, 0, 0, 0],
397
+ [0, 0, 0, 0, 0],
398
+ [0, 0, 0, 0, 0],
399
+ [0, 0, 0, 0, 0],
400
+ [1, 0, 0, 1, 145],
401
+ [1, 0, 0, 1, 144],
402
+ [0, 0, 0, 0, 0],
403
+ [0, 0, 0, 0, 0],
404
+ [0, 0, 0, 0, 0],
405
+ [0, 0, 0, 0, 0],
406
+ [0, 0, 0, 0, 0],
407
+ [0, 0, 0, 0, 0],
408
+ [1, 0, 0, -1, 137],
409
+ [0, 0, 0, 0, 0],
410
+ [0, 0, 0, 0, 0],
411
+ [0, 0, 0, 0, 0],
412
+ [1, 0, 1, 0, 133],
413
+ [1, 0, 1, 0, 132],
414
+ [1, 1, 0, 0, 131],
415
+ [1, 1, 0, 0, 130],
416
+ [0, 0, 0, 0, 0],
417
+ [0, 0, 0, 0, 0],
418
+ [0, 0, 0, 0, 0],
419
+ [0, 0, 0, 0, 0],
420
+ [0, 0, 0, 0, 0],
421
+ [0, 0, 0, 0, 0],
422
+ [0, 0, 0, 0, 0],
423
+ [0, 0, 0, 0, 0],
424
+ [0, 0, 0, 0, 0],
425
+ [0, 0, 0, 0, 0],
426
+ [0, 0, 0, 0, 0],
427
+ [0, 0, 0, 0, 0],
428
+ [0, 0, 0, 0, 0],
429
+ [0, 0, 0, 0, 0],
430
+ [0, 0, 0, 0, 0],
431
+ [0, 0, 0, 0, 0],
432
+ [0, 0, 0, 0, 0],
433
+ [0, 0, 0, 0, 0],
434
+ [0, 0, 0, 0, 0],
435
+ [0, 0, 0, 0, 0],
436
+ [0, 0, 0, 0, 0],
437
+ [0, 0, 0, 0, 0],
438
+ [0, 0, 0, 0, 0],
439
+ [0, 0, 0, 0, 0],
440
+ [0, 0, 0, 0, 0],
441
+ [0, 0, 0, 0, 0],
442
+ [0, 0, 0, 0, 0],
443
+ [0, 0, 0, 0, 0],
444
+ [0, 0, 0, 0, 0],
445
+ [1, 0, 0, 1, 100],
446
+ [0, 0, 0, 0, 0],
447
+ [1, 0, 0, 1, 98],
448
+ [0, 0, 0, 0, 0],
449
+ [1, 0, 0, 1, 96],
450
+ [0, 0, 0, 0, 0],
451
+ [0, 0, 0, 0, 0],
452
+ [0, 0, 0, 0, 0],
453
+ [0, 0, 0, 0, 0],
454
+ [0, 0, 0, 0, 0],
455
+ [0, 0, 0, 0, 0],
456
+ [0, 0, 0, 0, 0],
457
+ [1, 0, 1, 0, 88],
458
+ [0, 0, 0, 0, 0],
459
+ [0, 0, 0, 0, 0],
460
+ [0, 0, 0, 0, 0],
461
+ [0, 0, 0, 0, 0],
462
+ [0, 0, 0, 0, 0],
463
+ [1, 0, -1, 0, 82],
464
+ [0, 0, 0, 0, 0],
465
+ [0, 0, 0, 0, 0],
466
+ [0, 0, 0, 0, 0],
467
+ [0, 0, 0, 0, 0],
468
+ [0, 0, 0, 0, 0],
469
+ [0, 0, 0, 0, 0],
470
+ [0, 0, 0, 0, 0],
471
+ [1, 0, 1, 0, 74],
472
+ [0, 0, 0, 0, 0],
473
+ [1, 0, 1, 0, 72],
474
+ [0, 0, 0, 0, 0],
475
+ [1, 0, 0, -1, 70],
476
+ [0, 0, 0, 0, 0],
477
+ [0, 0, 0, 0, 0],
478
+ [1, -1, 0, 0, 67],
479
+ [0, 0, 0, 0, 0],
480
+ [1, -1, 0, 0, 65],
481
+ [0, 0, 0, 0, 0],
482
+ [0, 0, 0, 0, 0],
483
+ [0, 0, 0, 0, 0],
484
+ [0, 0, 0, 0, 0],
485
+ [0, 0, 0, 0, 0],
486
+ [0, 0, 0, 0, 0],
487
+ [0, 0, 0, 0, 0],
488
+ [0, 0, 0, 0, 0],
489
+ [1, 1, 0, 0, 56],
490
+ [0, 0, 0, 0, 0],
491
+ [0, 0, 0, 0, 0],
492
+ [0, 0, 0, 0, 0],
493
+ [1, -1, 0, 0, 52],
494
+ [0, 0, 0, 0, 0],
495
+ [0, 0, 0, 0, 0],
496
+ [0, 0, 0, 0, 0],
497
+ [0, 0, 0, 0, 0],
498
+ [0, 0, 0, 0, 0],
499
+ [0, 0, 0, 0, 0],
500
+ [0, 0, 0, 0, 0],
501
+ [1, 1, 0, 0, 44],
502
+ [0, 0, 0, 0, 0],
503
+ [0, 0, 0, 0, 0],
504
+ [0, 0, 0, 0, 0],
505
+ [1, 1, 0, 0, 40],
506
+ [0, 0, 0, 0, 0],
507
+ [1, 0, 0, -1, 38],
508
+ [1, 0, -1, 0, 37],
509
+ [0, 0, 0, 0, 0],
510
+ [0, 0, 0, 0, 0],
511
+ [0, 0, 0, 0, 0],
512
+ [1, 0, -1, 0, 33],
513
+ [0, 0, 0, 0, 0],
514
+ [0, 0, 0, 0, 0],
515
+ [0, 0, 0, 0, 0],
516
+ [0, 0, 0, 0, 0],
517
+ [1, -1, 0, 0, 28],
518
+ [0, 0, 0, 0, 0],
519
+ [1, 0, -1, 0, 26],
520
+ [1, 0, 0, -1, 25],
521
+ [0, 0, 0, 0, 0],
522
+ [0, 0, 0, 0, 0],
523
+ [0, 0, 0, 0, 0],
524
+ [0, 0, 0, 0, 0],
525
+ [1, -1, 0, 0, 20],
526
+ [0, 0, 0, 0, 0],
527
+ [1, 0, -1, 0, 18],
528
+ [0, 0, 0, 0, 0],
529
+ [0, 0, 0, 0, 0],
530
+ [0, 0, 0, 0, 0],
531
+ [0, 0, 0, 0, 0],
532
+ [0, 0, 0, 0, 0],
533
+ [0, 0, 0, 0, 0],
534
+ [0, 0, 0, 0, 0],
535
+ [0, 0, 0, 0, 0],
536
+ [1, 0, 0, -1, 9],
537
+ [0, 0, 0, 0, 0],
538
+ [0, 0, 0, 0, 0],
539
+ [1, 0, 0, -1, 6],
540
+ [0, 0, 0, 0, 0],
541
+ [0, 0, 0, 0, 0],
542
+ [0, 0, 0, 0, 0],
543
+ [0, 0, 0, 0, 0],
544
+ [0, 0, 0, 0, 0],
545
+ [0, 0, 0, 0, 0]
546
+ ]
sparc3d/ops/sparse_backend.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse-convolution primitives built on spconv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from math import prod
6
+ from typing import Any, Optional, Tuple
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ _SPARSE_CONV_FLAG = "_sparc3d_sparse_conv"
12
+ """Instance attribute marking a module that consumes and returns a sparse tensor."""
13
+
14
+ _SPCONV_INT32_BYTE_LIMIT = 2**31 - 1
15
+ """Largest input matrix spconv's implicit-GEMM indexing can address."""
16
+
17
+ _spconv_conv_class: Optional[type] = None
18
+
19
+
20
+ def _spconv():
21
+ """Import spconv's PyTorch namespace, with an actionable error if it is absent."""
22
+ try:
23
+ import spconv.pytorch as spconv
24
+ except ImportError as error:
25
+ raise ImportError(
26
+ "the sparse autoencoder needs spconv; install sparc3d[inference]"
27
+ ) from error
28
+ return spconv
29
+
30
+
31
+ def _submanifold_conv_class() -> type:
32
+ """Build (once) a submanifold convolution that ignores autocast and preserves dtype.
33
+
34
+ The released weights require a convolution that returns whatever precision its input
35
+ features carry and casts its kernel to match, regardless of any enclosing autocast
36
+ context. Stock spconv does the opposite: it downcasts under autocast, and outside one
37
+ it has no kernel for half features at all. The subclass restores the required
38
+ behaviour, at the cost of running the GEMM in the weight's precision.
39
+ """
40
+ global _spconv_conv_class
41
+ if _spconv_conv_class is not None:
42
+ return _spconv_conv_class
43
+
44
+ spconv = _spconv()
45
+
46
+ class SubMConv3d(spconv.SubMConv3d):
47
+ def _large_forward(self, input: Any) -> Any:
48
+ """Evaluate a submanifold convolution without cumm's int32 GEMM limit."""
49
+ from spconv.pytorch import ops
50
+ from spconv.pytorch.core import IndiceData
51
+
52
+ indice_key = (
53
+ f"{self.indice_key}_large" if self.indice_key is not None else None
54
+ )
55
+ data = input.find_indice_pair(indice_key)
56
+ if data is None:
57
+ out_indices, pairs, pair_counts = ops.get_indice_pairs(
58
+ input.indices,
59
+ input.batch_size,
60
+ input.spatial_shape,
61
+ spconv.ConvAlgo.Native,
62
+ self.kernel_size,
63
+ self.stride,
64
+ self.padding,
65
+ self.dilation,
66
+ self.output_padding,
67
+ True,
68
+ False,
69
+ )
70
+ data = IndiceData(
71
+ out_indices,
72
+ input.indices,
73
+ pairs,
74
+ pair_counts,
75
+ input.spatial_shape,
76
+ input.spatial_shape,
77
+ is_subm=True,
78
+ algo=spconv.ConvAlgo.Native,
79
+ ksize=self.kernel_size,
80
+ stride=self.stride,
81
+ padding=self.padding,
82
+ dilation=self.dilation,
83
+ )
84
+ else:
85
+ out_indices = data.out_indices
86
+ pairs = data.indice_pairs
87
+ pair_counts = data.indice_pair_num
88
+
89
+ features = input.features
90
+ kernel_volume = prod(self.kernel_size)
91
+ weights = self.weight.reshape(
92
+ self.out_channels, kernel_volume, self.in_channels
93
+ )
94
+ center = kernel_volume // 2
95
+ output = torch.empty(
96
+ (features.shape[0], self.out_channels),
97
+ dtype=features.dtype,
98
+ device=features.device,
99
+ )
100
+ chunk_rows = 250_000
101
+ for start in range(0, len(features), chunk_rows):
102
+ stop = min(start + chunk_rows, len(features))
103
+ output[start:stop] = features[start:stop] @ weights[:, center, :].T
104
+
105
+ def accumulate(
106
+ source: torch.Tensor,
107
+ target: torch.Tensor,
108
+ weight: torch.Tensor,
109
+ ) -> None:
110
+ for start in range(0, len(source), chunk_rows):
111
+ stop = min(start + chunk_rows, len(source))
112
+ contribution = features[source[start:stop]] @ weight.T
113
+ output.index_add_(0, target[start:stop], contribution)
114
+
115
+ for offset in range(center):
116
+ count = int(pair_counts[offset])
117
+ source = pairs[0, offset, :count].long()
118
+ target = pairs[1, offset, :count].long()
119
+ accumulate(source, target, weights[:, offset, :])
120
+ accumulate(target, source, weights[:, kernel_volume - 1 - offset, :])
121
+ if self.bias is not None:
122
+ output.add_(self.bias)
123
+
124
+ result = input.replace_feature(output)
125
+ if indice_key is not None:
126
+ result.indice_dict[indice_key] = data
127
+ result.indices = out_indices
128
+ return result
129
+
130
+ def forward(self, input: Any) -> Any:
131
+ feats_dtype = input.features.dtype
132
+ matrix_element_size = max(
133
+ input.features.element_size(), self.weight.element_size()
134
+ )
135
+ input_bytes = input.features.numel() * matrix_element_size
136
+ with torch.autocast("cuda", enabled=False):
137
+ if feats_dtype != self.weight.dtype:
138
+ input = input.replace_feature(input.features.to(self.weight.dtype))
139
+ if input_bytes >= _SPCONV_INT32_BYTE_LIMIT:
140
+ # Both cumm convolution algorithms flatten the feature matrix
141
+ # with int32 indices. Chunked PyTorch GEMMs retain the exact
142
+ # native pair traversal for final 1536-scale decoding.
143
+ out = self._large_forward(input)
144
+ else:
145
+ out = super().forward(input)
146
+ if out.features.dtype != feats_dtype:
147
+ out = out.replace_feature(out.features.to(feats_dtype))
148
+ return out
149
+
150
+ _spconv_conv_class = SubMConv3d
151
+ return _spconv_conv_class
152
+
153
+
154
+ def Conv3d(
155
+ in_channels: int,
156
+ out_channels: int,
157
+ kernel_size: int = 3,
158
+ stride: int = 1,
159
+ padding: Optional[int] = None,
160
+ bias: bool = True,
161
+ ) -> nn.Module:
162
+ """Build a submanifold sparse convolution.
163
+
164
+ A submanifold convolution keeps the input's occupied coordinates and their row
165
+ order, which is what lets the surrounding code index features positionally.
166
+
167
+ Args:
168
+ in_channels: Input channels.
169
+ out_channels: Output channels.
170
+ kernel_size: Cubic kernel extent; must be odd.
171
+ stride: Only ``1`` is supported; a strided convolution would change the
172
+ occupied set and is not part of this architecture.
173
+ padding: Only the submanifold value ``kernel_size // 2`` (or ``None``, which
174
+ means the same) is supported.
175
+ bias: Whether to add a per-output-channel bias.
176
+
177
+ Returns:
178
+ A module taking and returning an spconv sparse tensor.
179
+
180
+ Raises:
181
+ ValueError: ``kernel_size`` is even, or ``stride``/``padding`` are not the
182
+ submanifold configuration.
183
+ """
184
+ if kernel_size % 2 == 0:
185
+ raise ValueError(f"kernel_size must be odd, got {kernel_size}")
186
+ if stride != 1:
187
+ raise ValueError(f"only stride 1 is supported, got {stride}")
188
+ if padding is not None and padding != kernel_size // 2:
189
+ raise ValueError(
190
+ f"only submanifold padding {kernel_size // 2} is supported, got {padding}"
191
+ )
192
+
193
+ import os
194
+
195
+ extra = {}
196
+ if os.environ.get("SPCONV_ALGO", "").lower() == "native":
197
+ # HF Space patch: the implicit-GEMM kernels shipped by spconv-cu120 are not
198
+ # built for Blackwell (sm_120); the native gather-GEMM-scatter path is.
199
+ extra["algo"] = _spconv().ConvAlgo.Native
200
+ conv = _submanifold_conv_class()(
201
+ in_channels,
202
+ out_channels,
203
+ kernel_size=kernel_size,
204
+ bias=bias,
205
+ **extra,
206
+ # Submanifold output coordinates always equal the input's, so every
207
+ # convolution over one tensor lineage can share the cached index pairs.
208
+ indice_key=f"subm{kernel_size}",
209
+ )
210
+ setattr(conv, _SPARSE_CONV_FLAG, True)
211
+ return conv
212
+
213
+
214
+ def is_sparse_conv(module: nn.Module) -> bool:
215
+ """Return whether ``module`` was built by :func:`Conv3d`."""
216
+ return getattr(module, _SPARSE_CONV_FLAG, False)
217
+
218
+
219
+ def sparse_tensor(
220
+ feats: torch.Tensor,
221
+ coords: torch.Tensor,
222
+ spatial_shape: Optional[Tuple[int, int, int]] = None,
223
+ ) -> Any:
224
+ """Wrap features and coordinates in an spconv sparse tensor.
225
+
226
+ Args:
227
+ feats: Features ``[N, C]``.
228
+ coords: Integer coordinates ``[N, 4]`` laid out as ``[batch, x, y, z]``.
229
+ spatial_shape: Grid extent per axis; derived from ``coords`` when ``None``.
230
+
231
+ Returns:
232
+ The spconv sparse tensor.
233
+
234
+ Raises:
235
+ ValueError: ``coords`` is not ``[N, 4]``, holds negative entries, or exceeds
236
+ ``spatial_shape``.
237
+ """
238
+ if coords.ndim != 2 or coords.shape[1] != 4:
239
+ raise ValueError(f"expected coords [N, 4], got {tuple(coords.shape)}")
240
+ coords = coords.int().contiguous()
241
+
242
+ spconv = _spconv()
243
+ grid = coords[:, 1:]
244
+ if int(grid.min()) < 0:
245
+ raise ValueError("grid coordinates must be non-negative")
246
+ extent = (grid.max(dim=0).values + 1).tolist()
247
+ if spatial_shape is None:
248
+ spatial_shape = tuple(extent)
249
+ elif any(bound < size for bound, size in zip(spatial_shape, extent)):
250
+ raise ValueError(
251
+ f"grid coordinates reach {extent}, outside spatial_shape {spatial_shape}"
252
+ )
253
+ return spconv.SparseConvTensor(
254
+ features=feats,
255
+ indices=coords,
256
+ spatial_shape=list(spatial_shape),
257
+ batch_size=int(coords[:, 0].max()) + 1,
258
+ )
259
+
260
+
261
+ def get_feats(tensor: Any) -> torch.Tensor:
262
+ """Return the feature matrix of a sparse tensor."""
263
+ return tensor.features
264
+
265
+
266
+ def replace_feats(tensor: Any, feats: torch.Tensor) -> Any:
267
+ """Return a sparse tensor with the same coordinates and new features.
268
+
269
+ spconv rejects assignment to ``features``, so the result may be a different
270
+ object than ``tensor``; callers must use the returned value.
271
+
272
+ Args:
273
+ tensor: Sparse tensor.
274
+ feats: Replacement features ``[N, C]``, one row per coordinate.
275
+
276
+ Returns:
277
+ The updated sparse tensor.
278
+
279
+ Raises:
280
+ ValueError: ``feats`` has a different number of rows than the tensor holds.
281
+ """
282
+ current = get_feats(tensor)
283
+ if feats.shape[0] != current.shape[0]:
284
+ raise ValueError(
285
+ f"expected {current.shape[0]} rows to match the coordinates, "
286
+ f"got {feats.shape[0]}"
287
+ )
288
+ return tensor.replace_feature(feats)
sparc3d/ops/triton/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Triton implementations of SPARC3D surface voxelization."""
2
+
3
+ from sparc3d.ops.triton.voxelize import coarse_voxelize
4
+
5
+ __all__ = [
6
+ "coarse_voxelize",
7
+ ]
sparc3d/ops/triton/voxelize.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Triton surface-voxelization kernels and CSR construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Tuple
6
+
7
+ import torch
8
+ import triton
9
+ import triton.language as tl
10
+
11
+
12
+ @triton.jit
13
+ def _axis_overlaps(
14
+ v0x,
15
+ v0y,
16
+ v0z,
17
+ v1x,
18
+ v1y,
19
+ v1z,
20
+ v2x,
21
+ v2y,
22
+ v2z,
23
+ ax,
24
+ ay,
25
+ az,
26
+ ):
27
+ p0 = v0x * ax + v0y * ay + v0z * az
28
+ p1 = v1x * ax + v1y * ay + v1z * az
29
+ p2 = v2x * ax + v2y * ay + v2z * az
30
+ lower = tl.minimum(tl.minimum(p0, p1), p2)
31
+ upper = tl.maximum(tl.maximum(p0, p1), p2)
32
+ radius = 0.5 * (tl.abs(ax) + tl.abs(ay) + tl.abs(az))
33
+ return (lower <= radius) & (upper >= -radius)
34
+
35
+
36
+ @triton.jit
37
+ def _triangle_overlaps_voxel(
38
+ a0,
39
+ a1,
40
+ a2,
41
+ b0,
42
+ b1,
43
+ b2,
44
+ c0,
45
+ c1,
46
+ c2,
47
+ x,
48
+ y,
49
+ z,
50
+ ):
51
+ center_x = x.to(tl.float32) + 0.5
52
+ center_y = y.to(tl.float32) + 0.5
53
+ center_z = z.to(tl.float32) + 0.5
54
+ v0x, v0y, v0z = a0 - center_x, a1 - center_y, a2 - center_z
55
+ v1x, v1y, v1z = b0 - center_x, b1 - center_y, b2 - center_z
56
+ v2x, v2y, v2z = c0 - center_x, c1 - center_y, c2 - center_z
57
+
58
+ overlaps = (
59
+ (tl.minimum(tl.minimum(v0x, v1x), v2x) <= 0.5)
60
+ & (tl.maximum(tl.maximum(v0x, v1x), v2x) >= -0.5)
61
+ & (tl.minimum(tl.minimum(v0y, v1y), v2y) <= 0.5)
62
+ & (tl.maximum(tl.maximum(v0y, v1y), v2y) >= -0.5)
63
+ & (tl.minimum(tl.minimum(v0z, v1z), v2z) <= 0.5)
64
+ & (tl.maximum(tl.maximum(v0z, v1z), v2z) >= -0.5)
65
+ )
66
+
67
+ e0x, e0y, e0z = v1x - v0x, v1y - v0y, v1z - v0z
68
+ e1x, e1y, e1z = v2x - v1x, v2y - v1y, v2z - v1z
69
+ e2x, e2y, e2z = v0x - v2x, v0y - v2y, v0z - v2z
70
+
71
+ overlaps &= _axis_overlaps(
72
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e0z, -e0y
73
+ )
74
+ overlaps &= _axis_overlaps(
75
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e0z, 0.0, e0x
76
+ )
77
+ overlaps &= _axis_overlaps(
78
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e0y, -e0x, 0.0
79
+ )
80
+ overlaps &= _axis_overlaps(
81
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e1z, -e1y
82
+ )
83
+ overlaps &= _axis_overlaps(
84
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e1z, 0.0, e1x
85
+ )
86
+ overlaps &= _axis_overlaps(
87
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e1y, -e1x, 0.0
88
+ )
89
+ overlaps &= _axis_overlaps(
90
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e2z, -e2y
91
+ )
92
+ overlaps &= _axis_overlaps(
93
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e2z, 0.0, e2x
94
+ )
95
+ overlaps &= _axis_overlaps(
96
+ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e2y, -e2x, 0.0
97
+ )
98
+
99
+ nx = e0y * e1z - e0z * e1y
100
+ ny = e0z * e1x - e0x * e1z
101
+ nz = e0x * e1y - e0y * e1x
102
+ plane_offset = -(nx * v0x + ny * v0y + nz * v0z)
103
+ plane_radius = 0.5 * (tl.abs(nx) + tl.abs(ny) + tl.abs(nz))
104
+ return overlaps & (tl.abs(plane_offset) <= plane_radius)
105
+
106
+
107
+ @triton.jit
108
+ def _load_triangle(vertices_ptr, faces_ptr, face_id):
109
+ index0 = tl.load(faces_ptr + face_id * 3)
110
+ index1 = tl.load(faces_ptr + face_id * 3 + 1)
111
+ index2 = tl.load(faces_ptr + face_id * 3 + 2)
112
+ a0 = tl.load(vertices_ptr + index0 * 3)
113
+ a1 = tl.load(vertices_ptr + index0 * 3 + 1)
114
+ a2 = tl.load(vertices_ptr + index0 * 3 + 2)
115
+ b0 = tl.load(vertices_ptr + index1 * 3)
116
+ b1 = tl.load(vertices_ptr + index1 * 3 + 1)
117
+ b2 = tl.load(vertices_ptr + index1 * 3 + 2)
118
+ c0 = tl.load(vertices_ptr + index2 * 3)
119
+ c1 = tl.load(vertices_ptr + index2 * 3 + 1)
120
+ c2 = tl.load(vertices_ptr + index2 * 3 + 2)
121
+ return a0, a1, a2, b0, b1, b2, c0, c1, c2
122
+
123
+
124
+ @triton.jit
125
+ def _count_intersections_kernel(
126
+ vertices_ptr,
127
+ faces_ptr,
128
+ counts_ptr,
129
+ resolution: tl.constexpr,
130
+ ):
131
+ face_id = tl.program_id(0)
132
+ a0, a1, a2, b0, b1, b2, c0, c1, c2 = _load_triangle(
133
+ vertices_ptr, faces_ptr, face_id
134
+ )
135
+ start_x = tl.maximum(
136
+ tl.floor(tl.minimum(tl.minimum(a0, b0), c0)).to(tl.int32), 0
137
+ )
138
+ start_y = tl.maximum(
139
+ tl.floor(tl.minimum(tl.minimum(a1, b1), c1)).to(tl.int32), 0
140
+ )
141
+ start_z = tl.maximum(
142
+ tl.floor(tl.minimum(tl.minimum(a2, b2), c2)).to(tl.int32), 0
143
+ )
144
+ end_x = tl.minimum(
145
+ tl.floor(tl.maximum(tl.maximum(a0, b0), c0)).to(tl.int32),
146
+ resolution - 1,
147
+ )
148
+ end_y = tl.minimum(
149
+ tl.floor(tl.maximum(tl.maximum(a1, b1), c1)).to(tl.int32),
150
+ resolution - 1,
151
+ )
152
+ end_z = tl.minimum(
153
+ tl.floor(tl.maximum(tl.maximum(a2, b2), c2)).to(tl.int32),
154
+ resolution - 1,
155
+ )
156
+
157
+ count = 0
158
+ for z in tl.range(start_z, end_z + 1):
159
+ for y in tl.range(start_y, end_y + 1):
160
+ for x in tl.range(start_x, end_x + 1):
161
+ count += _triangle_overlaps_voxel(
162
+ a0, a1, a2, b0, b1, b2, c0, c1, c2, x, y, z
163
+ ).to(tl.int32)
164
+ tl.store(counts_ptr + face_id, count)
165
+
166
+
167
+ @triton.jit
168
+ def _emit_intersections_kernel(
169
+ vertices_ptr,
170
+ faces_ptr,
171
+ offsets_ptr,
172
+ keys_ptr,
173
+ face_ids_ptr,
174
+ resolution: tl.constexpr,
175
+ ):
176
+ face_id = tl.program_id(0)
177
+ a0, a1, a2, b0, b1, b2, c0, c1, c2 = _load_triangle(
178
+ vertices_ptr, faces_ptr, face_id
179
+ )
180
+ start_x = tl.maximum(
181
+ tl.floor(tl.minimum(tl.minimum(a0, b0), c0)).to(tl.int32), 0
182
+ )
183
+ start_y = tl.maximum(
184
+ tl.floor(tl.minimum(tl.minimum(a1, b1), c1)).to(tl.int32), 0
185
+ )
186
+ start_z = tl.maximum(
187
+ tl.floor(tl.minimum(tl.minimum(a2, b2), c2)).to(tl.int32), 0
188
+ )
189
+ end_x = tl.minimum(
190
+ tl.floor(tl.maximum(tl.maximum(a0, b0), c0)).to(tl.int32),
191
+ resolution - 1,
192
+ )
193
+ end_y = tl.minimum(
194
+ tl.floor(tl.maximum(tl.maximum(a1, b1), c1)).to(tl.int32),
195
+ resolution - 1,
196
+ )
197
+ end_z = tl.minimum(
198
+ tl.floor(tl.maximum(tl.maximum(a2, b2), c2)).to(tl.int32),
199
+ resolution - 1,
200
+ )
201
+
202
+ output = tl.load(offsets_ptr + face_id)
203
+ for z in tl.range(start_z, end_z + 1):
204
+ for y in tl.range(start_y, end_y + 1):
205
+ for x in tl.range(start_x, end_x + 1):
206
+ hit = _triangle_overlaps_voxel(
207
+ a0, a1, a2, b0, b1, b2, c0, c1, c2, x, y, z
208
+ )
209
+ key = (
210
+ (z.to(tl.int64) << 22)
211
+ | (y.to(tl.int64) << 11)
212
+ | x.to(tl.int64)
213
+ )
214
+ tl.store(keys_ptr + output, key, mask=hit)
215
+ tl.store(face_ids_ptr + output, face_id, mask=hit)
216
+ output += hit.to(tl.int32)
217
+
218
+
219
+ def coarse_voxelize(
220
+ vertices: torch.Tensor,
221
+ faces: torch.Tensor,
222
+ resolution: int,
223
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
224
+ """Voxelize triangles and return voxel-to-face CSR tensors.
225
+
226
+ Args:
227
+ vertices: CUDA float coordinates with shape ``[V, 3]`` in voxel space.
228
+ faces: CUDA triangle indices with shape ``[F, 3]``.
229
+ resolution: Side length of the voxel grid.
230
+
231
+ Returns:
232
+ Encoded coordinates, grouped face IDs, CSR starts, CSR counts, and
233
+ per-face voxel counts.
234
+
235
+ Raises:
236
+ ValueError: Inputs do not satisfy the low-level voxelizer contract.
237
+ """
238
+ if not vertices.is_cuda or not faces.is_cuda:
239
+ raise ValueError("vertices and faces must be CUDA tensors")
240
+ if vertices.device != faces.device:
241
+ raise ValueError("vertices and faces must be on the same device")
242
+ if vertices.ndim != 2 or vertices.shape[1] != 3:
243
+ raise ValueError("vertices must have shape [num_vertices, 3]")
244
+ if faces.ndim != 2 or faces.shape[1] != 3:
245
+ raise ValueError("faces must have shape [num_faces, 3]")
246
+ if resolution < 1 or resolution > 2048:
247
+ raise ValueError(f"resolution must be in [1, 2048], got {resolution}")
248
+
249
+ vertices = vertices.to(torch.float32).contiguous()
250
+ faces = faces.to(torch.int32).contiguous()
251
+ num_faces = faces.shape[0]
252
+ counts = torch.zeros(num_faces, dtype=torch.int32, device=faces.device)
253
+ if num_faces == 0:
254
+ empty_u64 = torch.empty(0, dtype=torch.uint64, device=faces.device)
255
+ empty_u32 = torch.empty(0, dtype=torch.uint32, device=faces.device)
256
+ return empty_u64, empty_u32, counts, counts, counts
257
+
258
+ _count_intersections_kernel[(num_faces,)](
259
+ vertices, faces, counts, resolution, num_warps=1
260
+ )
261
+ inclusive = torch.cumsum(counts, dim=0, dtype=torch.int32)
262
+ offsets = torch.cat(
263
+ [torch.zeros(1, dtype=torch.int32, device=faces.device), inclusive[:-1]]
264
+ )
265
+ total = int(inclusive[-1].item())
266
+ if total == 0:
267
+ empty_u64 = torch.empty(0, dtype=torch.uint64, device=faces.device)
268
+ empty_u32 = torch.empty(0, dtype=torch.uint32, device=faces.device)
269
+ empty_i32 = torch.empty(0, dtype=torch.int32, device=faces.device)
270
+ return empty_u64, empty_u32, empty_i32, empty_i32, counts
271
+
272
+ keys = torch.empty(total, dtype=torch.int64, device=faces.device)
273
+ face_ids = torch.empty(total, dtype=torch.int32, device=faces.device)
274
+ _emit_intersections_kernel[(num_faces,)](
275
+ vertices,
276
+ faces,
277
+ offsets,
278
+ keys,
279
+ face_ids,
280
+ resolution,
281
+ num_warps=1,
282
+ )
283
+
284
+ sorted_keys, order = torch.sort(keys, stable=True)
285
+ grouped_faces = face_ids[order]
286
+ unique_keys, unique_counts = torch.unique_consecutive(
287
+ sorted_keys, return_counts=True
288
+ )
289
+ unique_counts = unique_counts.to(torch.int32)
290
+ unique_starts = torch.cat(
291
+ [
292
+ torch.zeros(1, dtype=torch.int32, device=faces.device),
293
+ torch.cumsum(unique_counts, dim=0, dtype=torch.int32)[:-1],
294
+ ]
295
+ )
296
+ return (
297
+ unique_keys.to(torch.uint64),
298
+ grouped_faces.to(torch.uint32),
299
+ unique_starts,
300
+ unique_counts,
301
+ counts,
302
+ )